Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/dashboard-json-upload

pull/13949/head
LeoMorgan113 1 year ago
parent
commit
ebd7b2ed5f
  1. 175
      README.md
  2. 3
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  3. 35
      application/src/main/data/json/system/widget_types/api_usage.json
  4. 10
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  5. 3
      application/src/main/resources/thingsboard.yml
  6. 46
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
  7. 11
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java
  8. 4
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  9. 54
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.html
  10. 125
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss
  11. 177
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  12. 85
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html
  13. 56
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss
  14. 156
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts
  15. 106
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts
  16. 99
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html
  17. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss
  18. 211
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts
  19. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  20. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  21. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  22. 5
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  23. 4
      ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts
  24. 68
      ui-ngx/src/app/shared/components/icon.component.ts
  25. 7
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  26. 137
      ui-ngx/src/app/shared/components/material-icons.component.html
  27. 12
      ui-ngx/src/app/shared/components/material-icons.component.ts
  28. 3
      ui-ngx/src/app/shared/models/constants.ts
  29. 1
      ui-ngx/src/app/shared/models/resource.models.ts
  30. 10732
      ui-ngx/src/assets/dashboard/api_usage.json
  31. 65
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  32. 4
      ui-ngx/src/scss/mixins.scss
  33. 2
      ui-ngx/src/styles.scss

175
README.md

@ -1,43 +1,150 @@
# ThingsBoard ![banner](https://github.com/user-attachments/assets/3584b592-33dd-4fb4-91d4-47b62b34806c)
[![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)
<div align="center">
# Open-source IoT platform for data collection, processing, visualization, and device management.
</div>
<br>
<div align="center">
💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/)&ensp;•&ensp;🌐 [Website](https://thingsboard.io/)&ensp;•&ensp;📚 [Documentation](https://thingsboard.io/docs/)&ensp;•&ensp;📔 [Blog](https://thingsboard.io/blog/)&ensp;•&ensp;▶️ [Live demo](https://demo.thingsboard.io/signup)&ensp;•&ensp;🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all)
</div>
## 🚀 Installation options
* Install ThingsBoard [On-premise](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise)
* Try [ThingsBoard Cloud](https://thingsboard.io/installations/)
* or [Use our Live demo](https://demo.thingsboard.io/signup)
## 💡 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
<table>
<tr>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/255cca4f-b111-44e8-99ea-0af55f8e3681" alt="Provision and manage devices and assets" width="378" />
<h3>Provision and manage <br> devices and assets</h3>
</div>
<div align="center">
<p>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.</p>
</div>
<br>
<div align="center">
<a href="https://thingsboard.io/docs/user-guide/entities-and-relations/">Read more ➜</a>
</div>
<br>
</td>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/24b41d10-150a-42dd-ab1a-32ac9b5978c1" alt="Collect and visualize your data" width="378" />
<h3>Collect and visualize <br> your data</h3>
</div>
<div align="center">
<p>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.</p>
</div>
<br>
<div align="center">
<a href="https://thingsboard.io/iot-data-visualization/">Read more ➜</a>
</div>
<br>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/6f2a6dd2-7b33-4d17-8b92-d1f995adda2c" alt="SCADA Dashboards" width="378" />
<h3>SCADA Dashboards</h3>
</div>
<div align="center">
<p>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.</p>
</div>
<br>
<div align="center">
<a href="https://thingsboard.io/use-cases/scada/">Read more ➜</a>
</div>
<br>
</td>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/c23dcc9b-aeba-40ef-9973-49b953fc1257" alt="Process and React" width="378" />
<h3>Process and React</h3>
</div>
<div align="center">
<p>Define data processing rule chains. Transform and normalize your device data. Raise alarms on incoming telemetry events, attribute updates, device inactivity and user actions.<br></p>
</div>
<br>
<br>
<div align="center">
<a href="https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/">Read more ➜</a>
</div>
<br>
</td>
</tr>
</table>
## ⚙️ 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/)
<div align="center">
[**Read more about Rule Engine ➜**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
</div>
## 📦 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/)
[**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/)
[![Fleet tracking](https://github.com/user-attachments/assets/9e8938ba-ee0c-4599-9494-d74b7de8a63d "Fleet tracking")](https://thingsboard.io/use-cases/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. [**Smart metering**](https://thingsboard.io/smart-metering/)
<img src="./img/logo.png?raw=true" width="100" height="100">
## 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/)
[**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/)
[![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://thingsboard.io/smart-metering/) <div align="center">
[![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/). </div>
## 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)

3
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -13,6 +13,7 @@
"home_page_widgets.quick_links", "home_page_widgets.quick_links",
"home_page_widgets.documentation_links", "home_page_widgets.documentation_links",
"home_page_widgets.dashboards", "home_page_widgets.dashboards",
"home_page_widgets.usage_info" "home_page_widgets.usage_info",
"api_usage"
] ]
} }

35
application/src/main/data/json/system/widget_types/api_usage.json

File diff suppressed because one or more lines are too long

10
application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java

@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -60,7 +61,10 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_
@RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX) @RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX)
@Slf4j @Slf4j
public class RuleEngineController extends BaseController { 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_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. " + 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" + "The generated message contains two important metadata fields:\n\n" +
@ -85,7 +89,7 @@ public class RuleEngineController extends BaseController {
public DeferredResult<ResponseEntity> handleRuleEngineRequest( public DeferredResult<ResponseEntity> handleRuleEngineRequest(
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(null, null, null, DEFAULT_TIMEOUT, requestBody); return handleRuleEngineRequest(null, null, null, defaultResponseTimeout, requestBody);
} }
@ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)",
@ -104,7 +108,7 @@ public class RuleEngineController extends BaseController {
@PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(entityType, entityIdStr, null, DEFAULT_TIMEOUT, requestBody); return handleRuleEngineRequest(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 (handleRuleEngineRequest)",

3
application/src/main/resources/thingsboard.yml

@ -100,6 +100,9 @@ server:
rate_limits: 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 # 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}" 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 # Application info parameters
app: app:

46
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java

@ -16,9 +16,7 @@
package org.thingsboard.rule.engine.action; package org.thingsboard.rule.engine.action;
import com.fasterxml.jackson.databind.JsonNode; 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.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -31,18 +29,21 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@Slf4j 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( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
name = "clear alarm", relationTypes = {"Cleared", "False"}, name = "clear alarm", relationTypes = {"Cleared", "False"},
configClazz = TbClearAlarmNodeConfiguration.class, configClazz = TbClearAlarmNodeConfiguration.class,
nodeDescription = "Clear Alarm", nodeDescription = "Clear Alarm",
nodeDetails = nodeDetails = """
"Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.
"Node output:\n" + 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. " + 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 <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " + Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>.
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.", Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.""",
configDirective = "tbActionNodeClearAlarmConfig", configDirective = "tbActionNodeClearAlarmConfig",
icon = "notifications_off" icon = "notifications_off"
) )
@ -55,22 +56,26 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfig
@Override @Override
protected ListenableFuture<TbAlarmResult> processAlarm(TbContext ctx, TbMsg msg) { protected ListenableFuture<TbAlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); String alarmType = TbNodeUtils.processPattern(config.getAlarmType(), msg);
Alarm alarm;
if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { ListenableFuture<Alarm> alarmFuture;
alarm = ctx.getAlarmService().findAlarmById(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); if (msg.getOriginator().getEntityType() == EntityType.ALARM) {
alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId()));
} else { } else {
alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); alarmFuture = ctx.getAlarmService().findLatestActiveByOriginatorAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), alarmType);
} }
if (alarm != null && !alarm.getStatus().isCleared()) {
return clearAlarm(ctx, msg, alarm); return transformAsync(alarmFuture, alarm -> {
} if (alarm != null && !alarm.getStatus().isCleared()) {
return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); return clearAlarmAsync(ctx, msg, alarm);
}
return immediateFuture(new TbAlarmResult(false, false, false, null));
}, ctx.getDbCallbackExecutor());
} }
private ListenableFuture<TbAlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { private ListenableFuture<TbAlarmResult> clearAlarmAsync(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<JsonNode> asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); ListenableFuture<JsonNode> 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); AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details);
if (result.isSuccessful()) { if (result.isSuccessful()) {
return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm());
@ -79,4 +84,5 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfig
} }
}, ctx.getDbCallbackExecutor()); }, ctx.getDbCallbackExecutor());
} }
} }

11
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.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.function.Consumer; 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.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
@ -112,8 +113,8 @@ class TbClearAlarmNodeTest {
.endTs(oldEndDate) .endTs(oldEndDate)
.build(); .build();
when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(immediateFuture(null));
when(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, "SomeType")).thenReturn(activeAlarm); when(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, "SomeType")).thenReturn(FluentFuture.from(immediateFuture(activeAlarm)));
when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class)))
.thenReturn(AlarmApiCallResult.builder() .thenReturn(AlarmApiCallResult.builder()
.successful(true) .successful(true)
@ -172,8 +173,8 @@ class TbClearAlarmNodeTest {
.build(); .build();
expectedAlarm.setId(id); expectedAlarm.setId(id);
when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(immediateFuture(null));
when(alarmServiceMock.findAlarmById(tenantId, id)).thenReturn(activeAlarm); when(alarmServiceMock.findAlarmByIdAsync(tenantId, id)).thenReturn(immediateFuture(activeAlarm));
when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class)))
.thenReturn(AlarmApiCallResult.builder() .thenReturn(AlarmApiCallResult.builder()
.successful(true) .successful(true)

4
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -29,8 +29,8 @@
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2">
<mat-form-field class="mat-block"> <mat-form-field appearance="outline" subscriptSizing="dynamic" class="mat-block tb-inline-field">
<mat-label translate>attribute.key</mat-label> <mat-label translate>attribute.key</mat-label>
<input matInput formControlName="key" required> <input matInput formControlName="key" required>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('required')"> <mat-error *ngIf="attributeFormGroup.get('key').hasError('required')">

54
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-api-usage-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-api-usage-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-api-usage-content">
<div class="api-items-list">
@for (api of apiUsages; track api){
<div class="api-item {{api.status.value}}"
[class.cursor-pointer]="api.state"
[class.active]="api.state && api.state === currentState"
(click)="updateState($event, api.state)">
<div class="api-item-content">
<div class="api-item-title">{{ api.label }}</div>
<div class="api-item-statistic">
<div class="api-item-statistic-count">{{ api.current.value }} / {{ api.maxLimit.value }}</div>
<div class="api-item-statistic-progress">
<mat-progress-bar mode="determinate"
[value]="api.progress"
></mat-progress-bar>
</div>
<div class="api-item-statistic-status">
<div class="statistic-status">{{ ('api-usage.status.' + api.status.value) | translate }}</div>
<div class="statistic-icon">
@if (api.status.value === 'enabled') {
<tb-icon class="tb-mat-18">check_circle</tb-icon>
} @else {
<tb-icon class="tb-mat-18">warning</tb-icon>
}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
</div>
}
</div>
</div>
</div>

125
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss

@ -0,0 +1,125 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 {
--mdc-linear-progress-active-indicator-color: #{$enabled-color};
}
}
&.disabled {
.api-item-statistic-status {
color: $disabled-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-active-indicator-color: #{$disabled-color};
}
}
&.warning {
.api-item-statistic-status {
color: $warning-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-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 {
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;
font-weight: 500;
color: $tb-primary-color;
}
.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 {
--mdc-linear-progress-track-height: 8px;
--mdc-linear-progress-active-indicator-height: 8px;
padding: 4px 0;
.mat-mdc-progress-bar {
border-radius: 6px;
}
}
}
}
}
}
}
}

177
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -0,0 +1,177 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES 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 { DataKey, 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 {
ApiUsageDataKeysSettings,
apiUsageDefaultSettings,
ApiUsageWidgetSettings
} 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
})
export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
settings: ApiUsageWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
backgroundStyle$: Observable<ComponentStyle>;
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: this.getUniqueDataKeys(this.settings.dataKeys)
}
const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = {
datasources: [ds],
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {
const data = formattedDataFormDatasourceData(subscription.data);
this.apiUsages.forEach(key => {
const progress = 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 = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
});
this.cd.detectChanges();
}
}
};
this.ctx.subscriptionApi.createSubscription(apiUsageSubscriptionOptions, true).subscribe();
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;
}
updateState($event: MouseEvent, stateName: string) {
$event?.preventDefault();
if (stateName?.length) {
this.ctx.stateController.updateState(stateName, this.ctx.stateController.getStateParams(), this.ctx.isMobile);
}
}
parseApiUsages() {
this.settings.dataKeys.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},
});
})
}
getUniqueDataKeys(data: ApiUsageDataKeysSettings[]): DataKey[] {
const seenNames = new Set<string>();
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;
});
};
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();
}
}

85
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html

@ -0,0 +1,85 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="dataKeyFormGroup" class="tb-form-table-row tb-api-usage-data-key-row">
<div class="tb-source-field">
<mat-form-field
class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchDashboardStates.bind(this)"
placeholderText="{{ 'widget-config.set' | translate }}"
style="flex: 1"
formControlName="state">
</tb-string-autocomplete>
</div>
<div class="tb-data-key-fields">
<tb-data-key-input
class="tb-data-key-field"
required
requiredText="{{ 'widgets.api-usage.status-required'}}"
[datasourceType]="DatasourceType.entity"
[entityAliasId]="dsEntityAliasId"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('status')"
formControlName="status">
</tb-data-key-input>
<tb-data-key-input
class="tb-data-key-field"
required
requiredText="{{ 'widgets.api-usage.limit-required'}}"
[datasourceType]="DatasourceType.entity"
[entityAliasId]="dsEntityAliasId"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('maxLimit')"
formControlName="maxLimit">
</tb-data-key-input>
<tb-data-key-input
class="tb-data-key-field"
required
requiredText="{{ 'widgets.api-usage.current-number-required'}}"
[datasourceType]="DatasourceType.entity"
[entityAliasId]="dsEntityAliasId"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('current')"
formControlName="current">
</tb-data-key-input>
</div>
<div class="tb-form-table-row-cell-buttons">
<div class="tb-remove-button">
<button type="button"
mat-icon-button
(click)="dataKeyRemoved.emit()"
matTooltip="{{ 'widgets.api-usage.delete-key' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>

56
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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%;
}
}
}

156
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts

@ -0,0 +1,156 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES 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
})
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<Array<string>> {
return of(this.context.callbacks.fetchDashboardStates(searchText));
}
}

106
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts

@ -0,0 +1,106 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES 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<DataKey>;
generateDataKey: (key: DataKey) => DataKey;
}
export interface ApiUsageWidgetSettings {
dsEntityAliasId: string;
dataKeys: 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: '',
dataKeys: [
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}', 'notificationApiState', 'smsLimit', 'smsCount'),
],
targetDashboardState: 'default',
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '0'
};

99
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html

@ -0,0 +1,99 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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-container [formGroup]="apiUsageWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.datasource</div>
<tb-entity-alias-select
class="tb-entity-alias-field"
tbRequired
[appearance]="'outline'"
[aliasController]="context.aliasController"
formControlName="dsEntityAliasId"
[callbacks]="context.callbacks">
</tb-entity-alias-select>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-table tb-map-data-layers">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.label</div>
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.state-name</div>
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.status</div>
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.limit</div>
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.current-number</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="layerDrop($event)" *ngIf="dataKeysFormArray().controls?.length; else noDataLayers" class="tb-form-table-body">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
*ngFor="let dataKeyControl of dataKeysFormArray().controls; trackBy: trackByDataKey; let $index = index;">
<div class="tb-draggable-form-table-row">
<tb-api-usage-data-key-row class="flex-1"
[context]="context"
[dsEntityAliasId]="apiUsageWidgetSettingsForm.get('dsEntityAliasId').value"
[formControl]="dataKeyControl"
(dataKeyRemoved)="removeDataKey($index)">
</tb-api-usage-data-key-row>
<div class="tb-form-table-row-cell-buttons">
<button [class.tb-hidden]="!dragEnabled"
mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addDataKey()">
{{ 'widgets.api-usage.add-key' | translate }}
</button>
</div>
</div>
<ng-template #noDataLayers>
<span class="tb-prompt flex items-center justify-center">{{ 'widgets.api-usage.no-key' | translate }}</span>
</ng-template>
<tb-string-autocomplete [fetchOptionsFn]="fetchDashboardStates.bind(this)"
additionalClass="tb-suffix-show-on-hover"
appearance="outline"
panelWidth=""
label="{{ 'widgets.api-usage.target-dashboard-state' | translate }}"
style="flex: 1"
formControlName="targetDashboardState">
</tb-string-autocomplete>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>

62
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss

@ -0,0 +1,62 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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;
}
}
}

211
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts

@ -0,0 +1,211 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES 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
} 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
}
],
})
export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
apiUsageWidgetSettingsForm: UntypedFormGroup;
context: ApiUsageSettingsContext;
constructor(protected store: Store<AppState>,
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('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false});
}
dataKeysFormArray(): UntypedFormArray {
return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray;
}
trackByDataKey(index: number): any {
return index;
}
get dragEnabled(): boolean {
return this.dataKeysFormArray().controls.length > 1;
}
layerDrop(event: CdkDragDrop<string[]>) {
const layer = this.dataKeysFormArray().at(event.previousIndex);
this.dataKeysFormArray().removeAt(event.previousIndex);
this.dataKeysFormArray().insert(event.currentIndex, layer);
}
removeDataKey(index: number) {
(this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index);
}
addDataKey() {
const dataKey = {
label: '',
state: '',
status: null,
maxLimit: null,
current: null
};
const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') 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,
dataKeys: settings?.dataKeys,
targetDashboardState: settings?.targetDashboardState,
background: settings?.background,
padding: settings.padding
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.apiUsageWidgetSettingsForm = this.fb.group({
dsEntityAliasId: [settings?.dsEntityAliasId],
dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys),
targetDashboardState: [settings?.targetDashboardState],
background: [settings?.background, []],
padding: [settings.padding, []]
});
}
private prepareDataKeysFormArray(dataKeys: ApiUsageDataKeysSettings[]): UntypedFormArray {
const dataKeysControls: Array<AbstractControl> = [];
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<DataKey> {
return this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(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,
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<Array<string>> {
return of(this.callbacks.fetchDashboardStates(searchText));
}
}

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -375,6 +375,12 @@ import {
ValueStepperWidgetSettingsComponent ValueStepperWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component'; } 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 { 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({ @NgModule({
declarations: [ declarations: [
@ -508,7 +514,9 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings
LabelValueCardWidgetSettingsComponent, LabelValueCardWidgetSettingsComponent,
UnreadNotificationWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent,
MapWidgetSettingsComponent MapWidgetSettingsComponent,
ApiUsageWidgetSettingsComponent,
ApiUsageDataKeyRowComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -647,7 +655,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings
LabelValueCardWidgetSettingsComponent, LabelValueCardWidgetSettingsComponent,
UnreadNotificationWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent,
MapWidgetSettingsComponent MapWidgetSettingsComponent,
ApiUsageWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -94,6 +94,7 @@ import {
SelectMapEntityPanelComponent SelectMapEntityPanelComponent
} from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; } 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 { 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({ @NgModule({
declarations: [ declarations: [
@ -151,7 +152,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane
ScadaSymbolWidgetComponent, ScadaSymbolWidgetComponent,
SelectMapEntityPanelComponent, SelectMapEntityPanelComponent,
MapTimelinePanelComponent, MapTimelinePanelComponent,
MapWidgetComponent MapWidgetComponent,
ApiUsageWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -214,7 +216,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane
UnreadNotificationWidgetComponent, UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent, NotificationTypeFilterPanelComponent,
ScadaSymbolWidgetComponent, ScadaSymbolWidgetComponent,
MapWidgetComponent MapWidgetComponent,
ApiUsageWidgetComponent
], ],
providers: [ providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}, {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule},

1
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -71,6 +71,7 @@
</mat-form-field> </mat-form-field>
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton iconClearButton
allowedCustomIcon
[color]="widgetSettings.get('iconColor').value" [color]="widgetSettings.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </tb-material-icon-select>

5
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -147,6 +147,7 @@ export interface WidgetAction extends IWidgetAction {
export interface IDashboardWidget { export interface IDashboardWidget {
updateWidgetParams(): void; updateWidgetParams(): void;
updateParamsFromData(detectChanges?: boolean): void;
} }
export class WidgetContext { export class WidgetContext {
@ -478,6 +479,10 @@ export class WidgetContext {
} }
} }
updateParamsFromData(detectChanges = false) {
this.dashboardWidget.updateParamsFromData(detectChanges);
}
updateAliases(aliasIds?: Array<string>) { updateAliases(aliasIds?: Array<string>) {
this.aliasController.updateAliases(aliasIds); this.aliasController.updateAliases(aliasIds);
} }

4
ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts

@ -81,7 +81,9 @@ export class JsLibraryTableConfigResolver {
search: 'javascript.search', search: 'javascript.search',
selectedEntities: 'javascript.selected-javascript-resources' selectedEntities: 'javascript.selected-javascript-resources'
}; };
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); this.config.entityResources = {
helpLinkId: 'jsExtension'
};
this.config.headerComponent = JsLibraryTableHeaderComponent; this.config.headerComponent = JsLibraryTableHeaderComponent;
this.config.entityTitle = (resource) => resource ? this.config.entityTitle = (resource) => resource ?

68
ui-ngx/src/app/shared/components/icon.component.ts

@ -33,6 +33,9 @@ import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { ContentObserver } from '@angular/cdk/observers'; import { ContentObserver } from '@angular/cdk/observers';
import { isTbImage } from '@shared/models/resource.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
const _TbIconBase = mixinColor( const _TbIconBase = mixinColor(
class { class {
@ -70,7 +73,7 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
host: { host: {
role: 'img', role: 'img',
class: 'mat-icon notranslate', class: 'mat-icon notranslate',
'[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"', '[attr.data-mat-icon-type]': '_useSvgIcon ? "svg" : (_useImageIcon ? null : "font")',
'[attr.data-mat-icon-name]': '_svgName', '[attr.data-mat-icon-name]': '_svgName',
'[attr.data-mat-icon-namespace]': '_svgNamespace', '[attr.data-mat-icon-namespace]': '_svgNamespace',
'[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"', '[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"',
@ -99,6 +102,9 @@ export class TbIconComponent extends _TbIconBase
private _textElement = null; private _textElement = null;
_useImageIcon = false;
private _imageElement = null;
private _previousPath?: string; private _previousPath?: string;
private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>; private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>;
@ -109,6 +115,8 @@ export class TbIconComponent extends _TbIconBase
private contentObserver: ContentObserver, private contentObserver: ContentObserver,
private renderer: Renderer2, private renderer: Renderer2,
private _iconRegistry: MatIconRegistry, private _iconRegistry: MatIconRegistry,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
@Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation,
private readonly _errorHandler: ErrorHandler) { private readonly _errorHandler: ErrorHandler) {
super(elementRef); super(elementRef);
@ -148,16 +156,29 @@ export class TbIconComponent extends _TbIconBase
private _updateIcon() { private _updateIcon() {
const useSvgIcon = isSvgIcon(this.icon); const useSvgIcon = isSvgIcon(this.icon);
const useImageIcon = isTbImage(this.icon);
if (this._useSvgIcon !== useSvgIcon) { if (this._useSvgIcon !== useSvgIcon) {
this._useSvgIcon = useSvgIcon; this._useSvgIcon = useSvgIcon;
if (!this._useSvgIcon) { if (!this._useSvgIcon) {
this._updateSvgIcon(undefined); this._updateSvgIcon(undefined);
} else { } else {
this._updateFontIcon(undefined); 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) { if (this._useSvgIcon) {
this._updateSvgIcon(this.icon); this._updateSvgIcon(this.icon);
} else if (this._useImageIcon) {
this._updateImageIcon(this.icon);
} else { } else {
this._updateFontIcon(this.icon); this._updateFontIcon(this.icon);
} }
@ -278,4 +299,49 @@ export class TbIconComponent extends _TbIconBase
} }
} }
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;
}
}
} }

7
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -71,6 +71,10 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
@coerceBoolean() @coerceBoolean()
iconClearButton = false; iconClearButton = false;
@Input()
@coerceBoolean()
allowedCustomIcon = false;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;
@ -169,7 +173,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
this.viewContainerRef, MaterialIconsComponent, 'left', true, null, this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
{ {
selectedIcon: this.materialIconFormGroup.get('icon').value, selectedIcon: this.materialIconFormGroup.get('icon').value,
iconClearButton: this.iconClearButton iconClearButton: this.iconClearButton,
allowedCustomIcon: this.allowedCustomIcon,
}, },
{}, {},
{}, {}, true); {}, {}, true);

137
ui-ngx/src/app/shared/components/material-icons.component.html

@ -16,63 +16,86 @@
--> -->
<div class="tb-material-icons-panel"> <div class="tb-material-icons-panel">
<div *ngIf="showTitle" class="tb-material-icons-title" translate>icon.icons</div> <div class="flex w-full">
<mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic"> <span *ngIf="showTitle" class="tb-material-icons-title flex-1" translate>icon.icons</span>
<mat-icon matPrefix>search</mat-icon> @if (allowedCustomIcon) {
<input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/> <tb-toggle-select [(ngModel)]="isCustomIcon" [ngModelOptions]="{standalone: true}" (ngModelChange)="selectedIcon = null">
<button *ngIf="searchIconControl.value" <tb-toggle-option [value]="false">{{ 'resource.system' | translate }}</tb-toggle-option>
type="button" <tb-toggle-option [value]="true">{{ 'icon.custom' | translate }}</tb-toggle-option>
matSuffix mat-icon-button aria-label="Clear" </tb-toggle-select>
(click)="clearSearch()"> <span class="flex-1"></span>
<mat-icon class="material-icons">close</mat-icon> }
</button> </div>
</mat-form-field> @if (!isCustomIcon) {
<cdk-virtual-scroll-viewport [class.!hidden]="notFound" #iconsPanel <mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic">
[itemSize]="iconsRowHeight" class="tb-material-icons-viewport" <mat-icon matPrefix>search</mat-icon>
[style.width]="iconsPanelWidth" <input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/>
[style.height]="iconsPanelHeight"> <button *ngIf="searchIconControl.value"
<div *cdkVirtualFor="let iconRow of iconRows$ | async" class="tb-material-icons-row"> type="button"
<ng-container *ngFor="let icon of iconRow"> matSuffix mat-icon-button aria-label="Clear"
<button *ngIf="icon.name === selectedIcon" (click)="clearSearch()">
class="tb-select-icon-button" <mat-icon class="material-icons">close</mat-icon>
mat-raised-button </button>
color="primary" </mat-form-field>
(click)="selectIcon(icon)" <cdk-virtual-scroll-viewport [class.!hidden]="notFound" #iconsPanel
matTooltip="{{ icon.displayName }}" [itemSize]="iconsRowHeight" class="tb-material-icons-viewport"
matTooltipPosition="above" [style.width]="iconsPanelWidth"
type="button"> [style.height]="iconsPanelHeight">
<tb-icon matButtonIcon>{{icon.name}}</tb-icon> <div *cdkVirtualFor="let iconRow of iconRows$ | async" class="tb-material-icons-row">
</button> <ng-container *ngFor="let icon of iconRow">
<button *ngIf="icon.name !== selectedIcon" <button *ngIf="icon.name === selectedIcon"
class="tb-select-icon-button" class="tb-select-icon-button"
mat-button mat-raised-button
(click)="selectIcon(icon)" color="primary"
matTooltip="{{ icon.displayName }}" (click)="selectIcon(icon.name)"
matTooltipPosition="above" matTooltip="{{ icon.displayName }}"
type="button"> matTooltipPosition="above"
<tb-icon matButtonIcon>{{icon.name}}</tb-icon> type="button">
</button> <tb-icon matButtonIcon>{{icon.name}}</tb-icon>
</ng-container> </button>
<button *ngIf="icon.name !== selectedIcon"
class="tb-select-icon-button"
mat-button
(click)="selectIcon(icon.name)"
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
</button>
</ng-container>
</div>
</cdk-virtual-scroll-viewport>
<ng-container *ngIf="notFound">
<div class="tb-no-data-available" [style.width]="iconsPanelWidth">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
</div>
</ng-container>
<div class="tb-material-icons-panel-buttons" *ngIf="iconClearButton || !showAllSubject.value">
<button *ngIf="iconClearButton"
mat-button
color="primary"
type="button"
(click)="clearIcon()"
[disabled]="!selectedIcon">
{{ 'action.clear' | translate }}
</button>
<span class="flex-1"></span>
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
{{ 'action.show-more' | translate }}
</button>
</div> </div>
</cdk-virtual-scroll-viewport> } @else {
<ng-container *ngIf="notFound"> <tb-gallery-image-input [style.width]="iconsPanelWidth"
<div class="tb-no-data-available" [style.width]="iconsPanelWidth"> [(ngModel)]="selectedIcon">
<div class="tb-no-data-bg"></div> </tb-gallery-image-input>
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div> <div class="tb-material-icons-panel-buttons">
<span class="flex-1"></span>
<button class="tb-material-icons-show-more" mat-button color="primary"
[disabled]="!selectedIcon"
(click)="selectIcon(selectedIcon)">
{{ 'action.set' | translate }}
</button>
</div> </div>
</ng-container> }
<div class="tb-material-icons-panel-buttons" *ngIf="iconClearButton || !showAllSubject.value">
<button *ngIf="iconClearButton"
mat-button
color="primary"
type="button"
(click)="clearIcon()"
[disabled]="!selectedIcon">
{{ 'action.clear' | translate }}
</button>
<span class="flex-1"></span>
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
{{ 'action.show-more' | translate }}
</button>
</div>
</div> </div>

12
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 { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { isTbImage } from '@shared/models/resource.models';
@Component({ @Component({
selector: 'tb-material-icons', selector: 'tb-material-icons',
@ -61,6 +62,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
@coerceBoolean() @coerceBoolean()
showTitle = true; showTitle = true;
@Input()
@coerceBoolean()
allowedCustomIcon = false;
@Input() @Input()
popover: TbPopoverComponent; popover: TbPopoverComponent;
@ -71,6 +76,8 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
showAllSubject = new BehaviorSubject<boolean>(false); showAllSubject = new BehaviorSubject<boolean>(false);
searchIconControl: UntypedFormControl; searchIconControl: UntypedFormControl;
isCustomIcon = false;
iconsRowHeight = 48; iconsRowHeight = 48;
iconsPanelHeight: string; iconsPanelHeight: string;
@ -122,14 +129,15 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
map((data) => data.iconRows), map((data) => data.iconRows),
share() share()
); );
this.isCustomIcon = isTbImage(this.selectedIcon)
} }
clearSearch() { clearSearch() {
this.searchIconControl.patchValue('', {emitEvent: true}); this.searchIconControl.patchValue('', {emitEvent: true});
} }
selectIcon(icon: MaterialIcon) { selectIcon(icon: string) {
this.iconSelected.emit(icon.name); this.iconSelected.emit(icon);
} }
clearIcon() { clearIcon() {

3
ui-ngx/src/app/shared/models/constants.ts

@ -177,6 +177,7 @@ export const HelpLinks = {
entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/bulk-provisioning`, entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/bulk-provisioning`,
rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/rule-chains`, rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/rule-chains`,
lwm2mResourceLibrary: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/lwm2m-api`, 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`, dashboards: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/dashboards`,
otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates`, otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates`,
widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widget-types`, widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widget-types`,
@ -206,7 +207,7 @@ export const HelpLinks = {
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`, mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`,
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`, calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`,
aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/ai-models`, aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`,
timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`,
trendzSettings: `${helpBaseUrl}/docs/trendz/` trendzSettings: `${helpBaseUrl}/docs/trendz/`
} }

1
ui-ngx/src/app/shared/models/resource.models.ts

@ -188,6 +188,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 isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url);
export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX); 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} => { export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => {
const res = url.match(IMAGES_URL_REGEXP); const res = url.match(IMAGES_URL_REGEXP);

10732
ui-ngx/src/assets/dashboard/api_usage.json

File diff suppressed because it is too large

65
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -865,15 +865,18 @@
"api-features": "API features", "api-features": "API features",
"api-usage": "API usage", "api-usage": "API usage",
"alarm": "Alarm", "alarm": "Alarm",
"alarms-created": "Alarms created", "alarms-created": "Created alarms",
"queue-stats": "Queue Stats", "queue-stats": "Queue Stats",
"processing-failures-and-timeouts": "Processing Failures and Timeouts", "processing-failures-and-timeouts": "Processing Failures and Timeouts",
"exceptions": "Exceptions", "exceptions": "Exceptions",
"alarms-created-daily-activity": "Alarms created daily activity", "alarms-created-daily-activity": "Created alarms daily activity",
"alarms-created-hourly-activity": "Alarms created hourly activity", "alarms-created-hourly-activity": "Created alarms hourly activity",
"alarms-created-monthly-activity": "Alarms created monthly activity", "alarms-created-monthly-activity": "Created alarms monthly activity",
"data-points": "Data points", "data-points": "Data points",
"data-points-storage-days": "Data points storage days", "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", "device-api": "Device API",
"email": "Email", "email": "Email",
"email-messages": "Email messages", "email-messages": "Email messages",
@ -899,14 +902,15 @@
"processing-timeouts": "${entityName} Processing Timeouts", "processing-timeouts": "${entityName} Processing Timeouts",
"rule-chain": "Rule Chain", "rule-chain": "Rule Chain",
"rule-engine": "Rule Engine", "rule-engine": "Rule Engine",
"rule-engine-daily-activity": "Rule Engine daily activity",
"rule-engine-executions": "Rule Engine executions", "rule-engine-executions": "Rule Engine executions",
"rule-engine-hourly-activity": "Rule Engine hourly activity", "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-monthly-activity": "Rule Engine monthly activity",
"rule-engine-statistics": "Rule Engine Statistics", "rule-engine-statistics": "Rule Engine Statistics",
"rule-node": "Rule Node", "rule-node": "Rule Node",
"sms": "SMS", "sms": "SMS",
"sms-messages": "SMS messages", "sms-messages": "SMS messages",
"sms-messages-hourly-activity": "SMS messages hourly activity",
"sms-messages-daily-activity": "SMS messages daily activity", "sms-messages-daily-activity": "SMS messages daily activity",
"sms-messages-monthly-activity": "SMS messages monthly activity", "sms-messages-monthly-activity": "SMS messages monthly activity",
"successful": "${entityName} Successful", "successful": "${entityName} Successful",
@ -916,13 +920,40 @@
"telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity",
"telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity",
"transport": "Transport", "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-daily-activity": "Transport daily activity",
"transport-data-points": "Transport data points", "transport-data-points": "Transport data points",
"transport-hourly-activity": "Transport hourly activity", "transport-data-points-hourly-activity": "Transport data points hourly activity",
"transport-messages": "Transport messages", "transport-data-points-daily-activity": "Transport data points daily activity",
"transport-monthly-activity": "Transport monthly activity", "transport-data-points-monthly-activity": "Transport data points monthly activity",
"view-details": "View details", "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": { "api-limit": {
"cassandra-write-queries-core": "Rest API Cassandra write queries", "cassandra-write-queries-core": "Rest API Cassandra write queries",
@ -9501,6 +9532,21 @@
"how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard" "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"
} }
}, },
"color": { "color": {
@ -9509,6 +9555,7 @@
"icon": { "icon": {
"icon": "Icon", "icon": "Icon",
"icons": "Icons", "icons": "Icons",
"custom": "Custom",
"select-icon": "Select icon", "select-icon": "Select icon",
"material-icons": "Material icons", "material-icons": "Material icons",
"show-all": "Show all icons", "show-all": "Show all icons",

4
ui-ngx/src/scss/mixins.scss

@ -26,6 +26,10 @@
width: #{$size}px; width: #{$size}px;
height: #{$size}px; height: #{$size}px;
} }
img {
width: #{$size}px;
height: #{$size}px;
}
} }
@mixin tb-mat-icon-button-size($size) { @mixin tb-mat-icon-button-size($size) {

2
ui-ngx/src/styles.scss

@ -896,7 +896,7 @@ pre.tb-highlight {
} }
.mat-icon { .mat-icon {
svg { svg, img {
vertical-align: inherit; vertical-align: inherit;
} }
&.tb-mat-12 { &.tb-mat-12 {

Loading…
Cancel
Save