diff --git a/.github/release.yml b/.github/release.yml index 53b2e3ae65..70852dcbb2 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,27 +23,19 @@ changelog: labels: - 'Major Core' - 'Major Rule Engine' - exclude: - labels: - - 'Bug' + - title: 'Major UI' labels: - 'Major UI' - exclude: - labels: - - 'Bug' + - title: 'Major Transport' labels: - 'Major Transport' - exclude: - labels: - - 'Bug' + - title: 'Major Edge' labels: - 'Major Edge' - exclude: - labels: - - 'Bug' + - title: 'Core & Rule Engine' labels: - 'Core' @@ -51,38 +43,63 @@ changelog: exclude: labels: - 'Bug' + - title: 'UI' labels: - 'UI' exclude: labels: - 'Bug' + - title: 'Transport' labels: - 'Transport' exclude: labels: - 'Bug' + - title: 'Edge' labels: - 'Edge' exclude: labels: - 'Bug' + - title: 'Bug: Core & Rule Engine' labels: - - 'Core' - - 'Rule Engine' - 'Bug' + exclude: + labels: + - 'UI' + - 'Transport' + - 'Edge' + - title: 'Bug: UI' labels: - - 'UI' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'Transport' + - 'Edge' + - title: 'Bug: Transport' labels: - - 'Transport' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'UI' + - 'Edge' + - title: 'Bug: Edge' labels: - - 'Edge' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'UI' + - 'Transport' diff --git a/.github/workflows/check-configuration-files.yml b/.github/workflows/check-configuration-files.yml index 561b7d0019..0ea9d61923 100644 --- a/.github/workflows/check-configuration-files.yml +++ b/.github/workflows/check-configuration-files.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/workflows/license-header-format.yml b/.github/workflows/license-header-format.yml index 20a1c1350a..0ac41c7bc8 100644 --- a/.github/workflows/license-header-format.yml +++ b/.github/workflows/license-header-format.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 6267cc3a7a..169a276db4 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,13 @@
-💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • ▶️ [Live demo](https://demo.thingsboard.io/signup) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all) +💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all)
## 🚀 Installation options -* Install ThingsBoard [On-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) +Install ThingsBoard [on-premises](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) or use [ThingsBoard Cloud](https://thingsboard.io/installations/). ## 💡 Getting started with ThingsBoard @@ -123,9 +121,9 @@ ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that [![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/) +[**Site fleet tracking**](https://thingsboard.io/use-cases/site-fleet-tracking/) -[![Fleet tracking](https://github.com/user-attachments/assets/9e8938ba-ee0c-4599-9494-d74b7de8a63d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) +[![Site fleet tracking](https://github.com/user-attachments/assets/d6ce0766-b138-4a42-86aa-7112a543026c "Site fleet tracking")](https://thingsboard.io/use-cases/site-fleet-tracking/) [**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) diff --git a/TEST_FAST.md b/TEST_FAST.md new file mode 100644 index 0000000000..c0f1174584 --- /dev/null +++ b/TEST_FAST.md @@ -0,0 +1,31 @@ + +## Running tests in parallel with a reasonable memory usage + +```bash +export MAVEN_OPTS="-Xmx1024m" +export NODE_OPTIONS="--max_old_space_size=4096" +export SUREFIRE_JAVA_OPTS="-Xmx1200m -Xss256k -XX:+ExitOnOutOfMemoryError" + +mvn clean install -T6 -DskipTests +mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4 +mvn test -pl dao -Dparallel=packages -DforkCount=4 + +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 + +#the rest of application tests +mvn test -pl application -Dtest=' +!**/nosql/*Test.java, +!org.thingsboard.server.controller.**, +!org.thingsboard.server.edge.**, +!org.thingsboard.server.service.**, +!org.thingsboard.server.transport.mqtt.**, +!org.thingsboard.server.transport.coap.**, +!org.thingsboard.server.transport.lwm2m.** +' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 +``` diff --git a/application/pom.xml b/application/pom.xml index 4cbd9c3b64..e66a5abaf3 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,6 +1,6 @@ + + + + + + diff --git a/build.sh b/build.sh index 22258fbd2e..d2a965f281 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/build_proto.sh b/build_proto.sh index f0690e75bb..9228dcb3a2 100755 --- a/build_proto.sh +++ b/build_proto.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 92b174ea61..6326c45685 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -1,6 +1,6 @@ + + diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 1f4b5ff41b..5983b96b95 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -1,6 +1,6 @@ + + + diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index fecffeacce..304ccf5ede 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-check-log-folders.sh b/docker/docker-check-log-folders.sh index 6122f3d2c1..32b07011fc 100755 --- a/docker/docker-check-log-folders.sh +++ b/docker/docker-check-log-folders.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.cassandra.volumes.yml b/docker/docker-compose.cassandra.volumes.yml index 8e92069a06..cca3c630cd 100644 --- a/docker/docker-compose.cassandra.volumes.yml +++ b/docker/docker-compose.cassandra.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.confluent.yml b/docker/docker-compose.confluent.yml index 46cb0235ee..2513d19a55 100644 --- a/docker/docker-compose.confluent.yml +++ b/docker/docker-compose.confluent.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.edqs.volumes.yml b/docker/docker-compose.edqs.volumes.yml index cebc7dd290..c1ead97911 100644 --- a/docker/docker-compose.edqs.volumes.yml +++ b/docker/docker-compose.edqs.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 3ebb526a2d..b076282346 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.hybrid.yml b/docker/docker-compose.hybrid.yml index 6b4129ec56..5e033bcdb3 100644 --- a/docker/docker-compose.hybrid.yml +++ b/docker/docker-compose.hybrid.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.kafka.yml b/docker/docker-compose.kafka.yml index 06bafafc61..b33a295d4a 100644 --- a/docker/docker-compose.kafka.yml +++ b/docker/docker-compose.kafka.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.postgres.volumes.yml b/docker/docker-compose.postgres.volumes.yml index 696189a639..225da31eaa 100644 --- a/docker/docker-compose.postgres.volumes.yml +++ b/docker/docker-compose.postgres.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.postgres.yml b/docker/docker-compose.postgres.yml index bf43e1823d..7ea9354f57 100644 --- a/docker/docker-compose.postgres.yml +++ b/docker/docker-compose.postgres.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.prometheus-grafana.yml b/docker/docker-compose.prometheus-grafana.yml index 38699f1040..659e1bc0b6 100644 --- a/docker/docker-compose.prometheus-grafana.yml +++ b/docker/docker-compose.prometheus-grafana.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.valkey-cluster.volumes.yml b/docker/docker-compose.valkey-cluster.volumes.yml index e8da9844d9..332732203c 100644 --- a/docker/docker-compose.valkey-cluster.volumes.yml +++ b/docker/docker-compose.valkey-cluster.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.valkey-cluster.yml b/docker/docker-compose.valkey-cluster.yml index b8ce0f96ec..7c2df9b723 100644 --- a/docker/docker-compose.valkey-cluster.yml +++ b/docker/docker-compose.valkey-cluster.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.valkey-sentinel.volumes.yml b/docker/docker-compose.valkey-sentinel.volumes.yml index 3f4a09a912..5fb2df97bc 100644 --- a/docker/docker-compose.valkey-sentinel.volumes.yml +++ b/docker/docker-compose.valkey-sentinel.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.valkey-sentinel.yml b/docker/docker-compose.valkey-sentinel.yml index e528a1bb9a..5bdb6573f7 100644 --- a/docker/docker-compose.valkey-sentinel.yml +++ b/docker/docker-compose.valkey-sentinel.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.valkey.volumes.yml b/docker/docker-compose.valkey.volumes.yml index 02a7c397a3..e296345832 100644 --- a/docker/docker-compose.valkey.volumes.yml +++ b/docker/docker-compose.valkey.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.valkey.yml b/docker/docker-compose.valkey.yml index 3eba086a9b..897cbb70cd 100644 --- a/docker/docker-compose.valkey.yml +++ b/docker/docker-compose.valkey.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.volumes.yml b/docker/docker-compose.volumes.yml index a01247c25d..d31b845c4a 100644 --- a/docker/docker-compose.volumes.yml +++ b/docker/docker-compose.volumes.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 224f94c103..e6be50235e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-create-log-folders.sh b/docker/docker-create-log-folders.sh index ed66d4e156..b05abc6231 100755 --- a/docker/docker-create-log-folders.sh +++ b/docker/docker-create-log-folders.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-install-tb.sh b/docker/docker-install-tb.sh index fa50e58c8e..aae75fc05f 100755 --- a/docker/docker-install-tb.sh +++ b/docker/docker-install-tb.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-remove-services.sh b/docker/docker-remove-services.sh index 3124119abc..cc036a400d 100755 --- a/docker/docker-remove-services.sh +++ b/docker/docker-remove-services.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-start-services.sh b/docker/docker-start-services.sh index 0d256abcf6..ae447ccff1 100755 --- a/docker/docker-start-services.sh +++ b/docker/docker-start-services.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-stop-services.sh b/docker/docker-stop-services.sh index 54386d10dd..5812d24d9e 100755 --- a/docker/docker-stop-services.sh +++ b/docker/docker-stop-services.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-update-service.sh b/docker/docker-update-service.sh index b77b7bbf6c..6f513db056 100755 --- a/docker/docker-update-service.sh +++ b/docker/docker-update-service.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker-upgrade-tb.sh b/docker/docker-upgrade-tb.sh index 4bf2c0807e..3bc4df7871 100755 --- a/docker/docker-upgrade-tb.sh +++ b/docker/docker-upgrade-tb.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/monitoring/grafana/provisioning/dashboards/dashboard.yml b/docker/monitoring/grafana/provisioning/dashboards/dashboard.yml index 78fdffac9f..f5ce688ff2 100644 --- a/docker/monitoring/grafana/provisioning/dashboards/dashboard.yml +++ b/docker/monitoring/grafana/provisioning/dashboards/dashboard.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/monitoring/grafana/provisioning/datasources/datasource.yml b/docker/monitoring/grafana/provisioning/datasources/datasource.yml index 535134b723..758dd77484 100644 --- a/docker/monitoring/grafana/provisioning/datasources/datasource.yml +++ b/docker/monitoring/grafana/provisioning/datasources/datasource.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/monitoring/prometheus/prometheus.yml b/docker/monitoring/prometheus/prometheus.yml index 04b4b287de..e0ea16b9a4 100644 --- a/docker/monitoring/prometheus/prometheus.yml +++ b/docker/monitoring/prometheus/prometheus.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 The Thingsboard Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/tb-edqs/conf/logback.xml b/docker/tb-edqs/conf/logback.xml index c8e97e48e4..859b0c6763 100644 --- a/docker/tb-edqs/conf/logback.xml +++ b/docker/tb-edqs/conf/logback.xml @@ -1,7 +1,7 @@ - + @@ -34,5 +34,4 @@ - diff --git a/monitoring/src/main/resources/lwm2m/models/0.xml b/monitoring/src/main/resources/lwm2m/models/0.xml index cfe4e551c0..1614459802 100644 --- a/monitoring/src/main/resources/lwm2m/models/0.xml +++ b/monitoring/src/main/resources/lwm2m/models/0.xml @@ -1,7 +1,7 @@ - 1.68.1 + 1.76.0 1.2.8 1.18.38 1.2.5 1.2.5 1.7.1 - 3.2.5 + 3.5.4 3.4.0 2.8.8TB 2.2.30 @@ -86,6 +86,7 @@ org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090. Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network --> 3.9.1 + 1.10.1 8.10.1 3.5.3 2.2 @@ -101,7 +102,7 @@ 1.6.4 1.6.1 - 1.9.4 + 1.11.0 4.4 1.0.4TB 3.7.1 @@ -110,7 +111,7 @@ 4.0.2 1.7.5 3.8.0 - 1.1.0 + 1.8.0-TB 2.38.0 1.24 1.11.0 @@ -126,7 +127,7 @@ 5.0.0 7.10.1 - 1.20.6 + 1.21.4 1.0.2 1.12 6.1.0 @@ -146,7 +147,7 @@ 9.2.0 1.1.10.5 9.10.0 - 4.1.125.Final + 4.1.128.Final @@ -661,8 +662,10 @@ ${surefire.version} - -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=20 - --add-opens java.base/java.lang.reflect=ALL-UNNAMED + -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=200 + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + -Dqueue.edqs.local.rocksdb_path="target/rocksdb/fork_${surefire.forkNumber}/edqs" + -Dqueue.calculated_fields.rocks_db_path="target/rocksdb/fork_${surefire.forkNumber}/cf" @@ -911,7 +914,7 @@ import - dev.langchain4j + org.thingsboard.langchain4j langchain4j-bom ${langchain4j.version} pom @@ -1153,6 +1156,17 @@ org.apache.kafka kafka-clients ${kafka.version} + + + org.lz4 + lz4-java + + + + + at.yawk.lz4 + lz4-java + ${lz4.version} com.github.springtestdbunit @@ -1433,6 +1447,12 @@ org.apache.cassandra cassandra-all ${cassandra-all.version} + + + org.lz4 + lz4-java + + org.testng @@ -1908,6 +1928,10 @@ central https://repo1.maven.org/maven2/ + + thingsboard-repo + https://repo.thingsboard.io/artifactory/libs-release-public + spring-snapshots Spring Snapshots diff --git a/rest-client/pom.xml b/rest-client/pom.xml index c3cf2460dd..9996b19d00 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -1,6 +1,6 @@ +
+ + + +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('pattern')) { + {{ 'common.hint.title-pattern' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + +
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'alarm-rule.create-conditions' | translate }}
+
+ + +
+
+
+
{{ 'alarm-rule.clear-condition' | translate }}
+
+
+ + +
+ @if (isEditValue) { + + } +
+
+ alarm-rule.no-clear-alarm-rule +
+
+ +
+
+
+ + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + + } +
+
+ + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
+
+ + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts new file mode 100644 index 0000000000..13c388675c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, DestroyRef, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldArgument, + CalculatedFieldConfiguration, + CalculatedFieldInfo, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { RelationTypes } from '@shared/models/relation.models'; +import { + AlarmRule, + AlarmRuleConditionType, + alarmRuleEntityTypeList, + AlarmRuleExpressionType +} from '@shared/models/alarm-rule.models'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-alarm-rules', + templateUrl: './alarm-rules.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + standalone: false +}) +export class AlarmRulesComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly alarmRuleEntityTypeList = alarmRuleEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + return inject(CalculatedFieldFormService).buildAlarmRuleForm(); + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + this.entityForm.patchValue({ configuration, debugSettings, entityId, ...value }, {emitEvent: false}); + if (!entityId) { + this.entityForm.get('configuration').disable({emitEvent: false}); + } + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityFormValue(), + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + get arguments(): Record { + return this.entityForm.get('configuration.arguments').value; + } + + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + + get configFormGroup(): FormGroup { + return this.entityForm.get('configuration') as FormGroup; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.entityForm.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + }, + alarmDetails: null + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index d67cd51a9a..e1f1eed67c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -1,6 +1,6 @@ -
+
{{ 'alarm-rule.condition' | translate }}
-
{{ 'alarm-rule.schedule-title' | translate }}
-
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss index ac77089857..96494ab98d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ display: flex; flex: 1; &-button { - --mat-outlined-button-horizontal-padding: 3px 0px 12px; + --mat-button-outlined-horizontal-padding: 3px 0px 12px; display: block; width: 100%; } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 6f17aa8b59..fe0670b4e7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import { getAlarmScheduleRangeText, utcTimestampToTimeOfDay } from '@shared/models/device.models'; -import { TimeUnit } from '@shared/models/time/time.models'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { CfAlarmRuleConditionDialogComponent, CfAlarmRuleConditionDialogData @@ -42,7 +42,8 @@ import { AlarmRuleConditionType, AlarmRuleExpressionType, AlarmRuleSchedule, - AlarmRuleScheduleType + AlarmRuleScheduleType, + checkPredicates } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { @@ -53,21 +54,22 @@ import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @Component({ - selector: 'tb-cf-alarm-rule-condition', - templateUrl: './cf-alarm-rule-condition.component.html', - styleUrls: ['./cf-alarm-rule-condition.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CfAlarmRuleConditionComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CfAlarmRuleConditionComponent), - multi: true, - } - ] + selector: 'tb-cf-alarm-rule-condition', + templateUrl: './cf-alarm-rule-condition.component.html', + styleUrls: ['./cf-alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true, + } + ], + standalone: false }) export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges { @@ -136,50 +138,43 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali writeValue(value: AlarmRuleCondition): void { this.modelValue = value; this.updateConditionInfo(); - if (value) { - this.onValidatorChange(); - } + this.recalculateArgumentValidity(); } ngOnChanges(changes: SimpleChanges) { if (changes.arguments) { - if (changes.arguments && !changes.arguments.firstChange && this.modelValue) { - this.onValidatorChange(); + if (changes.arguments && !changes.arguments.firstChange) { + this.recalculateArgumentValidity(); } } } + private recalculateArgumentValidity(): void { + if (!this.modelValue || !this.arguments) { + this.filtersArgumentsValid = true; + this.schedulerArgumentsValid = true; + return; + } + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); + this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); + } + private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean { const arg = obj?.schedule?.dynamicValueArgument; return !arg || validArguments.includes(arg); } - private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean { - const validSet = new Set(validArguments); + private areFilterAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); const filters = obj?.expression?.filters || obj?.filters || []; for (const filter of filters) { if (filter.argument && !validSet.has(filter.argument)) { return false; } } - function checkPredicates(predicates: any[]): boolean { - for (const p of predicates) { - if (p.value?.dynamicValueArgument) { - if (!validSet.has(p.value.dynamicValueArgument)) { - return false; - } - } - if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) { - if (!checkPredicates(p.predicates)) { - return false; - } - } - } - return true; - } for (const filter of filters) { if (Array.isArray(filter.predicates)) { - if (!checkPredicates(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { return false; } } @@ -187,21 +182,14 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali return true; } - public conditionSet() { - return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); + public conditionSet(): boolean { + return !!this.modelValue && !!(this.modelValue.expression?.expression || this.modelValue.expression?.filters); } public validate(control: AbstractControl): ValidationErrors | null { - this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments)); - this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); - this.onValidatorChange = () => { - control.updateValueAndValidity({ emitEvent: true }); - }; - return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : { - alarmRuleCondition: { - valid: false, - } - }; + const hasCondition = this.conditionSet(); + const argsValid = this.filtersArgumentsValid && this.schedulerArgumentsValid; + return (hasCondition && argsValid) ? null : { alarmRuleCondition: { valid: false } }; } public openFilterDialog($event: Event) { @@ -222,7 +210,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (result) { this.modelValue = {...this.modelValue, ...result}; this.updateModel(); - this.cd.detectChanges(); } }); } @@ -265,7 +252,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (this.modelValue.value.dynamicValueArgument) { this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { attribute: `${this.modelValue.value.dynamicValueArgument}` - }); + }) + ' ' + this.translate.instant(timeUnitTranslationMap.get(this.modelValue.unit)).toLowerCase(); } else { this.specText = this.translate.instant('alarm-rule.condition-during', { during: duringText @@ -288,10 +275,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali private updateModel() { this.updateConditionInfo(); + this.recalculateArgumentValidity(); this.propagateChange(this.modelValue); - if (this.modelValue) { - this.onValidatorChange(); - } + this.onValidatorChange(); + this.cd.detectChanges(); } public openScheduleDialog($event: Event) { @@ -309,15 +296,17 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali } }).afterClosed().subscribe((result) => { if (result) { + if (!this.modelValue) { + this.modelValue = {} as AlarmRuleCondition; + } this.modelValue.schedule = result; this.updateModel(); - this.cd.detectChanges(); } }); } private updateScheduleText() { - let schedule = this.modelValue?.schedule; + const schedule = this.modelValue?.schedule; this.scheduleText = ''; if (isDefinedAndNotNull(schedule)) { if (schedule.dynamicValueArgument) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index a9f4ad61a5..47b6281fdd 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -1,6 +1,6 @@ -
+
- @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { -
-
- alarm-rule.alarm-rule-additional-info -
- - - - +
+
+ alarm-rule.alarm-rule-additional-info
- } - @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { -
-
- alarm-rule.alarm-rule-mobile-dashboard -
- - + + + + +
+
+
+ alarm-rule.alarm-rule-mobile-dashboard
- } + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss index cea9c68955..53283aad68 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index cd6e06f293..706477f72a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -38,21 +38,22 @@ import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @Component({ - selector: 'tb-cf-alarm-rule', - templateUrl: './cf-alarm-rule.component.html', - styleUrls: ['./cf-alarm-rule.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CfAlarmRuleComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CfAlarmRuleComponent), - multi: true, - } - ] + selector: 'tb-cf-alarm-rule', + templateUrl: './cf-alarm-rule.component.html', + styleUrls: ['./cf-alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true, + } + ], + standalone: false }) export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index 6253aa9a4b..f38df65c29 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html index 2d12355f48..3908d919b3 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -1,6 +1,6 @@ - -
+
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
} -
- + @if (!createAlarmRulesFormArray().controls.length) { + alarm-rule.add-create-alarm-rule-prompt - -
-
+ + } +
+
- } -
-
-
-
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
- -
+ @if (index) { + + }
- @if (index) { - - } -
- } - @if (!filtersFormArray.length) { - - alarm-rule.no-filter - - - } -
- + } +
+ +} @else { + alarm-rule.no-filter +} + +
+ @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { +
+ @if (index) { +
+ {{ complexOperationTranslations.get(operation) | translate }} +
+ } +
+
+ + + +
-
- } - @if (!predicatesFormArray.length) { - - alarm-rule.no-filter - - - } + } +
-
-
+ } @else { + alarm-rule.no-filter + } +
-
-
-
+ +
+
audit-log.filter-types
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss index 77bd5aea60..501a5e2419 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,3 +33,12 @@ } } } + +::ng-deep { + .tb-audit-log-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts index e78b38965d..631a795f7d 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,32 +16,44 @@ import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { TemplatePortal } from '@angular/cdk/portal'; -import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { isDefined, isDefinedAndNotNull } from '@app/core/utils'; import { StringItemsOption } from '@app/shared/components/string-items-list.component'; import { ActionType, actionTypeTranslations, - auditLogFilterEquals, AuditLogFilter, + auditLogFilterEquals, POSITION_MAP, } from '@app/shared/public-api'; import { TranslateService } from '@ngx-translate/core'; // @dynamic @Component({ - selector: 'tb-audit-log-filter', - templateUrl: './audit-log-filter.component.html', - styleUrls: ['./audit-log-filter.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AuditLogFilterComponent), - multi: true - } - ] + selector: 'tb-audit-log-filter', + templateUrl: './audit-log-filter.component.html', + styleUrls: ['./audit-log-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AuditLogFilterComponent), + multi: true + } + ], + standalone: false }) export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { @@ -56,9 +68,9 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { actionTypeTranslations = actionTypeTranslations; - buttonDisplayValue = this.translate.instant('audit-log.filter'); + buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title'); - auditLogFilterForm: UntypedFormGroup; + auditLogFilterForm: FormGroup; auditLogOverlayRef: OverlayRef; @@ -68,7 +80,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { private propagateChange = (_: any) => {}; - constructor(private fb: UntypedFormBuilder, + constructor(private fb: FormBuilder, private translate: TranslateService, private overlay: Overlay, private nativeElement: ElementRef, @@ -93,7 +105,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { @@ -152,11 +164,13 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { cancel() { this.updateAuditLogFilterForm(this.auditLogFilter); + this.auditLogFilterForm.markAsPristine(); this.auditLogOverlayRef.dispose(); } update() { this.auditLogFilterUpdated(this.auditLogFilterForm.value); + this.auditLogFilterForm.markAsPristine(); this.auditLogOverlayRef.dispose(); } diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html index 3d982197c8..c82257803d 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html @@ -1,6 +1,6 @@ +
+ + + +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('pattern')) { + {{ 'common.hint.title-pattern' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + + + {{ 'common.type' | translate }} + + + {{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value) + ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) + : entityForm.get('type').value }} + + @for (type of fieldTypes; track type) { + + + {{ CalculatedFieldTypeTranslations.get(type).name | translate }} + +
+ + {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} + +
+ } +
+
+
+ @switch (entityForm.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss new file mode 100644 index 0000000000..695df5063a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +::ng-deep { + .tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; + } + + .tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts new file mode 100644 index 0000000000..4790789080 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -0,0 +1,169 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, DestroyRef, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldConfiguration, + CalculatedFieldInfo, + calculatedFieldsEntityTypeList, + CalculatedFieldType, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-calculated-field', + templateUrl: './calculated-field.component.html', + styleUrls: ['./calculated-field.component.scss'], + standalone: false +}) +export class CalculatedFieldComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + disabledConfiguration = false; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + readonly fieldTypes = calculatedFieldTypes; + readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + const form = inject(CalculatedFieldFormService).buildForm(); + inject(CalculatedFieldFormService).setupTypeChange(form, inject(DestroyRef), () => this.isEditValue); + return form; + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + const preparedConfig = this.cfFormService.prepareConfig(configuration); + this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true}); + setTimeout(() => { + this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); + this.entityForm.get('type').updateValueAndValidity(); + }); + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityValue, + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts index e9bf146bfd..c58b595ba1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -44,12 +44,22 @@ import { import { EntityAggregationComponentModule } from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module'; +import { + CalculatedFieldsHeaderComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { + CalculatedFieldsFilterConfigComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; @NgModule({ declarations: [ CalculatedFieldDialogComponent, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, + CalculatedFieldsHeaderComponent, + CalculatedFieldsFilterConfigComponent, + CalculatedFieldComponent, ], imports: [ CommonModule, @@ -64,6 +74,7 @@ import { exports: [ CalculatedFieldDialogComponent, CalculatedFieldScriptTestDialogComponent, + CalculatedFieldComponent, ] }) export class CalculatedFieldsModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 98e4a29549..efb767f92e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ import { DateEntityTableColumn, + EntityLinkTableColumn, EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { PageLink } from '@shared/models/page/page-link'; import { EMPTY, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; @@ -34,70 +35,100 @@ import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; -import { catchError, filter, switchMap, tap } from 'rxjs/operators'; +import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, ArgumentType, CalculatedField, CalculatedFieldEventArguments, + CalculatedFieldInfo, CalculatedFieldScriptConfiguration, + CalculatedFieldsQuery, CalculatedFieldType, CalculatedFieldTypeTranslations, + debugCfActionEnabled, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, PropagationWithExpression, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, - CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { isObject } from '@core/utils'; +import { deepClone, getEntityDetailsPageURL, isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; +import { + CalculatedFieldsHeaderComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; +import { Router } from '@angular/router'; +import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component'; + +export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { - title: this.translate.instant('calculated-fields.see-debug-events'), - action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + title: this.translate.instant('action.see-debug-events'), + action: (calculatedField: CalculatedFieldsTableEntity, + openCalculatedFieldEdit = true, + afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback), }; + calculatedFieldFilterConfig: CalculatedFieldsQuery; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, private datePipe: DatePipe, - public entityId: EntityId = null, + private entityId: EntityId = null, private store: Store, private destroyRef: DestroyRef, private renderer: Renderer2, - public entityName: string, + private entityName: string, private ownerId: EntityId = null, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, private utilsService: UtilsService, + private router: Router, + public pageMode = false, ) { super(); + if (this.pageMode) { + this.headerComponent = CalculatedFieldsHeaderComponent; + + this.entityComponent = CalculatedFieldComponent; + this.entityTabsComponent = CalculatedFieldsTabsComponent; + this.rowPointer = true; + } this.tableTitle = this.translate.instant('entity.type-calculated-fields'); - this.detailsPanelEnabled = false; - this.pageMode = false; + this.detailsPanelEnabled = this.pageMode; this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); + this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedFieldDialog.bind(this); - this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); + this.saveEntity = (cf) => this.calculatedFieldsService.saveCalculatedField(cf); + this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); + this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + + this.onEntityAction = action => this.onCFAction(action); + this.addActionDescriptors = [ { name: this.translate.instant('calculated-fields.create'), @@ -115,35 +146,37 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '250px'); - expressionColumn.sortable = false; - expressionColumn.cellContentFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel?.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; - } - expressionColumn.cellTooltipFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel?.length < 45 ? null : expressionLabel - }; - this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', '33%', + this.columns.push(new EntityTableColumn('name', 'common.name', this.pageMode ? '33%' : '60%', entity => this.utilsService.customTranslation(entity.name, entity.name))); - this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); - this.columns.push(expressionColumn); + if (this.pageMode) { + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + entity => this.utilsService.customTranslation(entity.entityName, entity.entityName), + entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); + } + this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '23%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.cellActionDescriptors.push( + { + name: this.translate.instant('action.copy'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.copyCalculatedField($event, entity), + }, { name: this.translate.instant('action.export'), icon: 'file_download', isEnabled: () => true, - onAction: (event$, entity) => this.exportCalculatedField(event$, entity), + onAction: ($event, entity) => this.exportCalculatedField($event, entity), }, { name: this.translate.instant('entity-view.events'), icon: 'mdi:clipboard-text-clock', isEnabled: () => true, - onAction: (_, entity) => this.openDebugEventsDialog(entity), + onAction: ($event, entity) => + this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity), }, { name: '', @@ -152,39 +185,31 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), - }, - { + } + ); + if (!this.pageMode) { + this.cellActionDescriptors.push({ name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, - onAction: (_, entity) => this.editCalculatedField(entity), - } - ); - } - - private getExpressionLabel(entity: CalculatedField): string { - if (entity.type === CalculatedFieldType.SCRIPT || - entity.type === CalculatedFieldType.PROPAGATION && entity.configuration.applyExpressionToResolvedArguments === true) { - return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')'; - } else if (entity.type === CalculatedFieldType.SIMPLE) { - return entity.configuration.expression ?? ''; + onAction: ($event, entity) => this.editCalculatedField($event, entity), + }) } - return ''; } - fetchCalculatedFields(pageLink: PageLink): Observable> { - return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); + fetchCalculatedFields(pageLink: PageLink): Observable> { + return this.pageMode ? + this.calculatedFieldsService.getCalculatedFields(pageLink, this.calculatedFieldFilterConfig): + this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink); } - onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + private onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void { + $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { ...this.additionalDebugActionConfig, - action: () => this.openDebugEventsDialog(calculatedField) + action: () => this.openDebugEventsDialog($event, calculatedField) }; - if ($event) { - $event.stopPropagation(); - } const { viewContainerRef, renderer } = this.entityDebugSettingsService; if (!viewContainerRef || !renderer) { @@ -202,7 +227,24 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 1) { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + } else { + table.entityDetailsPanel.matTabGroup._tabs.changes.pipe( + first() + ).subscribe(() => { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + }) + } + } + } + + private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void { + $event?.stopPropagation(); this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) .subscribe((res) => { if (res) { @@ -211,20 +253,23 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false, disabledSelectType = false): Observable { + const entityId = this.entityId || value?.entityId; + const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, buttonTitle, - entityId: this.entityId, + entityId, + entityName, tenantId: this.tenantId, - entityName: this.entityName, - ownerId: this.ownerId, + ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, additionalDebugActionConfig: this.additionalDebugActionConfig, getTestScriptDialogFn: this.getTestScriptDialog.bind(this), isDirty, + disabledSelectType }, enterAnimationDuration: isDirty ? 0 : null, }) @@ -232,33 +277,63 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig(CalculatedFieldDebugDialogComponent, { + private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity, openCalculatedFieldEdit = true, afterCloseCallback?: (expression: string) => void ): void { + $event?.stopPropagation(); + + const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => { + this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit) + .subscribe(expression => dialogRef.close(expression)); + }; + + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + title: 'calculated-fields.debugging', tenantId: this.tenantId, - value: calculatedField, - getTestScriptDialogFn: this.getTestScriptDialog.bind(this), + entityId: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + onDebugEventSelected, + debugActionDisabled: !debugCfActionEnabled(calculatedField) } }) .afterClosed() - .subscribe(); + .subscribe(value => { + if (afterCloseCallback) { + afterCloseCallback(value) + } + }); } - private exportCalculatedField($event: Event, calculatedField: CalculatedField): void { - if ($event) { - $event.stopPropagation(); - } + private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void { + $event?.stopPropagation(); this.importExportService.exportCalculatedField(calculatedField.id.id); } + private copyCalculatedField($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); + const copyCalculatedField = deepClone(calculatedField); + if (this.pageMode) { + copyCalculatedField.entityId = null; + delete (copyCalculatedField as CalculatedFieldInfo).entityName; + } + delete copyCalculatedField.id; + this.getCalculatedFieldDialog(copyCalculatedField, 'action.apply', false) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog() .pipe( filter(Boolean), switchMap(calculatedField => { - if (calculatedField.type === CalculatedFieldType.ALARM) { + if (calculatedField.type === CalculatedFieldType.ALARM || !CalculatedFieldType[calculatedField.type]) { this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'), type: 'error', @@ -270,7 +345,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true, true)), filter(Boolean), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), filter(Boolean), @@ -309,7 +384,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { if ( calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || @@ -338,7 +413,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { if (openCalculatedFieldEdit) { - this.editCalculatedField({ + this.editCalculatedField(null, { entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression} as any }, true) @@ -349,4 +424,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig): boolean { + switch (action.action) { + case 'open': + this.openCalculatedField(action.event, action.entity); + return true; + case 'export': + this.exportCalculatedField(action.event, action.entity); + return true; + } + return false; + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html index df433bc70e..102a334402 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html @@ -1,6 +1,6 @@ @if (calculatedFieldsTableConfig) { - + } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss index 3feb1e7429..175df5b779 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,7 @@ * limitations under the License. */ :host ::ng-deep { - tb-entities-table { - .mat-drawer-container { - background-color: white; - } + .white-background { + --mat-sidenav-content-background-color: white; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index ee3d013907..feba407f52 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -36,13 +36,15 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ - selector: 'tb-calculated-fields-table', - templateUrl: './calculated-fields-table.component.html', - styleUrls: ['./calculated-fields-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [EntityDebugSettingsService] + selector: 'tb-calculated-fields-table', + templateUrl: './calculated-fields-table.component.html', + styleUrls: ['./calculated-fields-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService], + standalone: false }) export class CalculatedFieldsTableComponent { @@ -55,6 +57,8 @@ export class CalculatedFieldsTableComponent { calculatedFieldsTableConfig: CalculatedFieldsTableConfig; + pageMode: boolean = false; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, @@ -65,10 +69,13 @@ export class CalculatedFieldsTableComponent { private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, private utilsService: UtilsService, - private destroyRef: DestroyRef) { - + private destroyRef: DestroyRef, + private route: ActivatedRoute, + private router: Router + ) { + this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { - if (this.active()) { + if (this.active() || this.pageMode) { this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( this.calculatedFieldsService, this.translate, @@ -83,6 +90,8 @@ export class CalculatedFieldsTableComponent { this.importExportService, this.entityDebugSettingsService, this.utilsService, + this.router, + this.pageMode, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html index 30533dc714..56f6cb0331 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -1,6 +1,6 @@ -
-
+
+
@@ -91,23 +93,36 @@
+ @if (!isEditValue) { + + } @else { + + } -
-
- {{ 'calculated-fields.no-arguments' | translate }} -
@if (errorText || (dataSource.isEmpty() | async)) { }
+
+ {{ 'calculated-fields.no-arguments' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss index 77ced9edef..7a2b96f497 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,10 @@ } } + .no-data-found { + font-size: 16px; + } + .max-args-warning { .mat-icon { color: #FAA405; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index bca335211e..14cfde23fe 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, DestroyRef, @@ -46,7 +47,7 @@ import { import { CalculatedFieldArgumentPanelComponent } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component'; -import { MatButton } from '@angular/material/button'; +import { MatIconButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; @@ -64,21 +65,22 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { BaseData } from '@shared/models/base-data'; @Component({ - selector: 'tb-calculated-field-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-calculated-field-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator, OnChanges, AfterViewInit { @@ -87,7 +89,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces @Input() entityName: string; @Input() ownerId: EntityId; @Input() isScript: boolean; - @Input() disabledAddButton = false; + @Input({transform: booleanAttribute}) disable = false; + @Input({transform: booleanAttribute}) isEditValue = true; @Input() watchKeyChange = false; @ViewChild(MatSort, { static: true }) sort: MatSort; @@ -153,7 +156,11 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces validate(): ValidationErrors | null { this.updateErrorText(); - return this.errorText ? { argumentsFormArray: false } : null; + return this.errorText || !this.argumentsFormArray.controls.length ? { argumentsFormArray: false } : null; + } + + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; } onDelete($event: Event, argument: CalculatedFieldArgumentValue): void { @@ -163,7 +170,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.argumentsFormArray.markAsDirty(); } - manageArgument($event: Event, matButton: MatButton, argument = {} as CalculatedFieldArgumentValue): void { + manageArgument($event: Event, matButton: MatIconButton, argument = {} as CalculatedFieldArgumentValue, readonly: boolean = false): void { $event?.stopPropagation(); if (this.popoverComponent && !this.popoverComponent.tbHidden) { this.popoverComponent.hide(); @@ -185,6 +192,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces ownerId: this.ownerId, watchKeyChange: this.watchKeyChange, usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName), + readonly }; this.popoverComponent = this.popoverService.displayPopover({ trigger, @@ -220,8 +228,6 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; - } else if (!this.argumentsFormArray.controls.length) { - this.errorText = 'calculated-fields.hint.arguments-empty'; } else { this.errorText = ''; } @@ -236,7 +242,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } writeValue(argumentsObj: Record): void { - this.argumentsFormArray.clear(); + this.argumentsFormArray.clear({emitEvent: false}); this.populateArgumentsFormArray(argumentsObj); this.updateEntityNameMap(this.argumentsFormArray.value); } @@ -246,7 +252,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } protected changeIsScriptMode(): void { - this.argumentsFormArray.updateValueAndValidity(); + this.argumentsFormArray.updateValueAndValidity({emitEvent: !this.disable}); } protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { @@ -261,7 +267,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces }; this.argumentsFormArray.push(this.fb.control(value), { emitEvent: false }); }); - this.argumentsFormArray.updateValueAndValidity(); + this.updateDataSource(this.argumentsFormArray.value); } private updateEntityNameMap(values: CalculatedFieldArgumentValue[]): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts index 494d6f0159..2b392564f9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts index 33dd5ffd8f..62dcff420e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -26,21 +26,22 @@ import { import { ArgumentEntityType } from '@shared/models/calculated-field.models'; @Component({ - selector: 'tb-entity-aggregation-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-entity-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts index d0e09e1d3e..b7208d0cbb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -41,21 +41,22 @@ import { isDefined, isUndefinedOrNull } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ - selector: 'tb-propagate-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => PropagateArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => PropagateArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-propagate-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagateArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagateArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts index 0c017f9ed0..52650bbf1c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -36,21 +36,22 @@ import { AliasFilterType } from '@shared/models/alias.models'; import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-related-aggregation-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-related-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss index 2e6b3952a7..f3218dbab7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts deleted file mode 100644 index 70bfa43c63..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -/// -/// 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 { AfterViewInit, Component, Inject, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { Router } from '@angular/router'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models'; -import { EventTableComponent } from '@home/components/event/event-table.component'; -import { - CalculatedField, - CalculatedFieldTestScriptFn, - CalculatedFieldType -} from '@shared/models/calculated-field.models'; - -export interface CalculatedFieldDebugDialogData { - tenantId: string; - value: CalculatedField; - getTestScriptDialogFn: CalculatedFieldTestScriptFn; -} - -@Component({ - selector: 'tb-calculated-field-debug-dialog', - styleUrls: ['calculated-field-debug-dialog.component.scss'], - templateUrl: './calculated-field-debug-dialog.component.html', -}) -export class CalculatedFieldDebugDialogComponent extends DialogComponent implements AfterViewInit { - - @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; - - readonly DebugEventType = DebugEventType; - readonly debugEventTypes = DebugEventType; - readonly EventType = EventType; - - dialogTitle: string; - - constructor(protected store: Store, - protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, - protected dialogRef: MatDialogRef) { - super(store, router, dialogRef); - this.dialogTitle = this.data.value.type === CalculatedFieldType.ALARM ? 'alarm-rule.debugging' : 'calculated-fields.debugging'; - } - - ngAfterViewInit(): void { - this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => { - return (this.data.value.type === CalculatedFieldType.SCRIPT || - (this.data.value.type === CalculatedFieldType.PROPAGATION && this.data.value.configuration.applyExpressionToResolvedArguments) - ) && !!(event as Event).body.arguments - }); - this.eventsTable.entitiesTable.updateData(); - } - - cancel(): void { - this.dialogRef.close(null); - } - - onDebugEventSelected(event: CalculatedFieldEventBody): void { - this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments)) - .subscribe(expression => this.dialogRef.close(expression)); - } -} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 3070a6cbb2..b463930c09 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -1,6 +1,6 @@ -
+

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

diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss index 5e83c1821c..b34e717714 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - .calculated-field-dialog-container { width: 869px; max-width: 100%; display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content; - --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); - --mdc-outlined-text-field-container-shape: 6px; + --mat-form-field-outlined-outline-color: rgba(0,0,0,0.12); + --mat-form-field-outlined-container-shape: 6px; --mat-form-field-trailing-icon-color: rgba(0, 0, 0, 0.56); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index ee7bafee83..4e37e67c71 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,26 +18,30 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { CalculatedField, CalculatedFieldConfiguration, + calculatedFieldsEntityTypeList, CalculatedFieldTestScriptFn, CalculatedFieldType, - CalculatedFieldTypeTranslations, - OutputStrategyType + calculatedFieldTypes, + CalculatedFieldTypeTranslations } from '@shared/models/calculated-field.models'; -import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models'; -import { pairwise, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { deepTrim, isDefined } from '@core/utils'; +import { deepTrim } from '@core/utils'; +import { BaseData } from '@shared/models/base-data'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { FormGroup } from '@angular/forms'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -49,31 +53,35 @@ export interface CalculatedFieldDialogData { additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; getTestScriptDialogFn: CalculatedFieldTestScriptFn; isDirty?: boolean; + disabledSelectType?: boolean; } @Component({ - selector: 'tb-calculated-field-dialog', - templateUrl: './calculated-field-dialog.component.html', - styleUrls: ['./calculated-field-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-calculated-field-dialog', + templateUrl: './calculated-field-dialog.component.html', + styleUrls: ['./calculated-field-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CalculatedFieldDialogComponent extends DialogComponent { - fieldFormGroup = this.fb.group({ - name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], - type: [CalculatedFieldType.SIMPLE], - debugSettings: [], - configuration: this.fb.control({} as CalculatedFieldConfiguration), - }); + fieldFormGroup: FormGroup; additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; + entityName = this.data.entityName; + ownerId = this.data.ownerId; + + disabledConfiguration = false; + isLoading = false; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; readonly CalculatedFieldType = CalculatedFieldType; - readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[]; + readonly fieldTypes = calculatedFieldTypes; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; constructor(protected store: Store, @@ -82,15 +90,37 @@ export class CalculatedFieldDialogComponent extends DialogComponent, private calculatedFieldsService: CalculatedFieldsService, private destroyRef: DestroyRef, - private fb: FormBuilder) { + private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); - this.observeIsLoading(); - this.observeType(); + this.fieldFormGroup = this.cfFormService.buildForm(); + this.cfFormService.setupTypeChange(this.fieldFormGroup, this.destroyRef); this.applyDialogData(); + + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + + if (!this.data.entityId) { + this.fieldFormGroup.get('entityId').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((entityId) => { + this.disabledConfiguration = !entityId; + if (this.disabledConfiguration) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + } else { + this.fieldFormGroup.get('configuration').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration').updateValueAndValidity({emitEvent: false}); + } + }); + } + + if (this.data.disabledSelectType) { + this.fieldFormGroup.get('type').disable({emitEvent: false}); + } } get fromGroupValue(): CalculatedField { - return deepTrim(this.fieldFormGroup.value as CalculatedField); + return deepTrim(this.fieldFormGroup.getRawValue() as CalculatedField); } cancel(): void { @@ -99,60 +129,51 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); + } else { + this.fieldFormGroup.get('name').markAsTouched(); } } onTestScript(expression?: string): Observable { - const calculatedFieldId = this.data.value?.id?.id; - if (calculatedFieldId) { - return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) - .pipe( - switchMap(event => { - const args = event?.arguments ? JSON.parse(event.arguments) : null; - return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression); - }), - takeUntilDestroyed(this.destroyRef) - ) - } - return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression); + return this.cfFormService.testScript( + this.data.value?.id?.id, + this.fromGroupValue, + this.data.getTestScriptDialogFn, + this.destroyRef, + expression + ); } - private applyDialogData(): void { - const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; - if (configuration.type !== CalculatedFieldType.ALARM) { - if (isDefined(configuration?.output) && !configuration?.output?.strategy) { - configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN}; - } + changeEntity(entity: BaseData): void { + this.entityName = entity.name; + if (this.isAssignedToCustomer(entity as AssetInfo | DeviceInfo)) { + this.ownerId = (entity as AssetInfo | DeviceInfo).customerId; } - this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); - setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); } - private observeIsLoading(): void { - this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { - if (loading) { - this.fieldFormGroup.disable({emitEvent: false}); - } else { - this.fieldFormGroup.enable({emitEvent: false}); - if (this.data.isDirty) { - this.fieldFormGroup.markAsDirty(); - } - } - }); + get entityId(): EntityId { + return this.data.entityId || this.fieldFormGroup.get('entityId').value; + } + + private applyDialogData(): void { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {}; + const preparedConfig = this.cfFormService.prepareConfig(configuration); + this.fieldFormGroup.patchValue({ configuration: preparedConfig, type, debugSettings, entityId, ...value }, {emitEvent: false}); + setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); + if (!this.data.entityId) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + this.disabledConfiguration = true; + } } - private observeType(): void { - this.fieldFormGroup.get('type').valueChanges.pipe( - pairwise(), - takeUntilDestroyed(this.destroyRef) - ).subscribe(([prevType, nextType]) => { - if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || - ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { - this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false}); - } - }); + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html index 4dcbf5b38d..ab7987c340 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -1,6 +1,6 @@
-
+
{{ 'calculated-fields.arguments' | translate }}
@@ -26,10 +26,11 @@
-
+
{{ 'calculated-fields.metrics.metrics' | translate }}
}
+
+ +
+ {{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }} +
+
+
EntityAggregationComponentComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EntityAggregationComponentComponent), - multi: true - } - ], + selector: 'tb-entity-aggregation-component', + templateUrl: './entity-aggregation-component.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + } + ], + standalone: false }) export class EntityAggregationComponentComponent implements ControlValueAccessor, Validator { @@ -87,7 +88,10 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor @Input() testScript: (expression?: string) => Observable; + @Input({transform: booleanAttribute}) isEditValue = true; + readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; + readonly intermediateAggregationIntervalInSecForCF = getCurrentAuthState(this.store).intermediateAggregationIntervalInSecForCF; readonly DayInSec = DAY / SECOND; entityAggregationConfiguration = this.fb.group({ @@ -104,6 +108,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor watermark: this.fb.group({ duration: [HOUR/SECOND, Validators.required], }), + produceIntermediateResult: [false], output: this.fb.control(defaultCalculatedFieldOutput), }); @@ -153,6 +158,15 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.updatedOffsetHint(); }); + merge( + this.entityAggregationConfiguration.get('interval.type').valueChanges, + this.entityAggregationConfiguration.get('interval.durationSec').valueChanges + ).pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.checkProduceIntermediate(); + }); + this.entityAggregationConfiguration.valueChanges.pipe( takeUntilDestroyed() ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { @@ -171,9 +185,12 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)} } this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); - this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); - this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); - this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + if (this.entityAggregationConfiguration.enabled) { + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); + } this.updatedOffsetHint(); setTimeout(() => { this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); @@ -194,6 +211,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); } } @@ -255,16 +273,49 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor } } + private checkProduceIntermediate() { + const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; + let durationSec = 0; + switch (intervalType) { + case AggIntervalType.CUSTOM: + durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; + break + case AggIntervalType.HOUR: + durationSec = HOUR / SECOND; + break + case AggIntervalType.DAY: + durationSec = DAY / SECOND; + break + case AggIntervalType.WEEK: + case AggIntervalType.WEEK_SUN_SAT: + durationSec = WEEK / SECOND; + break + case AggIntervalType.MONTH: + durationSec = AVG_MONTH / SECOND; + break + case AggIntervalType.QUARTER: + durationSec = AVG_QUARTER / SECOND; + break + case AggIntervalType.YEAR: + durationSec = YEAR / SECOND; + break + } + if (durationSec > this.intermediateAggregationIntervalInSecForCF) { + this.entityAggregationConfiguration.get('produceIntermediateResult').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('produceIntermediateResult').disable({emitEvent: false}); + } + } + private updatedOffsetHint(): void { const offset = this.entityAggregationConfiguration.get('interval.offsetSec').value; const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; const durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; const offsetCategory = this.getTimeCategory(offset); const now = _moment.utc(); - let interval: string = ''; + let interval: string; if (intervalType === AggIntervalType.CUSTOM) { - const durationSecCategory = this.getTimeCategory(durationSec); - const formatString = this.getCustomFormatString(offsetCategory, durationSecCategory); + const formatString = this.getCustomFormatString(offsetCategory, this.getTimeCategory(durationSec)); const intervals: string[] = []; let allInterval = durationSec >= HOUR*6/SECOND && durationSec < DAY/SECOND; now.startOf('year').add(offset, 'seconds'); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts index 44bbbc7237..347d99ed15 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html index 9f4e331cc1..bb191e8f26 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -1,6 +1,6 @@
-
+
@@ -94,22 +96,35 @@
- + @if (!isEditValue) { + + } @else { + + }
-
- {{ 'calculated-fields.no-zone-configured' | translate }} -
@if (errorText) { }
+
+ {{ 'calculated-fields.no-zone-configured' | translate }} +
@if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts index 9dc2e99465..4afbc83dc5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, DestroyRef, @@ -39,7 +40,7 @@ import { CalculatedFieldGeofencingValue, GeofencingReportStrategyTranslations, } from '@shared/models/calculated-field.models'; -import { MatButton } from '@angular/material/button'; +import { MatIconButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; @@ -60,21 +61,22 @@ import { } from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; @Component({ - selector: 'tb-calculated-field-geofencing-zone-groups-table', - templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', - styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), - multi: true - } - ], + selector: 'tb-calculated-field-geofencing-zone-groups-table', + templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + } + ], + standalone: false }) export class CalculatedFieldGeofencingZoneGroupsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { @@ -82,6 +84,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro @Input({required: true}) tenantId: string; @Input({required: true}) entityName: string; @Input({required: true}) ownerId: EntityId; + @Input({transform: booleanAttribute}) isEditValue = true; @ViewChild(MatSort, { static: true }) sort: MatSort; @@ -90,6 +93,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro entityNameMap = new Map(); sortOrder = { direction: 'asc', property: '' }; dataSource = new CalculatedFieldZoneDatasource(); + disable = false; readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; readonly entityTypeTranslations = entityTypeTranslations; @@ -135,6 +139,10 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro return this.errorText ? { zonesFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { $event.stopPropagation(); const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); @@ -142,7 +150,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro this.zoneGroupsFormArray.markAsDirty(); } - manageZone($event: Event, matButton: MatButton, zone = {} as CalculatedFieldGeofencingValue): void { + manageZone($event: Event, matButton: MatIconButton, zone = {} as CalculatedFieldGeofencingValue, readonly: boolean = false): void { $event?.stopPropagation(); if (this.popoverComponent && !this.popoverComponent.tbHidden) { this.popoverComponent.hide(); @@ -162,6 +170,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro entityName: this.entityName, ownerId: this.ownerId, usedNames: this.zoneGroupsFormArray.value.map(({ name }) => name).filter(name => name !== zone.name), + readonly }; this.popoverComponent = this.popoverService.displayPopover({ trigger, @@ -195,8 +204,6 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro private updateErrorText(): void { if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { this.errorText = 'calculated-fields.hint.geofencing-entity-not-found'; - } else if (!this.zoneGroupsFormArray.controls.length) { - this.errorText = 'calculated-fields.hint.geofencing-empty'; } else { this.errorText = ''; } @@ -211,7 +218,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro } writeValue(zonesObj: Record): void { - this.zoneGroupsFormArray.clear(); + this.zoneGroupsFormArray.clear({emitEvent: false}); this.populateZonesFormArray(zonesObj); this.updateEntityNameMap(this.zoneGroupsFormArray.value); } @@ -228,7 +235,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro }; this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false }); }); - this.zoneGroupsFormArray.updateValueAndValidity(); + this.updateDataSource(this.zoneGroupsFormArray.value); } private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html index e0580be7f1..25f736a222 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html @@ -1,6 +1,6 @@
-
+
@@ -54,6 +56,20 @@
{{ AggFunctionTranslations.get(metric.function) | translate }}
+ + + {{ 'calculated-fields.metrics.argument-name' | translate }} + + +
{{ metric.input.key }}
+
+
{{ 'calculated-fields.metrics.filtered' | translate }} @@ -77,17 +93,30 @@
- + @if (disable) { + + } @else { + + }
-
- {{ 'calculated-fields.metrics.no-metrics-configured' | translate }} -
- @if (errorText) { - - } +
+
+ {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
@if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts index ec916213e7..8301c99a44 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ import { CalculatedFieldAggMetric, CalculatedFieldAggMetricValue, } from '@shared/models/calculated-field.models'; -import { MatButton } from '@angular/material/button'; +import { MatIconButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { isDefinedAndNotNull, isEqual } from '@core/utils'; @@ -59,21 +59,22 @@ import { AceHighlightRules } from '@shared/models/ace/ace.models'; import { Observable } from "rxjs"; @Component({ - selector: 'tb-calculated-field-metrics-table', - templateUrl: './calculated-field-metrics-table.component.html', - styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), - multi: true - } - ], + selector: 'tb-calculated-field-metrics-table', + templateUrl: './calculated-field-metrics-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + } + ], + standalone: false }) export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { @@ -89,6 +90,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu metricsFormArray = this.fb.array([]); sortOrder = { direction: 'asc' as SortDirection, property: '' }; dataSource = new CalculatedFieldMetricsDatasource(); + disable = false; displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; @@ -116,7 +118,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu ngOnInit() { if (this.simpleMode) { - this.displayColumns = ['name', 'function', 'actions']; + this.displayColumns = ['name', 'function', 'argumentName', 'actions']; } } @@ -141,6 +143,10 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu return this.errorText ? { metricsFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { $event.stopPropagation(); const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); @@ -148,7 +154,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu this.metricsFormArray.markAsDirty(); } - manageMetrics($event: Event, matButton: MatButton, metric = {} as CalculatedFieldAggMetricValue): void { + manageMetrics($event: Event, matButton: MatIconButton, metric = {} as CalculatedFieldAggMetricValue, readonly: boolean = false): void { $event?.stopPropagation(); if (this.popoverComponent && !this.popoverComponent.tbHidden) { this.popoverComponent.hide(); @@ -168,7 +174,8 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu editorCompleter: this.editorCompleter, highlightRules: this.highlightRules, simpleMode: this.simpleMode, - testScript: this.testScript + testScript: this.testScript, + readonly }; this.popoverComponent = this.popoverService.displayPopover({ trigger, @@ -213,7 +220,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu } writeValue(metrics: Record): void { - this.metricsFormArray.clear(); + this.metricsFormArray.clear({emitEvent: false}); this.populateZonesFormArray(metrics); } @@ -225,7 +232,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu }; this.metricsFormArray.push(this.fb.control(value), { emitEvent: false }); }); - this.metricsFormArray.updateValueAndValidity(); + this.updateDataSource(this.metricsFormArray.value); } private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts index e7e45fd445..ee72828bc8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index f81d5afee2..aef5126fc3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -1,6 +1,6 @@
-
{{ 'calculated-fields.output' | translate }}
+
+ {{ 'calculated-fields.output' | translate }} +
@@ -28,7 +30,7 @@ @if (outputForm.get('type').value === OutputType.Attribute - && (entityId.entityType === EntityType.DEVICE || entityId.entityType === EntityType.DEVICE_PROFILE)) { + && (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.DEVICE_PROFILE)) { {{ 'calculated-fields.attribute-scope' | translate }} @@ -51,14 +53,9 @@ } @else {
- - {{ - (outputForm.get('type').value === OutputType.Timeseries - ? 'calculated-fields.timeseries-key' - : 'calculated-fields.attribute-key') - | translate - }} - + {{ (outputForm.get('type').value === OutputType.Timeseries + ? 'calculated-fields.timeseries-key' + : 'calculated-fields.attribute-key') | translate }} @if (outputForm.get('name').errors && outputForm.get('name').touched) { @@ -83,7 +80,7 @@
-
+
{{ 'calculated-fields.output-strategy.strategy' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss index 4b0d065555..b0d282b7f4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 239a4921e4..0fe3415ac1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -42,21 +42,22 @@ import { merge } from 'rxjs'; import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-calculate-field-output', - templateUrl: './calculated-field-output.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldOutputComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldOutputComponent), - multi: true - } - ], - styleUrls: ['./calculated-field-output.component.scss'], + selector: 'tb-calculate-field-output', + templateUrl: './calculated-field-output.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + } + ], + styleUrls: ['./calculated-field-output.component.scss'], + standalone: false }) export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @@ -78,6 +79,8 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val @Input({required: true}) entityId: EntityId; + disabled = false; + readonly outputTypes = Object.values(OutputType) as OutputType[]; readonly OutputType = OutputType; readonly AttributeScope = AttributeScope; @@ -120,6 +123,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.updatedStrategy(); }); + this.outputForm.get('strategy.saveTimeSeries').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => this.updateTimeSeriesTtl(value)); + merge( this.outputForm.get('strategy.type').valueChanges, this.outputForm.get('strategy.useCustomTtl').valueChanges @@ -142,7 +149,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val for (const propName of Object.keys(changes)) { const change = changes[propName]; if (change.currentValue !== change.previousValue) { - if (propName === 'simpleMode') { + if (propName === 'simpleMode' && !this.disabled) { this.updatedFormWithMode(); if (!change.firstChange) { this.outputForm.updateValueAndValidity(); @@ -161,7 +168,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) { this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false}); } - this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); + this.outputForm.get('type').updateValueAndValidity({onlySelf: true, emitEvent: false}); } registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { @@ -171,6 +178,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val registerOnTouched(_: any): void { } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.outputForm.disable({emitEvent: false}); } else { @@ -178,6 +186,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.updatedFormWithMode(); this.toggleScopeByOutputType(this.outputForm.get('type').value); this.updatedStrategy(); + this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value); + if (this.outputForm.invalid) { + this.outputForm.updateValueAndValidity(); + } } } @@ -242,4 +254,13 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val } } } + + private updateTimeSeriesTtl(value: boolean) { + if (value) { + this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false}); + } else { + this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false}); + this.outputForm.get('strategy.ttl').disable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts index 83b3970d9b..848aa1d5de 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html index bd92d25f92..bb06683d5d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -1,6 +1,6 @@
-
+
{{ 'calculated-fields.propagation-path-related-entities' | translate }}
@@ -55,12 +55,12 @@ [tenantId]="tenantId" [entityName]="entityName" [ownerId]="ownerId" - [isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/> + [watchKeyChange]="true" + [isEditValue]="isEditValue" + [isScript]="propagateConfiguration.get('applyExpressionToResolvedArguments').value"/>
-
-
- {{ 'calculated-fields.script' | translate }} -
+
+
{{ 'calculated-fields.script' | translate }}
@@ -89,8 +89,8 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index bce244c7ba..0db1c21c3b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -43,22 +43,26 @@ import { map } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@app/shared/models/rule-node.models'; import { EntitySearchDirection } from '@shared/models/relation.models'; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {getCurrentAuthState} from "@core/auth/auth.selectors"; @Component({ - selector: 'tb-propagation-configuration', - templateUrl: './propagation-configuration.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => PropagationConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => PropagationConfigurationComponent), - multi: true - } - ], + selector: 'tb-propagation-configuration', + templateUrl: './propagation-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + } + ], + standalone: false }) export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { @@ -77,6 +81,10 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, @Input({required: true}) testScript: () => Observable; + @Input({transform: booleanAttribute}) isEditValue = true; + + readonly maxRelatedEntitiesToReturnPerCfArgument = getCurrentAuthState(this.store).maxRelatedEntitiesToReturnPerCfArgument; + propagateConfiguration = this.fb.group({ arguments: this.fb.control({}, notEmptyObjectValidator()), applyExpressionToResolvedArguments: [false], @@ -88,6 +96,8 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, output: this.fb.control(defaultCalculatedFieldOutput), }); + disabled = false; + readonly ScriptLanguage = ScriptLanguage; readonly CalculatedFieldType = CalculatedFieldType; readonly OutputType = OutputType; @@ -108,7 +118,8 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, + private store: Store) { this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( takeUntilDestroyed() ).subscribe(() => { @@ -129,7 +140,9 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, writeValue(value: PropagationWithExpression): void { value.expression = value.expression ?? calculatedFieldDefaultScript; this.propagateConfiguration.patchValue(value, {emitEvent: false}); - this.updatedFormWithScript(); + if (!this.disabled) { + this.updatedFormWithScript(); + } setTimeout(() => { this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); }); @@ -142,6 +155,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, registerOnTouched(_: any): void { } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.propagateConfiguration.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts index 83dc4badf9..209f27eec1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index d4d4f9d1da..476714e3cb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,5 +15,4 @@ /// export * from './dialog/calculated-field-dialog.component'; -export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './test-dialog/calculated-field-script-test-dialog.component'; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html index af3139801b..f3ac0eefd8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -1,6 +1,6 @@
-
+
{{ 'calculated-fields.aggregation-path-related-entities' | translate }}
@@ -48,6 +48,7 @@ [entityId]="entityId" [tenantId]="tenantId" [entityName]="entityName" + [isEditValue]="isEditValue" [relation]="relatedAggregationConfiguration.get('relation').value"/>
@@ -63,21 +64,23 @@
-
- -
-
calculated-fields.use-latest-timestamp
-
-
-
+ @if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) { +
+ +
+
calculated-fields.use-latest-timestamp
+
+
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss index 6a8faea40f..d012d20732 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts index 57ec370b61..907bb31abb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -46,21 +46,22 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-related-entities-aggregation-component', - templateUrl: './related-entities-aggregation-component.component.html', - styleUrl: './related-entities-aggregation-component.component.scss', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), - multi: true - } - ], + selector: 'tb-related-entities-aggregation-component', + templateUrl: './related-entities-aggregation-component.component.html', + styleUrl: './related-entities-aggregation-component.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + } + ], + standalone: false }) export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { @@ -76,12 +77,15 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu @Input({required: true}) testScript: (expression?: string) => Observable; + @Input({transform: booleanAttribute}) isEditValue = true; + readonly ScriptLanguage = ScriptLanguage; readonly CalculatedFieldType = CalculatedFieldType; readonly OutputType = OutputType; readonly Directions = Object.values(EntitySearchDirection) as Array; readonly PropagationDirectionTranslations = PropagationDirectionTranslations; readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; + readonly maxRelatedEntitiesToReturnPerCfArgument = getCurrentAuthState(this.store).maxRelatedEntitiesToReturnPerCfArgument; relatedAggregationConfiguration = this.fb.group({ relation: this.fb.group({ @@ -117,7 +121,12 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu takeUntilDestroyed() ).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => { this.updatedModel(value); - }) + }); + this.relatedAggregationConfiguration.get('output').valueChanges.pipe( + takeUntilDestroyed(), + ).subscribe(() => { + this.toggleScopeByOutputType(); + }); } validate(): ValidationErrors | null { @@ -155,4 +164,12 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu value.scheduledUpdateInterval = this.minAllowedScheduledUpdateIntervalInSecForCF; this.propagateChange(value); } + + private toggleScopeByOutputType(): void { + if (this.relatedAggregationConfiguration.get('output').value.type === OutputType.Attribute) { + this.relatedAggregationConfiguration.get('useLatestTs').disable({emitEvent: false}); + } else { + this.relatedAggregationConfiguration.get('useLatestTs').enable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts index abc64568ec..aaa7905845 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html index cfab9d9def..161d2dcc25 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -1,6 +1,6 @@
-
{{ 'calculated-fields.arguments' | translate }}
+
+ {{ 'calculated-fields.arguments' | translate }} +
-
+
{{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }}
@@ -71,7 +77,7 @@ matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="simpleConfiguration.get('arguments').invalid" + [disabled]="simpleConfiguration.get('arguments').invalid || disabled" (click)="onTestScript()"> bug_report @@ -80,7 +86,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index 8dc6f01726..f2f9fbac2f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { booleanAttribute, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -46,20 +46,21 @@ import { map } from 'rxjs/operators'; type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; @Component({ - selector: 'tb-simple-configuration', - templateUrl: './simple-configuration.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SimpleConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SimpleConfigurationComponent), - multi: true - } - ], + selector: 'tb-simple-configuration', + templateUrl: './simple-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + } + ], + standalone: false }) export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { @@ -81,6 +82,8 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid @Input({required: true}) testScript: () => Observable; + @Input({transform: booleanAttribute}) isEditValue = true; + simpleConfiguration = this.fb.group({ arguments: this.fb.control({}), expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], @@ -104,6 +107,8 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) ); + disabled = false; + private propagateChange: (config: SimpeConfiguration) => void = () => { }; constructor(private fb: FormBuilder) { @@ -127,7 +132,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid for (const propName of Object.keys(changes)) { const change = changes[propName]; if (change.currentValue !== change.previousValue) { - if (propName === 'isScript') { + if (propName === 'isScript' && !this.disabled) { this.updatedFormWithScript(); if (!change.firstChange) { this.simpleConfiguration.updateValueAndValidity(); @@ -149,9 +154,9 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid formValue.expressionSIMPLE = formValue.expression; } this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); - this.updatedFormWithScript(); setTimeout(() => { this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + this.simpleConfiguration.get('output').updateValueAndValidity({onlySelf: true}); }); } @@ -163,6 +168,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.simpleConfiguration.disable({emitEvent: false}); } else { @@ -195,7 +201,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid } private toggleScopeByOutputType(): void { - if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) { + if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute || this.disabled) { this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false}); } else { this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts index 2e5e14426e..11c6899ae6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html index f393a9130b..83b758a6d8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html @@ -1,6 +1,6 @@ + + + + + + + + + + + +
+ + + + +
+ +
+ +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + + {{ entityTypeTranslations.get(type)?.type | translate }} + + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss new file mode 100644 index 0000000000..beb6c4a343 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +::ng-deep { + .tb-calculated-fields-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts new file mode 100644 index 0000000000..36095579e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts @@ -0,0 +1,295 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + DestroyRef, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { fromEvent, Subscription } from 'rxjs'; +import { POSITION_MAP } from '@shared/models/overlay.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + calculatedFieldsEntityTypeList, + CalculatedFieldsQuery, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { TranslateService } from '@ngx-translate/core'; + +export const CALCULATED_FIELDS_CONFIG_DATA = new InjectionToken('CalculatedFieldsFilterConfigData'); + +export interface CalculatedFieldsFilterConfigData { + panelMode: boolean; + userMode: boolean; + filterConfig: CalculatedFieldsQuery; + initialFilterConfig?: CalculatedFieldsQuery; +} + +@Component({ + selector: 'tb-calculated-fields-filter-config', + templateUrl: './calculated-fields-filter-config.component.html', + styleUrls: ['./calculated-fields-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldsFilterConfigComponent), + multi: true + } + ], + standalone: false +}) +export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlValueAccessor { + + @ViewChild('calculatedFieldsFilterPanel') + calculatedFieldsFilterPanel: TemplateRef; + + @Input() + disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @Input() + initialCfFilterConfig: CalculatedFieldsQuery = { + types: [], + entityType: null, + entities: [] + }; + + panelMode = false; + + buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + + cfFilterForm: FormGroup; + + panelResult: CalculatedFieldsQuery = null; + + entityType = EntityType; + + listEntityTypes = calculatedFieldsEntityTypeList; + entityTypeTranslations = entityTypeTranslations; + + readonly types: StringItemsOption[] = calculatedFieldTypes.map(item => ({ + name: this.translate.instant(CalculatedFieldTypeTranslations.get(item).name), + value: item + })); + + private cfFilterOverlayRef: OverlayRef; + private cfFilterConfig: CalculatedFieldsQuery; + private resizeWindows: Subscription; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(CALCULATED_FIELDS_CONFIG_DATA) + private data: CalculatedFieldsFilterConfigData | undefined, + @Optional() private overlayRef: OverlayRef, + private fb: FormBuilder, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private translate: TranslateService) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.cfFilterConfig = this.data.filterConfig; + this.initialCfFilterConfig = this.data.initialFilterConfig; + if (this.panelMode && !this.initialCfFilterConfig) { + this.initialCfFilterConfig = deepClone(this.cfFilterConfig); + } + } + this.cfFilterForm = this.fb.group({ + types: [null, []], + entityType: [null, []], + entities: [null, []] + }); + this.cfFilterForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + if (!this.buttonMode) { + this.cfConfigUpdated(this.cfFilterForm.value); + } + } + ); + if (this.panelMode) { + this.updateCfConfigForm(this.cfFilterConfig); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.cfFilterForm.disable({emitEvent: false}); + } else { + this.cfFilterForm.enable({emitEvent: false}); + } + } + + writeValue(cfFilterConfig?: CalculatedFieldsQuery): void { + this.cfFilterConfig = cfFilterConfig; + if (!this.initialCfFilterConfig && cfFilterConfig) { + this.initialCfFilterConfig = deepClone(cfFilterConfig); + } + this.updateButtonDisplayValue(); + this.updateCfConfigForm(cfFilterConfig); + } + + toggleCfFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + this.cfFilterOverlayRef = this.overlay.create(config); + this.cfFilterOverlayRef.backdropClick().subscribe(() => { + this.cfFilterOverlayRef.dispose(); + }); + this.cfFilterOverlayRef.attach(new TemplatePortal(this.calculatedFieldsFilterPanel, + this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.cfFilterOverlayRef.updatePosition(); + }); + } + + cancel() { + this.updateCfConfigForm(this.cfFilterConfig); + this.cfFilterForm.markAsPristine(); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + update() { + this.cfConfigUpdated(this.cfFilterForm.value); + this.cfFilterForm.markAsPristine(); + if (this.panelMode) { + this.panelResult = this.cfFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + reset() { + const cfFilterConfig = this.cfFilterFromFormValue(this.cfFilterForm.value); + if (!this.cfFilterConfigEquals(cfFilterConfig, this.initialCfFilterConfig)) { + this.updateCfConfigForm(this.initialCfFilterConfig); + this.cfFilterForm.markAsDirty(); + } + } + + private cfFilterConfigEquals = (filter1?: CalculatedFieldsQuery, filter2?: CalculatedFieldsQuery): boolean => { + if (filter1 === filter2) { + return true; + } + if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) { + return true; + } else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) { + if (!isArraysEqualIgnoreUndefined(filter1.types, filter2.types)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { + return false; + } + return filter1.entityType === filter2.entityType; + } + return false; + }; + + private updateCfConfigForm(cfFilterConfig?: CalculatedFieldsQuery) { + this.cfFilterForm.patchValue({ + types: cfFilterConfig?.types ?? [], + entityType: cfFilterConfig?.entityType ?? null, + entities: cfFilterConfig?.entities ?? [], + }, {emitEvent: false}); + } + + private cfConfigUpdated(formValue: any) { + this.cfFilterConfig = this.cfFilterFromFormValue(formValue); + this.updateButtonDisplayValue(); + this.propagateChange(this.cfFilterConfig); + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.cfFilterConfig?.types?.length) { + filterTextParts.push(this.cfFilterConfig.types.map((type) => this.translate.instant(CalculatedFieldTypeTranslations.get(type).name)).join(', ')); + } + if (this.cfFilterConfig?.entityType) { + filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.cfFilterConfig.entityType).type)); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('calculated-fields.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + + private cfFilterFromFormValue(formValue: any): CalculatedFieldsQuery { + return { + types: formValue?.types ?? [], + entityType: formValue?.entityType ?? null, + entities: formValue?.entities ?? [], + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html new file mode 100644 index 0000000000..f24c112f27 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss new file mode 100644 index 0000000000..c6571e8450 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + padding-right: 8px; + overflow: hidden; + max-width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts new file mode 100644 index 0000000000..07ed7deeda --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { CalculatedField, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; +import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; + +@Component({ + selector: 'tb-calculated-fields-table-header', + templateUrl: './calculated-fields-header.component.html', + styleUrls: ['./calculated-fields-header.component.scss'], + standalone: false +}) +export class CalculatedFieldsHeaderComponent extends EntityTableHeaderComponent { + + get calculatedFieldsTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfig as CalculatedFieldsTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + calculatedFieldsFilterChanged(calculatedFieldFilterConfig: CalculatedFieldsQuery) { + this.calculatedFieldsTableConfig.calculatedFieldFilterConfig = calculatedFieldFilterConfig; + this.calculatedFieldsTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index dacfa60d22..0e1b2a7e07 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -1,6 +1,6 @@ - - {{ 'device-profile.device-profile' | translate }} + + {{ 'device-profile.device-profile' | translate }} DeviceProfileAutocompleteComponent), - multi: true - }] + selector: 'tb-device-profile-autocomplete', + templateUrl: './device-profile-autocomplete.component.html', + styleUrls: ['./device-profile-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), + multi: true + }], + standalone: false }) export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { @@ -69,6 +70,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, modelValue: DeviceProfileId | null; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Input() subscriptSizing: SubscriptSizing = 'fixed'; @@ -106,6 +110,10 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @Input() hint: string; + @Input() + @coerceBoolean() + inlineField: boolean; + @Output() deviceProfileUpdated = new EventEmitter(); diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html index 80da175031..246912b170 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html @@ -1,6 +1,6 @@ - + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -87,6 +89,8 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { this.mapWidgetConfigForm = this.fb.group({ mapSettings: [settings, []], + timewindowConfig: [getTimewindowConfig(configData.config), []], + showTitle: [configData.config.showTitle, []], title: [configData.config.title, []], titleFont: [configData.config.titleFont, []], @@ -106,15 +110,10 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { actions: [configData.config.actions || {}, []] }); - if (this.trip) { - this.mapWidgetConfigForm.addControl('timewindowConfig', this.fb.control(getTimewindowConfig(configData.config))) - } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - if (this.trip) { - setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); - } + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.settings = config.mapSettings || {}; this.widgetConfig.config.showTitle = config.showTitle; @@ -186,4 +185,20 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { config.enableFullscreen = buttons.includes('fullscreen'); } + public get displayTimewindowConfig(): boolean { + if (this.trip) { + return true; + } else { + return this.widget ? MapModelDefinition.hasTimewindow(this.widget) : false; + } + } + + public get onlyHistoryTimewindow(): boolean { + if (this.trip) { + return false; + } else { + return this.widget ? MapModelDefinition.datasourcesHasOnlyComparisonAggregation(this.widget) : false; + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html index 4dab1821cb..606f092199 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html @@ -1,6 +1,6 @@ -
+
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss index 3fa268f735..d073f12894 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts index 0a48f67c9c..b994ff8b23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -52,9 +52,10 @@ const squareLayoutSize = 160; const horizontalLayoutHeight = 80; @Component({ - selector: 'tb-value-card-widget', - templateUrl: './value-card-widget.component.html', - styleUrls: ['./value-card-widget.component.scss'] + selector: 'tb-value-card-widget', + templateUrl: './value-card-widget.component.html', + styleUrls: ['./value-card-widget.component.scss'], + standalone: false }) export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts index f7a1c77b6e..eea2ef7b7e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html index b403014035..8d276d1231 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html @@ -1,6 +1,6 @@ +
+
+ {{ labelKey | translate}} +
+
+ + + + {{ ValueSourceTypeTranslation.get(type) | translate }} + + + + + +
+
+ + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts new file mode 100644 index 0000000000..24037f2170 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, UntypedFormControl, + UntypedFormGroup, ValidationErrors, Validator, + Validators, +} from '@angular/forms'; +import { + ValueSourceConfig, + ValueSourceType, + ValueSourceTypes, + ValueSourceTypeTranslation +} from '@shared/models/widget-settings.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DataKey, Datasource, DatasourceType, } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { merge } from 'rxjs'; + +@Component({ + selector: 'tb-axis-scale-row', + templateUrl: './axis-scale-row.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + ], + standalone: false +}) +export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + isPanelView = false; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + labelKey: string; + + ValueSourceType = ValueSourceType; + + DataKeyType = DataKeyType; + + DatasourceType = DatasourceType; + + ValueSourceTypeTranslation = ValueSourceTypeTranslation; + + ValueSourceTypes = ValueSourceTypes; + + limitForm: UntypedFormGroup; + + latestKeyFormControl: UntypedFormControl; + + entityKeyFormControl: UntypedFormControl; + + private propagateChanges: (value: any) => void = () => {}; + + private modelValue: ValueSourceConfig | null = null; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.limitForm = this.fb.group({ + type: [ValueSourceType.constant], + value: [null], + entityAlias: [null] + }); + this.latestKeyFormControl = this.fb.control(null, [Validators.required]); + this.entityKeyFormControl = this.fb.control(null, [Validators.required]); + merge( + this.latestKeyFormControl.valueChanges, + this.entityKeyFormControl.valueChanges, + this.limitForm.valueChanges + ).pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.updateValidators(); + this.updateModel(); + }); + } + + writeValue(value: ValueSourceConfig) { + this.modelValue = value; + this.limitForm.patchValue( + { + type: value.type || ValueSourceType.constant, + value: value.value, + entityAlias: value.entityAlias, + }, {emitEvent: false} + ); + if (value.type === ValueSourceType.latestKey) { + this.latestKeyFormControl.patchValue({ + type: value.latestKeyType, + name: value.latestKey + }, {emitEvent: false}); + } else if (value.type === ValueSourceType.entity) { + this.entityKeyFormControl.patchValue({ + type: value.entityKeyType, + name: value.entityKey + }, {emitEvent: false}); + } + + this.updateValidators(); + this.limitForm.markAllAsTouched(); + } + + registerOnChange(fn: any) { + this.propagateChanges = fn; + } + + registerOnTouched(fn: any) { + } + + validate(): ValidationErrors | null { + const type = this.limitForm.get('type')?.value; + const errors: any = {}; + + if (this.limitForm.invalid) { + errors.form = false; + } + + if (type === ValueSourceType.latestKey) { + if (!this.latestKeyFormControl.value || this.latestKeyFormControl.invalid) { + errors.latestKey = false; + } + } else if (type === ValueSourceType.entity) { + if (!this.limitForm.get('entityAlias')?.value) { + errors.entityAlias = false; + } + if (!this.entityKeyFormControl.value || this.entityKeyFormControl.invalid) { + errors.entityKey = false; + } + } + + return Object.keys(errors).length ? { axisLimitForm: errors } : null; + } + + private updateValidators() { + const axisTypeControl = this.limitForm.get('type'); + if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { + const type = axisTypeControl.value; + if (type === ValueSourceType.latestKey) { + this.latestKeyFormControl.setValidators([Validators.required]); + this.entityKeyFormControl.clearValidators(); + } else if (type === ValueSourceType.entity) { + this.latestKeyFormControl.clearValidators(); + this.limitForm.get('entityAlias').setValidators([Validators.required]); + this.entityKeyFormControl.setValidators([Validators.required]); + } else { + this.latestKeyFormControl.clearValidators(); + this.entityKeyFormControl.clearValidators(); + } + this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); + this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); + } + } + + private updateModel() { + const value = this.limitForm.value; + const type = value.type; + let updates: Partial = { type }; + if (type === ValueSourceType.latestKey) { + const latestKey: DataKey = this.latestKeyFormControl.value; + updates.latestKey = latestKey?.name; + updates.latestKeyType = latestKey?.type as any; + } else if (type === ValueSourceType.entity) { + const entityKey: DataKey = this.entityKeyFormControl.value; + updates.entityKey = entityKey?.name; + updates.entityKeyType = entityKey?.type as any; + updates.entityAlias = value?.entityAlias; + } else { + updates.value = value?.value; + } + this.modelValue = updates as ValueSourceConfig; + this.propagateChanges(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html index ed3533a1f1..d0cffac423 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html @@ -1,6 +1,6 @@ -
TripTimelineSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TripTimelineSettingsComponent), - multi: true - } - ] + selector: 'tb-trip-timeline-settings', + templateUrl: './trip-timeline-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripTimelineSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripTimelineSettingsComponent), + multi: true + } + ], + standalone: false }) export class TripTimelineSettingsComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html index 3367868788..d0a1ef44c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html @@ -1,6 +1,6 @@
-

{{ dialogTitle | translate}}

+

{{ data.title | translate}}

- -
-
- + + common.password + + lock + + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + + + login.password-again + + lock + + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 85396338d5..c72f1751a2 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -16,29 +16,32 @@ import { Component } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; +import { finalize } from 'rxjs/operators'; +import { PageComponent } from '@shared/components/page.component'; @Component({ - selector: 'tb-create-password', - templateUrl: './create-password.component.html', - styleUrls: ['./create-password.component.scss'] + selector: 'tb-create-password', + templateUrl: './create-password.component.html', + styleUrls: ['./password.component.scss'], + standalone: false }) export class CreatePasswordComponent extends PageComponent { passwordPolicy: UserPasswordPolicy; createPassword: FormGroup; + isLoading = false; + private activateToken: string; constructor(private route: ActivatedRoute, private authService: AuthService, private fb: FormBuilder) { super(); - this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; @@ -60,9 +63,10 @@ export class CreatePasswordComponent extends PageComponent { if (this.createPassword.invalid) { this.createPassword.markAllAsTouched(); } else { - this.authService.activate( - this.activateToken, - this.createPassword.get('newPassword').value, true).subscribe(); + this.isLoading = true; + this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true).pipe( + finalize(() => {this.isLoading = false;}) + ).subscribe(() => {}); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 94bd0b2cf0..495d6fa0cb 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -1,6 +1,6 @@ -