Browse Source

Merge branch 'improvements/connectors-config' of github.com:Dmitriymush/thingsboard into improvements/connectors-config

pull/10482/head
Dmitriymush 2 years ago
parent
commit
2ab8eb8f66
  1. 6
      application/src/main/data/json/system/widget_bundles/status_indicators.json
  2. 26
      application/src/main/data/json/system/widget_types/status_widget.json
  3. 2
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  4. 4
      application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java
  5. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  6. 19
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java
  7. 5
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java
  8. 24
      common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java
  9. 8
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
  10. 6
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java
  11. 6
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  12. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  13. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java
  14. 49
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java
  15. 10
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  16. 6
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java
  17. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java
  18. 6
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java
  19. 3
      monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java
  20. 3
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java
  21. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java
  22. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java
  23. 1
      ui-ngx/package.json
  24. 23
      ui-ngx/src/app/app.component.ts
  25. 15
      ui-ngx/src/app/core/auth/auth.service.ts
  26. 4
      ui-ngx/src/app/core/core.module.ts
  27. 7
      ui-ngx/src/app/core/services/resources.service.ts
  28. 24
      ui-ngx/src/app/core/settings/settings.effects.ts
  29. 10
      ui-ngx/src/app/core/settings/settings.utils.ts
  30. 30
      ui-ngx/src/app/core/translate/translate-default-loader.ts
  31. 2
      ui-ngx/src/app/core/utils.ts
  32. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  33. 101
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html
  34. 119
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts
  35. 33
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html
  36. 99
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss
  37. 242
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts
  38. 204
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts
  39. 111
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html
  40. 158
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts
  41. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  42. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html
  43. 79
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts
  44. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  45. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  46. 2
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  47. 2
      ui-ngx/src/app/shared/components/json-content.component.html
  48. 101
      ui-ngx/src/app/shared/components/json-content.component.ts
  49. 8263
      ui-ngx/src/assets/locale/locale.constant-ar_AR.json
  50. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  51. 20
      ui-ngx/src/assets/widget/status-widget/center-layout.svg
  52. 20
      ui-ngx/src/assets/widget/status-widget/default-layout.svg
  53. 18
      ui-ngx/src/assets/widget/status-widget/icon-layout.svg
  54. 7
      ui-ngx/yarn.lock

6
application/src/main/data/json/system/widget_bundles/status_indicators.json

@ -2,15 +2,15 @@
"widgetsBundle": {
"alias": "status_indicators",
"title": "Status indicators",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC",
"image": "tb-image:c3RhdHVzX2luZGljYXRvcnNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IlN0YXR1cyBpbmRpY2F0b3JzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC",
"description": "Contains widgets displaying battery level and signal strength.",
"order": 9000,
"externalId": null,
"name": "Status indicators"
},
"widgetTypeFqns": [
"battery_level",
"signal_strength",
"progress_bar"
"progress_bar",
"status_widget"
]
}

26
application/src/main/data/json/system/widget_types/status_widget.json

File diff suppressed because one or more lines are too long

2
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -647,7 +647,7 @@ public class TelemetryController extends BaseController {
Map<Long, List<KvEntry>> telemetryRequest;
JsonElement telemetryJson;
try {
telemetryJson = new JsonParser().parse(requestBody);
telemetryJson = JsonParser.parseString(requestBody);
} catch (Exception e) {
return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
}

4
application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java

@ -147,7 +147,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest
void checkDiffBetweenLatestTsForDevicesAndAsset(List<Device> devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException {
TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ);
Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent());
JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject();
JsonObject assetJsonObject = JsonParser.parseString(assetTsKvEntry.getJsonValue().get()).getAsJsonObject();
for (Device device : devices) {
Long assetValue = assetJsonObject.get(device.getName()).getAsLong();
TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER);
@ -182,7 +182,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest
JsonObject getJsonObject(String key, long value, Optional<String> tsKvEntryOpt) {
JsonObject jsonObject = new JsonObject();
if (tsKvEntryOpt.isPresent()) {
jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject();
jsonObject = JsonParser.parseString(tsKvEntryOpt.get()).getAsJsonObject();
}
jsonObject.addProperty(key, value);
return jsonObject;

7
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -67,6 +67,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
@ -147,9 +148,9 @@ public class LwM2MTestClient {
initializer.setClassForObject(SECURITY, Security.class);
initializer.setInstancesForObject(SECURITY, instances);
// SERVER
Server lwm2mServer = new Server(shortServerId, 300);
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
Server serverBs = new Server(shortServerIdBs0, 300);
Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60));
serverBs.setId(serverIdBs);
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
initializer.setClassForObject(SERVER, Server.class);
@ -163,7 +164,7 @@ public class LwM2MTestClient {
// SECURITY
initializer.setInstancesForObject(SECURITY, security);
// SERVER
Server lwm2mServer = new Server(shortServerId, 300);
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer );
}

19
common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java

@ -66,7 +66,6 @@ import java.util.stream.Collectors;
public class JsonConverter {
private static final Gson GSON = new Gson();
private static final JsonParser JSON_PARSER = new JsonParser();
private static final String CAN_T_PARSE_VALUE = "Can't parse value: ";
private static final String DEVICE_PROPERTY = "device";
@ -111,7 +110,7 @@ public class JsonConverter {
public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) {
long durationMs = 0L;
if (json != null && !json.isEmpty()) {
return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json));
return convertToClaimDeviceProto(deviceId, JsonParser.parseString(json));
}
return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs);
}
@ -160,7 +159,7 @@ public class JsonConverter {
result.addProperty("id", msg.getRequestId());
}
result.addProperty("method", msg.getMethodName());
result.add("params", JSON_PARSER.parse(msg.getParams()));
result.add("params", JsonParser.parseString(msg.getParams()));
return result;
}
@ -367,7 +366,7 @@ public class JsonConverter {
json.addProperty(name, entry.getLongV());
break;
case JSON_V:
json.add(name, JSON_PARSER.parse(entry.getJsonV()));
json.add(name, JsonParser.parseString(entry.getJsonV()));
break;
}
}
@ -388,7 +387,7 @@ public class JsonConverter {
result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getStringV()));
break;
case JSON_V:
result.add(de.getKv().getKey(), JSON_PARSER.parse(de.getKv().getJsonV()));
result.add(de.getKv().getKey(), JsonParser.parseString(de.getKv().getJsonV()));
break;
default:
throw new IllegalArgumentException("Unsupported data type: " + de.getKv().getType());
@ -412,7 +411,7 @@ public class JsonConverter {
result.add(de.getKey(), new JsonPrimitive(de.getStrValue().get()));
break;
case JSON:
result.add(de.getKey(), JSON_PARSER.parse(de.getJsonValue().get()));
result.add(de.getKey(), JsonParser.parseString(de.getJsonValue().get()));
break;
default:
throw new IllegalArgumentException("Unsupported data type: " + de.getDataType());
@ -422,7 +421,7 @@ public class JsonConverter {
public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) {
if (StringUtils.isEmpty(msg.getError())) {
return JSON_PARSER.parse(msg.getPayload());
return JsonParser.parseString(msg.getPayload());
} else {
JsonObject errorMsg = new JsonObject();
errorMsg.addProperty("error", msg.getError());
@ -456,7 +455,7 @@ public class JsonConverter {
result.addProperty("credentialsValue", payload.getCredentialsValue());
break;
case MQTT_BASIC:
result.add("credentialsValue", JSON_PARSER.parse(payload.getCredentialsValue()).getAsJsonObject());
result.add("credentialsValue", JsonParser.parseString(payload.getCredentialsValue()).getAsJsonObject());
break;
case LWM2M_CREDENTIALS:
break;
@ -572,7 +571,7 @@ public class JsonConverter {
}
public static JsonElement parse(String json) {
return JSON_PARSER.parse(json);
return JsonParser.parseString(json);
}
public static <T> T parse(String json, Class<T> clazz) {
@ -600,7 +599,7 @@ public class JsonConverter {
}
public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) {
JsonElement jsonElement = JSON_PARSER.parse(json);
JsonElement jsonElement = JsonParser.parseString(json);
if (jsonElement.isJsonObject()) {
return buildProvisionRequestMsg(jsonElement.getAsJsonObject());
} else {

5
common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java

@ -39,7 +39,6 @@ import java.util.List;
public class ProtoConverter {
public static final Gson GSON = new Gson();
public static final JsonParser JSON_PARSER = new JsonParser();
public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException {
TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload);
@ -165,7 +164,7 @@ public class ProtoConverter {
break;
case JSON_V:
try {
JSON_PARSER.parse(keyValueProto.getJsonV());
JsonParser.parseString(keyValueProto.getJsonV());
} catch (Exception e) {
throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!");
}
@ -184,7 +183,7 @@ public class ProtoConverter {
rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId());
String params = toDeviceRpcRequestMsg.getParams();
try {
JsonElement paramsElement = JSON_PARSER.parse(params);
JsonElement paramsElement = JsonParser.parseString(params);
rpcRequestJson.add("params", paramsElement);
DynamicMessage dynamicRpcRequest = DynamicProtoUtils.jsonToDynamicMessage(rpcRequestDynamicMessageBuilder, GSON.toJson(rpcRequestJson));
return dynamicRpcRequest.toByteArray();

24
common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java

@ -26,8 +26,6 @@ import java.util.ArrayList;
public class JsonConverterTest {
private final JsonParser JSON_PARSER = new JsonParser();
@BeforeEach
public void before() {
JsonConverter.setTypeCastEnabled(true);
@ -35,55 +33,55 @@ public class JsonConverterTest {
@Test
public void testParseBigDecimalAsLong() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E+1}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E+1}"), 0L);
Assert.assertEquals(10L, result.get(0L).get(0).getLongValue().get().longValue());
}
@Test
public void testParseBigDecimalAsDouble() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 101E-1}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 101E-1}"), 0L);
Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0);
}
@Test
public void testParseAttributesBigDecimalAsLong() {
var result = new ArrayList<>(JsonConverter.convertToAttributes(JSON_PARSER.parse("{\"meterReadingDelta\": 1E1}")));
var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}")));
Assert.assertEquals(10L, result.get(0).getLongValue().get().longValue());
}
@Test
public void testParseAsDoubleWithZero() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 42.0}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 42.0}"), 0L);
Assert.assertEquals(42.0, result.get(0L).get(0).getDoubleValue().get(), 0.0);
}
@Test
public void testParseAsDouble() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1.1}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1.1}"), 0L);
Assert.assertEquals(1.1, result.get(0L).get(0).getDoubleValue().get(), 0.0);
}
@Test
public void testParseAsLong() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 11}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 11}"), 0L);
Assert.assertEquals(11L, result.get(0L).get(0).getLongValue().get().longValue());
}
@Test
public void testParseBigDecimalAsStringOutOfLongRange() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
Assert.assertEquals("99701010061400066000", result.get(0L).get(0).getStrValue().get());
}
@Test
public void testParseBigDecimalAsStringOutOfLongRange2() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 99701010061400066001}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 99701010061400066001}"), 0L);
Assert.assertEquals("99701010061400066001", result.get(0L).get(0).getStrValue().get());
}
@Test
public void testParseBigDecimalAsStringOutOfLongRange3() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E19}"), 0L);
var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E19}"), 0L);
Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get());
}
@ -91,7 +89,7 @@ public class JsonConverterTest {
public void testParseBigDecimalOutOfLongRangeWithoutParsing() {
JsonConverter.setTypeCastEnabled(false);
Assertions.assertThrows(JsonSyntaxException.class, () -> {
JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L);
JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 89701010051400054084}"), 0L);
});
}
@ -99,7 +97,7 @@ public class JsonConverterTest {
public void testParseBigDecimalOutOfLongRangeWithoutParsing2() {
JsonConverter.setTypeCastEnabled(false);
Assertions.assertThrows(JsonSyntaxException.class, () -> {
JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
});
}
}

8
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java

@ -45,7 +45,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false);
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) {
throw new AdaptorException(ex);
}
@ -55,7 +55,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false);
try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload));
return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) {
throw new AdaptorException(ex);
}
@ -70,7 +70,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException {
Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
String payload = validatePayload(sessionId, inbound, false);
JsonObject response = new JsonParser().parse(payload).getAsJsonObject();
JsonObject response = JsonParser.parseString(payload).getAsJsonObject();
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")))
.setPayload(response.toString()).build();
}
@ -78,7 +78,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
@Override
public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false);
return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0);
return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), 0);
}
@Override

6
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java

@ -45,7 +45,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
ProtoConverter.validateDescriptor(telemetryMsgDescriptor);
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor)));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor)));
} catch (Exception e) {
throw new AdaptorException(e);
}
@ -55,7 +55,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException {
ProtoConverter.validateDescriptor(attributesMsgDescriptor);
try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor)));
return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor)));
} catch (Exception e) {
throw new AdaptorException(e);
}
@ -74,7 +74,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
} else {
ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor);
try {
JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor));
JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor));
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")))
.setPayload(response.toString()).build();
} catch (Exception e) {

6
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -185,7 +185,7 @@ public class DeviceApiController implements TbTransportService {
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
TransportService transportService = transportContext.getTransportService();
transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)),
transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(JsonParser.parseString(json)),
new HttpOkCallback(responseWriter));
}));
return responseWriter;
@ -205,7 +205,7 @@ public class DeviceApiController implements TbTransportService {
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
TransportService transportService = transportContext.getTransportService();
transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)),
transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(JsonParser.parseString(json)),
new HttpOkCallback(responseWriter));
}));
return responseWriter;
@ -301,7 +301,7 @@ public class DeviceApiController implements TbTransportService {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
JsonObject request = new JsonParser().parse(json).getAsJsonObject();
JsonObject request = JsonParser.parseString(json).getAsJsonObject();
TransportService transportService = transportContext.getTransportService();
transportService.registerSyncSession(sessionInfo,
new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo),

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java

@ -50,7 +50,6 @@ import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType.
public class LwM2mTransportServerHelper {
private final LwM2mTransportContext context;
private final static JsonParser JSON_PARSER = new JsonParser();
public void sendParametersOnThingsboardAttribute(List<TransportProtos.KeyValueProto> result, SessionInfoProto sessionInfo) {
PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder();
@ -231,7 +230,7 @@ public class LwM2mTransportServerHelper {
return kv.getStringV();
case JSON_V:
try {
return JSON_PARSER.parse(kv.getJsonV());
return JsonParser.parseString(kv.getJsonV());
} catch (Exception e) {
return null;
}

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java

@ -90,7 +90,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class);
// Redis key prefixes
private static final String REG_EP = "REG:EP:"; // (Endpoint => Registration)
public static final String REG_EP = "REG:EP:"; // (Endpoint => Registration)
private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint)
private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint)
private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint)

49
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java

@ -15,17 +15,24 @@
*/
package org.thingsboard.server.transport.lwm2m.server.store;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.peer.OscoreIdentity;
import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.JavaSerDesUtil;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
import java.util.concurrent.locks.Lock;
import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.REG_EP;
@Slf4j
public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
private static final String SEC_EP = "SEC#EP#";
private static final String LOCK_EP = "LOCK#EP#";
@ -49,11 +56,31 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
if (data == null || data.length == 0) {
return null;
} else {
if (SecurityMode.NO_SEC.equals(((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityMode())) {
TbLwM2MSecurityInfo tbLwM2MSecurityInfo = JavaSerDesUtil.decode(data);
if (tbLwM2MSecurityInfo != null) {
if (SecurityMode.NO_SEC.equals(tbLwM2MSecurityInfo.getSecurityMode())){
// for tests: redis connect NoSec (securityInfo == null)
log.info("lwm2m redis securityStore (decode -ok). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data);
return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(),
SecurityMode.NO_SEC.toString().getBytes());
} else {
return tbLwM2MSecurityInfo.getSecurityInfo();
}
} else if (SecurityMode.NO_SEC.equals(getSecurityModeByRegistration (connection, endpoint))){
// for tests: redis connect NoSec (securityInfo == null)
log.info("lwm2m redis securityStore (decode - bad, registration - unsecure). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data);
return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(),
SecurityMode.NO_SEC.toString().getBytes());
} else {
return ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityInfo();
// for tests: redis connect NoSec (securityInfo == null)
log.info("lwm2m redis securityStore (decode - bad, registration is not unsecure) - return null. Endpoint: [{}], key: [{}], data [{}]", endpoint, SEC_EP, data);
return null;
}
}
} finally {
@ -112,6 +139,11 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
}
byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized);
// for tests: redis connect NoSec (securityInfo == null)
log.info("lwm2m redis connect. Endpoint: [{}], secMode: [{}] key: [{}], tbSecurityInfoSerialized [{}]",
tbSecurityInfo.getEndpoint(), tbSecurityInfo.getSecurityMode().name(), SEC_EP, tbSecurityInfoSerialized);
if (previousData != null && info != null) {
String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity();
if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) {
@ -168,4 +200,17 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
private String toLockKey(String endpoint) {
return LOCK_EP + endpoint;
}
private SecurityMode getSecurityModeByRegistration (RedisConnection connection, String endpoint) {
try {
byte[] data = connection.get((REG_EP + endpoint).getBytes());
JsonNode registrationNode = JacksonUtil.fromString(new String(data != null ? data : new byte[0]), JsonNode.class);
String typeModeStr = registrationNode.get("transportdata").get("identity").get("type").asText();
return "unsecure".equals(typeModeStr) ? SecurityMode.NO_SEC : null;
} catch (Exception e) {
log.error("Redis: Failed get SecurityMode by Registration, endpoint: [{}]", endpoint, e);
return null;
}
}
}

10
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -59,7 +59,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) {
log.debug("Failed to decode post telemetry request", ex);
throw new AdaptorException(ex);
@ -70,7 +70,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload));
return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) {
log.debug("Failed to decode post attributes request", ex);
throw new AdaptorException(ex);
@ -162,7 +162,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException {
String payload = validatePayload(sessionId, payloadData, false);
try {
return new JsonParser().parse(payload);
return JsonParser.parseString(payload);
} catch (JsonSyntaxException ex) {
log.debug("Payload is in incorrect format: {}", payload);
throw new AdaptorException(ex);
@ -175,7 +175,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder();
result.setRequestId(getRequestId(topicName, topicBase));
String payload = inbound.payload().toString(UTF8);
JsonElement requestBody = new JsonParser().parse(payload);
JsonElement requestBody = JsonParser.parseString(payload);
Set<String> clientKeys = toStringSet(requestBody, "clientKeys");
Set<String> sharedKeys = toStringSet(requestBody, "sharedKeys");
if (clientKeys != null) {
@ -208,7 +208,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try {
int requestId = getRequestId(topicName, topicBase);
return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId);
return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), requestId);
} catch (IllegalStateException | JsonSyntaxException ex) {
log.debug("Failed to decode to server rpc request", ex);
throw new AdaptorException(ex);

6
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java

@ -50,7 +50,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor());
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) {
log.debug("Failed to decode post telemetry request", e);
throw new AdaptorException(e);
@ -63,7 +63,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor());
try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor)));
return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor)));
} catch (Exception e) {
log.debug("Failed to decode post attributes request", e);
throw new AdaptorException(e);
@ -102,7 +102,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor());
try {
int requestId = getRequestId(topicName, topicBase);
JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor));
JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor));
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build();
} catch (Exception e) {
log.debug("Failed to decode rpc response", e);

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -94,7 +94,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
byte[] bytes = getBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor());
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) {
log.debug("Failed to decode post telemetry request", e);
throw new AdaptorException(e);

6
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java

@ -24,8 +24,6 @@ import java.util.List;
public class JsonUtils {
private static final JsonParser jsonParser = new JsonParser();
public static JsonObject getJsonObject(List<KeyValueProto> tsKv) {
JsonObject json = new JsonObject();
for (KeyValueProto kv : tsKv) {
@ -43,7 +41,7 @@ public class JsonUtils {
json.addProperty(kv.getKey(), kv.getStringV());
break;
case JSON_V:
json.add(kv.getKey(), jsonParser.parse(kv.getJsonV()));
json.add(kv.getKey(), JsonParser.parseString(kv.getJsonV()));
break;
}
}
@ -51,7 +49,7 @@ public class JsonUtils {
}
public static JsonElement parse(String params) {
return jsonParser.parse(params);
return JsonParser.parseString(params);
}
}

3
monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java

@ -51,6 +51,7 @@ import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.eclipse.leshan.client.object.Security.noSec;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
@ -95,7 +96,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model);
initializer.setInstancesForObject(SECURITY, security);
initializer.setInstancesForObject(SERVER, new Server(123, 300));
initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(60)));
initializer.setInstancesForObject(DEVICE, this);
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();

3
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java

@ -61,6 +61,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
@ -118,7 +119,7 @@ public class LwM2MTestClient {
// SECURITY
initializer.setInstancesForObject(SECURITY, security);
// SERVER
Server lwm2mServer = new Server(shortServerId, 300);
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer);

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java

@ -72,7 +72,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
public class TbSaveToCustomCassandraTableNode implements TbNode {
private static final String TABLE_PREFIX = "cs_tb_";
private static final JsonParser parser = new JsonParser();
private static final String ENTITY_ID = "$entityId";
private TbSaveToCustomCassandraTableNodeConfiguration config;
@ -168,7 +167,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode {
}
private ListenableFuture<Void> save(TbMsg msg, TbContext ctx) {
JsonElement data = parser.parse(msg.getData());
JsonElement data = JsonParser.parseString(msg.getData());
if (!data.isJsonObject()) {
throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data);
} else {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java

@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
@ -75,7 +74,6 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage";
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final Gson gson = new Gson();
private final JsonParser parser = new JsonParser();
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
@ -88,7 +86,7 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
.find(ctx.getTenantId(), msg.getOriginator(), AttributeScope.SERVER_SCOPE, ctx.getServiceId())
.get(1, TimeUnit.MINUTES);
if (entry.isPresent()) {
JsonObject element = parser.parse(entry.get().getValueAsString()).getAsJsonObject();
JsonObject element = JsonParser.parseString(entry.get().getValueAsString()).getAsJsonObject();
return new EntityGeofencingState(element.get("inside").getAsBoolean(), element.get("stateSwitchTime").getAsLong(), element.get("stayed").getAsBoolean());
} else {
return new EntityGeofencingState(false, 0L, false);

1
ui-ngx/package.json

@ -43,7 +43,6 @@
"@ngrx/store": "^15.4.0",
"@ngrx/store-devtools": "^15.4.0",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.0",
"@tinymce/tinymce-angular": "^7.0.0",

23
ui-ngx/src/app/app.component.ts

@ -27,10 +27,13 @@ import { LocalStorageService } from '@core/local-storage/local-storage.service';
import { DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material/icon';
import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { getCurrentAuthState, selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip, tap } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
import { isEqual } from '@core/utils';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { SETTINGS_KEY } from '@core/settings/settings.effects';
@Component({
selector: 'tb-root',
@ -92,8 +95,16 @@ export class AppComponent implements OnInit {
this.store.pipe(select(selectIsUserLoaded))]
).pipe(
map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),
distinctUntilChanged(),
filter((data) => data.isUserLoaded ),
filter((data) => data.isUserLoaded),
distinctUntilChanged((a, b) => isEqual(a, b)),
tap((data) => {
let userLang = getCurrentAuthState(this.store).userDetails?.additionalInfo?.lang ?? null;
if (!userLang && !data.isAuthenticated) {
const settings = this.storageService.getItem(SETTINGS_KEY);
userLang = settings?.userLang ?? null;
}
this.notifyUserLang(userLang);
}),
skip(1),
).subscribe((data) => {
this.authService.gotoDefaultPlace(data.isAuthenticated);
@ -111,4 +122,8 @@ export class AppComponent implements OnInit {
}
}
private notifyUserLang(userLang: string) {
this.store.dispatch(new ActionSettingsChangeLanguage({userLang}));
}
}

15
ui-ngx/src/app/core/auth/auth.service.ts

@ -22,7 +22,7 @@ import { Observable, of, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { Router, UrlTree } from '@angular/router';
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils';
import { UserService } from '../http/user.service';
import { Store } from '@ngrx/store';
@ -35,7 +35,6 @@ import {
} from './auth.actions';
import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions';
import { AuthPayload, AuthState, SysParams, SysParamsState } from '@core/auth/auth.models';
import { TranslateService } from '@ngx-translate/core';
import { AuthUser } from '@shared/models/user.model';
@ -59,7 +58,6 @@ export class AuthService {
private userService: UserService,
private timeService: TimeService,
private router: Router,
private route: ActivatedRoute,
private zone: NgZone,
private utils: UtilsService,
private translate: TranslateService,
@ -419,14 +417,7 @@ export class AuthService {
this.loadSystemParams().subscribe(
(sysParams) => {
authPayload = {...authPayload, ...sysParams};
let userLang;
if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) {
userLang = authPayload.userDetails.additionalInfo.lang;
} else {
userLang = null;
}
loadUserSubject.next(authPayload);
this.notifyUserLang(userLang);
loadUserSubject.complete();
},
(err) => {
@ -607,10 +598,6 @@ export class AuthService {
this.store.dispatch(new ActionAuthAuthenticated(authPayload));
}
private notifyUserLang(userLang: string) {
this.store.dispatch(new ActionSettingsChangeLanguage({userLang}));
}
private updateAndValidateToken(token, prefix, notify) {
let valid = false;
const tokenData = this.jwtHelper.decodeToken(token);

4
ui-ngx/src/app/core/core.module.ts

@ -31,7 +31,6 @@ import {
TranslateModule,
TranslateParser
} from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TbMissingTranslationHandler } from './translate/missing-translate-handler';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog';
@ -41,9 +40,10 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp
import { WINDOW_PROVIDERS } from '@core/services/window.service';
import { HotkeyModule } from 'angular2-hotkeys';
import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
import { TranslateDefaultLoader } from '@core/translate/translate-default-loader';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
return new TranslateDefaultLoader(http);
}
@NgModule({

7
ui-ngx/src/app/core/services/resources.service.ts

@ -34,6 +34,7 @@ import { select, Store } from '@ngrx/store';
import { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { map, tap } from 'rxjs/operators';
import { RequestConfig } from '@core/http/http-utils';
declare const System;
@ -106,11 +107,11 @@ export class ResourcesService {
return this.loadResourceByType(fileType, url);
}
public downloadResource(downloadUrl: string): Observable<any> {
return this.http.get(downloadUrl, {
public downloadResource(downloadUrl: string, config?: RequestConfig): Observable<any> {
return this.http.get(downloadUrl, {...config, ...{
responseType: 'arraybuffer',
observe: 'response'
}).pipe(
}}).pipe(
map((response) => {
const headers = response.headers;
const filename = headers.get('x-filename');

24
ui-ngx/src/app/core/settings/settings.effects.ts

@ -28,7 +28,6 @@ import { AppState } from '@app/core/core.state';
import { LocalStorageService } from '@app/core/local-storage/local-storage.service';
import { TitleService } from '@app/core/services/title.service';
import { updateUserLang } from '@app/core/settings/settings.utils';
import { AuthService } from '@core/auth/auth.service';
import { UtilsService } from '@core/services/utils.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions';
@ -40,7 +39,6 @@ export class SettingsEffects {
constructor(
private actions$: Actions<SettingsActions>,
private store: Store<AppState>,
private authService: AuthService,
private utils: UtilsService,
private router: Router,
private localStorageService: LocalStorageService,
@ -49,26 +47,19 @@ export class SettingsEffects {
) {
}
persistSettings = createEffect(() => this.actions$.pipe(
setTranslateServiceLanguage = createEffect(() => this.actions$.pipe(
ofType(
SettingsActionTypes.CHANGE_LANGUAGE,
),
withLatestFrom(this.store.pipe(select(selectSettingsState))),
tap(([action, settings]) =>
this.localStorageService.setItem(SETTINGS_KEY, settings)
)
), {dispatch: false});
setTranslateServiceLanguage = createEffect(() => this.store.pipe(
select(selectSettingsState),
map(settings => settings.userLang),
distinctUntilChanged(),
tap(userLang => updateUserLang(this.translate, userLang))
map(settings => settings[1]),
distinctUntilChanged((a, b) => a?.userLang === b?.userLang),
tap(setting => {
this.localStorageService.setItem(SETTINGS_KEY, setting);
updateUserLang(this.translate, setting.userLang);
})
), {dispatch: false});
setTitle = createEffect(() => merge(
this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)),
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
@ -81,7 +72,6 @@ export class SettingsEffects {
})
), {dispatch: false});
setPublicId = createEffect(() => merge(
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
).pipe(

10
ui-ngx/src/app/core/settings/settings.utils.ts

@ -18,7 +18,7 @@ import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import * as _moment from 'moment';
export function updateUserLang(translate: TranslateService, userLang: string) {
export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs) {
let targetLang = userLang;
if (!env.production) {
console.log(`User lang: ${targetLang}`);
@ -29,7 +29,7 @@ export function updateUserLang(translate: TranslateService, userLang: string) {
console.log(`Fallback to browser lang: ${targetLang}`);
}
}
const detectedSupportedLang = detectSupportedLang(targetLang);
const detectedSupportedLang = detectSupportedLang(targetLang, translations);
if (!env.production) {
console.log(`Detected supported lang: ${detectedSupportedLang}`);
}
@ -37,10 +37,10 @@ export function updateUserLang(translate: TranslateService, userLang: string) {
_moment.locale([detectedSupportedLang]);
}
function detectSupportedLang(targetLang: string): string {
function detectSupportedLang(targetLang: string, translations: string[]): string {
const langTag = (targetLang || '').split('-').join('_');
if (langTag.length) {
if (env.supportedLangs.indexOf(langTag) > -1) {
if (translations.indexOf(langTag) > -1) {
return langTag;
} else {
const parts = langTag.split('_');
@ -50,7 +50,7 @@ function detectSupportedLang(targetLang: string): string {
} else {
lang = langTag;
}
const foundLangs = env.supportedLangs.filter(
const foundLangs = translations.filter(
(supportedLang: string) => {
const supportedLangParts = supportedLang.split('_');
return supportedLangParts[0] === lang;

30
ui-ngx/src/app/core/translate/translate-default-loader.ts

@ -0,0 +1,30 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export class TranslateDefaultLoader implements TranslateLoader {
constructor(private http: HttpClient) {
}
getTranslation(lang: string): Observable<object> {
return this.http.get(`/assets/locale/locale.constant-${lang}.json`);
}
}

2
ui-ngx/src/app/core/utils.ts

@ -338,6 +338,8 @@ export const isEmpty = (a: any): boolean => _.isEmpty(a);
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
export const setByPath = <T extends object>(object: T, path: string | number | symbol, value: any): T => _.set(object, path, value);
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
if (a === b) {
return true;

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -114,6 +114,9 @@ import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/
import {
ComparisonKeysTableComponent
} from '@home/components/widget/config/basic/chart/comparison-keys-table.component';
import {
StatusWidgetBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/status-widget-basic-config.component';
@NgModule({
declarations: [
@ -151,7 +154,8 @@ import {
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent
ComparisonKeysTableComponent,
StatusWidgetBasicConfigComponent
],
imports: [
CommonModule,
@ -191,7 +195,8 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent
TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -225,5 +230,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent,
'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent,
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent,
'tb-status-widget-basic-config': StatusWidgetBasicConfigComponent
};

101
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html

@ -0,0 +1,101 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="statusWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.status-widget.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of statusWidgetLayouts"
[value]="layout"
[image]="statusWidgetLayoutImageMap.get(layout)">
{{ statusWidgetLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-toggle-select [(ngModel)]="cardStyleMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="on">{{ 'widgets.status-widget.on' | translate }}</tb-toggle-option>
<tb-toggle-option value="off">{{ 'widgets.status-widget.off' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'on'"
[layout]="statusWidgetConfigForm.get('layout').value"
formControlName="onState">
</tb-status-widget-state-settings>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'off'"
[layout]="statusWidgetConfigForm.get('layout').value"
formControlName="offState">
</tb-status-widget-state-settings>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

119
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts

@ -0,0 +1,119 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES 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 { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import {
statusWidgetDefaultSettings,
statusWidgetLayoutImages,
statusWidgetLayouts,
statusWidgetLayoutTranslations,
StatusWidgetSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
@Component({
selector: 'tb-status-widget-basic-config',
templateUrl: './status-widget-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.statusWidgetConfigForm.get('targetDevice').value;
}
statusWidgetLayouts = statusWidgetLayouts;
statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations;
statusWidgetLayoutImageMap = statusWidgetLayoutImages;
valueType = ValueType;
statusWidgetConfigForm: UntypedFormGroup;
cardStyleMode = 'on';
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.statusWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: StatusWidgetSettings = {...statusWidgetDefaultSettings, ...(configData.config.settings || {})};
this.statusWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
onState: [settings.onState, []],
offState: [settings.offState, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.onState = config.onState;
this.widgetConfig.config.settings.offState = config.offState;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
}

33
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html

@ -0,0 +1,33 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div #statusWidgetPanel class="tb-status-widget-panel" [style]="backgroundStyle$ | async">
<div class="tb-status-widget-overlay" [style]="overlayStyle" [style.inset]="overlayInset"></div>
<div class="tb-status-widget-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #statusWidgetContent class="tb-status-widget-content" [class]="this.layout">
<div class="tb-status-widget-icon-container">
<tb-icon [style]="iconStyle">{{ icon }}</tb-icon>
</div>
<div class="tb-status-widget-labels-container">
<div *ngIf="showLabel" class="tb-status-widget-label" [style]="labelStyle">{{ label$ | async }}</div>
<div *ngIf="showStatus" class="tb-status-widget-status" [style]="statusStyle">{{ status$ | async }}</div>
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

99
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss

@ -0,0 +1,99 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-status-widget-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
> div:not(.tb-status-widget-overlay), > tb-icon {
z-index: 1;
}
.tb-status-widget-overlay {
position: absolute;
inset: 12px;
}
> div.tb-status-widget-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
.tb-status-widget-content {
width: 100%;
height: 100%;
padding: 16px;
position: relative;
display: flex;
flex-direction: column;
.tb-status-widget-icon-container {
display: flex;
width: 100%;
flex-direction: column;
}
.tb-status-widget-labels-container {
display: flex;
width: 100%;
flex-direction: column;
.tb-status-widget-label {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-status-widget-status {
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.default {
place-content: flex-start space-between;
align-items: flex-start;
}
&.center {
place-content: center flex-start;
align-items: center;
.tb-status-widget-icon-container {
flex: 1;
place-content: center;
align-items: center;
}
.tb-status-widget-labels-container {
flex-direction: column-reverse;
place-content: center flex-start;
align-items: center;
}
}
&.icon {
place-content: center;
align-items: center;
.tb-status-widget-icon-container {
flex: 1;
place-content: center;
align-items: center;
}
.tb-status-widget-labels-container {
display: none;
}
}
}
}

242
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts

@ -0,0 +1,242 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
Renderer2, ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import {
statusWidgetDefaultSettings,
StatusWidgetLayout,
StatusWidgetSettings, StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
import { Observable } from 'rxjs';
import {
backgroundStyle,
ComponentStyle,
iconStyle,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { UtilsService } from '@core/services/utils.service';
import { ValueType } from '@shared/models/constants';
const initialStatusWidgetSize = 147;
@Component({
selector: 'tb-status-widget',
templateUrl: './status-widget.component.html',
styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class StatusWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('statusWidgetPanel', {static: false})
statusWidgetPanel: ElementRef<HTMLElement>;
@ViewChild('statusWidgetContent', {static: false})
statusWidgetContent: ElementRef<HTMLElement>;
settings: StatusWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
overlayInset = '12px';
borderRadius = '';
layout: StatusWidgetLayout;
showLabel = true;
label$: Observable<string>;
labelStyle: ComponentStyle = {};
showStatus = true;
status$: Observable<string>;
statusStyle: ComponentStyle = {};
icon = '';
iconStyle: ComponentStyle = {};
private panelResize$: ResizeObserver;
private onLabel$: Observable<string>;
private onStatus$: Observable<string>;
private onBackground$: Observable<ComponentStyle>;
private onBackgroundDisabled$: Observable<ComponentStyle>;
private offLabel$: Observable<string>;
private offStatus$: Observable<string>;
private offBackground$: Observable<ComponentStyle>;
private offBackgroundDisabled$: Observable<ComponentStyle>;
private state = false;
private disabled = false;
private disabledState = false;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private utils: UtilsService,
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...statusWidgetDefaultSettings, ...this.ctx.settings};
this.layout = this.settings.layout;
this.onLabel$ = this.ctx.registerLabelPattern(this.settings.onState.label, this.onLabel$);
this.onStatus$ = this.ctx.registerLabelPattern(this.settings.onState.status, this.onStatus$);
this.onBackground$ = backgroundStyle(this.settings.onState.background, this.imagePipe, this.sanitizer);
this.onBackgroundDisabled$ = backgroundStyle(this.settings.onState.backgroundDisabled, this.imagePipe, this.sanitizer);
this.offLabel$ = this.ctx.registerLabelPattern(this.settings.offState.label, this.offLabel$);
this.offStatus$ = this.ctx.registerLabelPattern(this.settings.offState.status, this.offStatus$);
this.offBackground$ = backgroundStyle(this.settings.offState.background, this.imagePipe, this.sanitizer);
this.offBackgroundDisabled$ = backgroundStyle(this.settings.offState.backgroundDisabled, this.imagePipe, this.sanitizer);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onState(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
this.loading$.subscribe((loading) => {
this.updateDisabledState(loading || this.disabled);
});
this.updateStyle(this.state, this.disabled);
}
ngAfterViewInit(): void {
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.statusWidgetPanel.nativeElement);
if (this.showLabel) {
this.panelResize$.observe(this.statusWidgetPanel.nativeElement);
}
this.onResize();
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
this.borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}};
this.cd.detectChanges();
}
private onState(value: boolean): void {
const newState = !!value;
if (this.state !== newState) {
this.state = newState;
this.updateStyle(this.state, this.disabled || this.disabledState);
}
}
private onDisabled(value: boolean): void {
const newDisabled = !!value;
if (this.disabled !== newDisabled) {
this.disabled = newDisabled;
this.updateDisabledState(this.disabled);
}
}
private updateDisabledState(disabled: boolean) {
this.disabledState = disabled;
this.updateStyle(this.state, this.disabledState);
}
private onResize() {
const panelWidth = this.statusWidgetPanel.nativeElement.getBoundingClientRect().width;
const panelHeight = this.statusWidgetPanel.nativeElement.getBoundingClientRect().height;
const targetSize = Math.min(panelWidth, panelHeight);
const scale = targetSize / initialStatusWidgetSize;
const width = initialStatusWidgetSize;
const height = initialStatusWidgetSize;
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'height', height + 'px');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'transform', `scale(${scale})`);
this.overlayInset = (Math.floor(12 * scale * 100) / 100) + 'px';
this.cd.markForCheck();
}
private updateStyle(state: boolean, disabled: boolean) {
let stateSettings: StatusWidgetStateSettings;
if (state) {
this.label$ = this.onLabel$;
this.status$ = this.onStatus$;
this.backgroundStyle$ = disabled ? this.onBackgroundDisabled$ : this.onBackground$;
stateSettings = this.settings.onState;
} else {
this.label$ = this.offLabel$;
this.status$ = this.offStatus$;
this.backgroundStyle$ = disabled ? this.offBackgroundDisabled$ : this.offBackground$;
stateSettings = this.settings.offState;
}
this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon;
this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon;
this.icon = stateSettings.icon;
const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor;
const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor;
this.labelStyle = textStyle(stateSettings.labelFont);
this.labelStyle.color = primaryColor;
this.statusStyle = textStyle(stateSettings.statusFont);
this.statusStyle.color = secondaryColor;
this.iconStyle = iconStyle(stateSettings.iconSize, stateSettings.iconSizeUnit);
this.iconStyle.color = primaryColor;
this.overlayStyle = overlayStyle(disabled ? stateSettings.backgroundDisabled.overlay : stateSettings.background.overlay);
if (this.borderRadius) {
this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}};
}
this.cd.detectChanges();
}
}

204
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts

@ -0,0 +1,204 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
export enum StatusWidgetLayout {
default = 'default',
center = 'center',
icon = 'icon'
}
export const statusWidgetLayouts = Object.keys(StatusWidgetLayout) as StatusWidgetLayout[];
export const statusWidgetLayoutTranslations = new Map<StatusWidgetLayout, string>(
[
[StatusWidgetLayout.default, 'widgets.status-widget.layout-default'],
[StatusWidgetLayout.center, 'widgets.status-widget.layout-center'],
[StatusWidgetLayout.icon, 'widgets.status-widget.layout-icon']
]
);
export const statusWidgetLayoutImages = new Map<StatusWidgetLayout, string>(
[
[StatusWidgetLayout.default, 'assets/widget/status-widget/default-layout.svg'],
[StatusWidgetLayout.center, 'assets/widget/status-widget/center-layout.svg'],
[StatusWidgetLayout.icon, 'assets/widget/status-widget/icon-layout.svg']
]
);
export interface StatusWidgetStateSettings {
showLabel: boolean;
label: string;
labelFont: Font;
showStatus: boolean;
status: string;
statusFont: Font;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
primaryColor: string;
secondaryColor: string;
background: BackgroundSettings;
primaryColorDisabled: string;
secondaryColorDisabled: string;
backgroundDisabled: BackgroundSettings;
}
export interface StatusWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
layout: StatusWidgetLayout;
onState: StatusWidgetStateSettings;
offState: StatusWidgetStateSettings;
}
export const statusWidgetDefaultSettings: StatusWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: false,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
layout: StatusWidgetLayout.default,
onState: {
showLabel: true,
label: 'Window left corner',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
showStatus: true,
status: 'Opened',
statusFont: {
family: 'Roboto',
size: 10,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
icon: 'mdi:curtains',
iconSize: 32,
iconSizeUnit: 'px',
primaryColor: '#fff',
secondaryColor: 'rgba(255, 255, 255, 0.80)',
background: {
type: BackgroundType.color,
color: '#3F52DD',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
primaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
backgroundDisabled: {
type: BackgroundType.color,
color: '#CACACA',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
},
offState: {
showLabel: true,
label: 'Window left corner',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
showStatus: true,
status: 'Closed',
statusFont: {
family: 'Roboto',
size: 10,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
icon: 'mdi:curtains-closed',
iconSize: 32,
iconSizeUnit: 'px',
primaryColor: 'rgba(0, 0, 0, 0.87)',
secondaryColor: 'rgba(0, 0, 0, 0.54)',
background: {
type: BackgroundType.color,
color: '#FFF',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
primaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
backgroundDisabled: {
type: BackgroundType.color,
color: '#CACACA',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
}
};

111
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html

@ -0,0 +1,111 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="stateSettingsFormGroup">
<div *ngIf="layout !== StatusWidgetLayout.icon" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.status-widget.label' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="stateSettingsFormGroup.get('label').value">
</tb-font-settings>
</div>
</div>
<div *ngIf="layout !== StatusWidgetLayout.icon" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showStatus">
{{ 'widgets.status-widget.status' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="status" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="statusFont"
[previewText]="stateSettingsFormGroup.get('status').value">
</tb-font-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">
{{ 'widgets.status-widget.icon' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between" [class]="{'column-xs': layout === StatusWidgetLayout.icon, 'column-lt-md': layout !== StatusWidgetLayout.icon}">
<div>{{ 'widgets.status-widget.color-palette' | translate }}</div>
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center"
[fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'"
style="gap: 12px;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.primary-color-hint' | translate}}" translate>widgets.status-widget.primary</div>
<tb-color-input asBoxInput
formControlName="primaryColor">
</tb-color-input>
</div>
<mat-divider *ngIf="layout !== StatusWidgetLayout.icon" vertical fxHide.lt-md></mat-divider>
<div *ngIf="layout !== StatusWidgetLayout.icon" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.secondary-color-hint' | translate}}" translate>widgets.status-widget.secondary</div>
<tb-color-input asBoxInput
formControlName="secondaryColor">
</tb-color-input>
</div>
<mat-divider vertical fxHide.lt-sm [fxHide.lt-md]="layout !== StatusWidgetLayout.icon"></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.status-widget.background</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</div>
<div class="tb-form-row space-between" [class]="{'column-xs': layout === StatusWidgetLayout.icon, 'column-lt-md': layout !== StatusWidgetLayout.icon}">
<div>{{ 'widgets.status-widget.disabled-color-palette' | translate }}</div>
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center"
[fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'"
style="gap: 12px;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.primary-color-hint' | translate}}" translate>widgets.status-widget.primary</div>
<tb-color-input asBoxInput
formControlName="primaryColorDisabled">
</tb-color-input>
</div>
<mat-divider *ngIf="layout !== StatusWidgetLayout.icon" vertical fxHide.lt-md></mat-divider>
<div *ngIf="layout !== StatusWidgetLayout.icon" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.secondary-color-hint' | translate}}" translate>widgets.status-widget.secondary</div>
<tb-color-input asBoxInput
formControlName="secondaryColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical fxHide.lt-sm [fxHide.lt-md]="layout !== StatusWidgetLayout.icon"></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.status-widget.background</div>
<tb-background-settings formControlName="backgroundDisabled">
</tb-background-settings>
</div>
</div>
</div>
</ng-container>

158
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts

@ -0,0 +1,158 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { merge } from 'rxjs';
import {
StatusWidgetLayout,
StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
@Component({
selector: 'tb-status-widget-state-settings',
templateUrl: './status-widget-state-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StatusWidgetStateSettingsComponent),
multi: true
}
]
})
export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, ControlValueAccessor {
StatusWidgetLayout = StatusWidgetLayout;
@Input()
disabled: boolean;
@Input()
layout: StatusWidgetLayout;
private modelValue: StatusWidgetStateSettings;
private propagateChange = null;
public stateSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit(): void {
this.stateSettingsFormGroup = this.fb.group({
showLabel: [null, []],
label: [null, []],
labelFont: [null, []],
showStatus: [null, []],
status: [null, []],
statusFont: [null, []],
icon: [null, []],
iconSize: [null, []],
iconSizeUnit: [null, []],
primaryColor: [null, []],
secondaryColor: [null, []],
background: [null, []],
primaryColorDisabled: [null, []],
secondaryColorDisabled: [null, []],
backgroundDisabled: [null, []]
});
this.stateSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
merge(this.stateSettingsFormGroup.get('showLabel').valueChanges,
this.stateSettingsFormGroup.get('showStatus').valueChanges)
.subscribe(() => {
this.updateValidators();
});
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['layout'].includes(propName)) {
this.updateValidators();
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.stateSettingsFormGroup.disable({emitEvent: false});
} else {
this.stateSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: StatusWidgetStateSettings): void {
this.modelValue = value;
this.stateSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateValidators() {
if (this.layout === StatusWidgetLayout.icon) {
this.stateSettingsFormGroup.get('showLabel').disable({emitEvent: false});
this.stateSettingsFormGroup.get('label').disable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false});
this.stateSettingsFormGroup.get('showStatus').disable({emitEvent: false});
this.stateSettingsFormGroup.get('status').disable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColor').disable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColorDisabled').disable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('showLabel').enable({emitEvent: false});
this.stateSettingsFormGroup.get('showStatus').enable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColor').enable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColorDisabled').enable({emitEvent: false});
const showLabel: boolean = this.stateSettingsFormGroup.get('showLabel').value;
const showStatus: boolean = this.stateSettingsFormGroup.get('showStatus').value;
if (showLabel) {
this.stateSettingsFormGroup.get('label').enable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').enable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('label').disable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false});
}
if (showStatus) {
this.stateSettingsFormGroup.get('status').enable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').enable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('status').disable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false});
}
}
}
private updateModel() {
this.modelValue = this.stateSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
}

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

@ -145,6 +145,9 @@ import {
import {
TimeSeriesChartGridSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component';
import {
StatusWidgetStateSettingsComponent
} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component';
@NgModule({
declarations: [
@ -198,6 +201,7 @@ import {
TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],
@ -257,6 +261,7 @@ import {
TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html

@ -0,0 +1,82 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="statusWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.status-widget.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of statusWidgetLayouts"
[value]="layout"
[image]="statusWidgetLayoutImageMap.get(layout)">
{{ statusWidgetLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-toggle-select [(ngModel)]="cardStyleMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="on">{{ 'widgets.status-widget.on' | translate }}</tb-toggle-option>
<tb-toggle-option value="off">{{ 'widgets.status-widget.off' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'on'"
[layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="onState">
</tb-status-widget-state-settings>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'off'"
[layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="offState">
</tb-status-widget-state-settings>
</div>
</ng-container>

79
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts

@ -0,0 +1,79 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES 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, Injector } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
statusWidgetDefaultSettings,
statusWidgetLayoutImages,
statusWidgetLayouts,
statusWidgetLayoutTranslations
} from '@home/components/widget/lib/indicator/status-widget.models';
import { ValueType } from '@shared/models/constants';
@Component({
selector: 'tb-status-widget-settings',
templateUrl: './status-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss'],
})
export class StatusWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
statusWidgetLayouts = statusWidgetLayouts;
statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations;
statusWidgetLayoutImageMap = statusWidgetLayoutImages;
valueType = ValueType;
statusWidgetSettingsForm: UntypedFormGroup;
cardStyleMode = 'on';
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.statusWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...statusWidgetDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.statusWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
onState: [settings.onState, []],
offState: [settings.offState, []]
});
}
}

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

@ -342,6 +342,9 @@ import {
import {
TimeSeriesChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component';
import {
StatusWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/status-widget-settings.component';
@NgModule({
declarations: [
@ -464,7 +467,8 @@ import {
TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
],
imports: [
CommonModule,
@ -592,7 +596,8 @@ import {
TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -685,5 +690,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-slider-widget-settings': SliderWidgetSettingsComponent,
'tb-toggle-button-widget-settings': ToggleButtonWidgetSettingsComponent,
'tb-time-series-chart-key-settings': TimeSeriesChartKeySettingsComponent,
'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent
'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent,
'tb-status-widget-settings': StatusWidgetSettingsComponent
};

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

@ -93,6 +93,7 @@ import { DeviceInfoTableComponent } from '@home/components/widget/lib/gateway/co
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component';
import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/connectors-configuration/broker-security.component';
import { EllipsisChipListDirective } from '@home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
@NgModule({
declarations:
@ -152,7 +153,8 @@ import { EllipsisChipListDirective } from '@home/components/widget/lib/gateway/c
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
],
imports: [
CommonModule,
@ -216,7 +218,8 @@ import { EllipsisChipListDirective } from '@home/components/widget/lib/gateway/c
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

2
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -46,7 +46,7 @@
<input id="username-input" matInput type="email" autofocus formControlName="username" email required/>
<mat-icon matPrefix>email</mat-icon>
<mat-error *ngIf="loginFormGroup.get('username').invalid">
{{ 'user.invalid-email-format' | translate }}
{{ 'login.invalid-email-format' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="tb-appearance-transparent">

2
ui-ngx/src/app/shared/components/json-content.component.html

@ -18,7 +18,7 @@
<div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="hideToolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label>
<span fxFlex></span>
<button type="button"

101
ui-ngx/src/app/shared/components/json-content.component.ts

@ -18,17 +18,19 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
ViewChild, ViewEncapsulation
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, Validator } from '@angular/forms';
import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -38,6 +40,7 @@ import { guid } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer';
import { getAce } from '@shared/models/ace/ace.models';
import { beautifyJs } from '@shared/models/beautify.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-json-content',
@ -79,41 +82,30 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
@Input() editorStyle: {[klass: string]: any};
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
}
@Input() tbPlaceholder: string;
@Input()
set readonly(value: boolean) {
this.readonlyValue = coerceBooleanProperty(value);
}
@coerceBoolean()
hideToolbar = false;
private validateContentValue: boolean;
get validateContent(): boolean {
return this.validateContentValue;
}
@Input()
set validateContent(value: boolean) {
this.validateContentValue = coerceBooleanProperty(value);
}
@coerceBoolean()
readonly: boolean;
private validateOnChangeValue: boolean;
get validateOnChange(): boolean {
return this.validateOnChangeValue;
}
@Input()
set validateOnChange(value: boolean) {
this.validateOnChangeValue = coerceBooleanProperty(value);
}
@coerceBoolean()
validateContent: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@coerceBoolean()
validateOnChange: boolean;
@Input()
@coerceBoolean()
required: boolean;
@Output()
blur: EventEmitter<void> = new EventEmitter<void>();
fullscreen = false;
@ -124,6 +116,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
errorShowed = false;
private propagateChange = null;
private onTouched = () => {};
constructor(public elementRef: ElementRef,
protected store: Store<AppState>,
@ -163,11 +156,17 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
this.updateView();
}
});
if (this.validateContent) {
this.jsonEditor.on('blur', () => {
this.jsonEditor.on('blur', () => {
if (this.validateContent) {
this.contentValid = this.doValidate(true);
this.cd.markForCheck();
});
}
this.onTouched();
this.blur.next();
});
if (this.tbPlaceholder && this.tbPlaceholder.length) {
this.createPlaceholder();
}
this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize();
@ -177,6 +176,39 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
);
}
private createPlaceholder() {
this.jsonEditor.on('input', this.updateEditorPlaceholder.bind(this));
setTimeout(this.updateEditorPlaceholder.bind(this), 100);
}
private updateEditorPlaceholder() {
const shouldShow = !this.jsonEditor.session.getValue().length;
let node: HTMLElement = (this.jsonEditor.renderer as any).emptyMessageNode;
if (!shouldShow && node) {
this.jsonEditor.renderer.getMouseEventTarget().removeChild(node);
(this.jsonEditor.renderer as any).emptyMessageNode = null;
} else if (shouldShow && !node) {
const placeholderElement = $('<textarea></textarea>');
placeholderElement.text(this.tbPlaceholder);
placeholderElement.addClass('ace_invisible ace_emptyMessage');
placeholderElement.css({
padding: '0 9px',
width: '100%',
border: 'none',
textWrap: 'nowrap',
whiteSpace: 'pre',
overflow: 'hidden',
resize: 'none',
fontSize: '15px'
});
const rows = this.tbPlaceholder.split('\n').length;
placeholderElement.attr('rows', rows);
node = placeholderElement[0];
(this.jsonEditor.renderer as any).emptyMessageNode = node;
this.jsonEditor.renderer.getMouseEventTarget().appendChild(node);
}
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
@ -219,6 +251,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {

8263
ui-ngx/src/assets/locale/locale.constant-ar_AR.json

File diff suppressed because it is too large

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

@ -3483,6 +3483,7 @@
"new-password-again": "Confirm new password",
"password-link-sent-message": "Reset link has been sent",
"email": "Email",
"invalid-email-format": "Invalid email format.",
"login-with": "Login with {{name}}",
"or": "or",
"error": "Login error",
@ -5630,6 +5631,25 @@
"signal-strength-card-style": "Signal strength card style",
"no-signal-rssi-value": "\"No signal\" rssi value"
},
"status-widget": {
"behavior": "Behavior",
"layout": "Layout",
"layout-default": "Default",
"layout-center": "Center",
"layout-icon": "Icon",
"on": "On",
"off": "Off",
"label": "Label",
"status": "Status",
"icon": "Icon",
"color-palette": "Color palette",
"disabled-color-palette": "Disabled color palette",
"primary": "Primary",
"primary-color-hint": "Color of icon and label",
"secondary": "Secondary",
"secondary-color-hint": "Color of status",
"background": "Background"
},
"chart": {
"common-settings": "Common settings",
"enable-stacking-mode": "Enable stacking mode",
@ -7431,6 +7451,7 @@
"language": {
"language": "Language",
"locales": {
"ar_AR": "اَلْعَرَبِيَّةُ",
"ca_ES": "Catalan",
"cs_CZ": "Česky",
"da_DK": "Dansk",

20
ui-ngx/src/assets/widget/status-widget/center-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

20
ui-ngx/src/assets/widget/status-widget/default-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

18
ui-ngx/src/assets/widget/status-widget/icon-layout.svg

@ -0,0 +1,18 @@
<svg width="141" height="140" viewBox="0 0 141 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_5952_143941)">
<rect x="8.66675" y="4" width="124" height="124" rx="4" fill="#3F52DD"/>
<path d="M83.0387 55.8776H58.2949V53.6282H83.0387V55.8776ZM59.4196 77.2472H63.9185C63.9185 73.8731 61.6691 71.6236 61.6691 71.6236C68.4174 67.1248 69.5421 57.0023 69.5421 57.0023H59.4196V77.2472ZM81.914 57.0023H71.7915C71.7915 57.0023 72.9162 67.1248 79.6645 71.6236C79.6645 71.6236 77.4151 73.8731 77.4151 77.2472H81.914V57.0023Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_5952_143941" x="0.666748" y="0" width="140" height="140" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5952_143941"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5952_143941" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

7
ui-ngx/yarn.lock

@ -2572,13 +2572,6 @@
dependencies:
tslib "^2.3.0"
"@ngx-translate/http-loader@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz#905f38d8d13342621516635bf480ff9a4f73e9fc"
integrity sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==
dependencies:
tslib "^2.3.0"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"

Loading…
Cancel
Save