Browse Source

Merge branch 'master' into release-3.5

release-3.5
Igor Kulikov 3 years ago
parent
commit
89be3d9606
  1. 23
      application/src/main/data/upgrade/3.5.0/schema_update.sql
  2. 4
      application/src/main/java/org/thingsboard/server/controller/QueueController.java
  3. 2
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  4. 16
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 13
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  9. 36
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java
  10. 3
      application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
  11. 3
      application/src/main/resources/thingsboard.yml
  12. 96
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  13. 85
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  14. 58
      application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtilsTest.java
  15. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/client/AbstractMqttClientConnectionTest.java
  16. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java
  17. 5
      common/data/pom.xml
  18. 272
      common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java
  19. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  20. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  21. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
  22. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/ReturnCodeResolver.java
  23. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  24. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  25. 9
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java
  26. 8
      dao/src/main/resources/sql/schema-views-and-functions.sql
  27. 30
      dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java
  28. 2
      msa/js-executor/package.json
  29. 2
      msa/tb/docker-cassandra/Dockerfile
  30. 2
      msa/tb/pom.xml
  31. 2
      msa/web-ui/package.json
  32. 2
      pom.xml
  33. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  34. 17
      security.md
  35. 32
      ui-ngx/package.json
  36. 20
      ui-ngx/patches/@angular+core+15.2.9.patch
  37. 2
      ui-ngx/src/app/core/http/widget.service.ts
  38. 12
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  39. 18
      ui-ngx/src/app/core/services/dialog.service.ts
  40. 4
      ui-ngx/src/app/core/services/utils.service.ts
  41. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  42. 5
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  43. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  44. 2
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html
  45. 13
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  46. 7
      ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts
  47. 64
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  48. 8
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.html
  49. 13
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts
  50. 18
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts
  51. 1
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  52. 26
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  53. 15
      ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts
  54. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  55. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts
  56. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
  57. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
  58. 5
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  59. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.html
  60. 26
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  61. 2
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html
  62. 30
      ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw
  63. 3
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  64. 2
      ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html
  65. 30
      ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html
  66. 37
      ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss
  67. 49
      ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts
  68. 2
      ui-ngx/src/app/shared/components/file-input.component.scss
  69. 2
      ui-ngx/src/app/shared/components/image-input.component.scss
  70. 2
      ui-ngx/src/app/shared/components/multiple-image-input.component.scss
  71. 4
      ui-ngx/src/app/shared/components/notification/notification.component.html
  72. 5
      ui-ngx/src/app/shared/components/notification/notification.component.ts
  73. 2
      ui-ngx/src/app/shared/components/snack-bar-component.scss
  74. 2
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  75. 93
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  76. 1
      ui-ngx/src/app/shared/components/value-input.component.ts
  77. 2
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.html
  78. 24
      ui-ngx/src/app/shared/models/ace/tbel/worker-tbel.js
  79. 1
      ui-ngx/src/app/shared/models/constants.ts
  80. 6
      ui-ngx/src/app/shared/models/time/time.models.ts
  81. 3
      ui-ngx/src/app/shared/shared.module.ts
  82. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  83. 1143
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  84. 4
      ui-ngx/src/styles.scss
  85. 188
      ui-ngx/yarn.lock

23
application/src/main/data/upgrade/3.5.0/schema_update.sql

@ -0,0 +1,23 @@
--
-- Copyright © 2016-2023 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- FIX DASHBOARD TEMPLATES AFTER ANGULAR MIGRATION TO VER.15
UPDATE dashboard SET configuration = REPLACE(configuration, 'mat-button mat-icon-button', 'mat-icon-button')
WHERE configuration like '%mat-button mat-icon-button%';
UPDATE widget_type SET descriptor = REPLACE(descriptor, 'mat-button mat-icon-button', 'mat-icon-button')
WHERE descriptor like '%mat-button mat-icon-button%';

4
application/src/main/java/org/thingsboard/server/controller/QueueController.java

@ -83,7 +83,7 @@ public class QueueController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("serviceType", serviceType);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
ServiceType type = ServiceType.valueOf(serviceType);
ServiceType type = ServiceType.of(serviceType);
switch (type) {
case TB_RULE_ENGINE:
return queueService.findQueuesByTenantId(getTenantId(), pageLink);
@ -136,7 +136,7 @@ public class QueueController extends BaseController {
checkEntity(queue.getId(), queue, Resource.QUEUE);
ServiceType type = ServiceType.valueOf(serviceType);
ServiceType type = ServiceType.of(serviceType);
switch (type) {
case TB_RULE_ENGINE:
queue.setTenantId(getTenantId());

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

@ -406,7 +406,7 @@ public class UserController extends BaseController {
public void setUserCredentialsEnabled(
@ApiParam(value = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
@ApiParam(value = "Disable (\"true\") or enable (\"false\") the credentials.", defaultValue = "true")
@ApiParam(value = "Enable (\"true\") or disable (\"false\") the credentials.", defaultValue = "true")
@RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId));

16
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -249,22 +249,24 @@ public class ThingsboardInstallService {
case "3.4.4":
log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) {
systemDataLoaderService.createDefaultNotificationConfigs();
} else {
log.info("Skipping default notification configs creation");
}
installScripts.loadSystemLwm2mResources();
case "3.5.0":
log.info("Upgrading ThingsBoard from version 3.5.0 to 3.5.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.5.0");
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
//TODO update CacheCleanupService on the next version upgrade
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
}
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
installScripts.loadSystemLwm2mResources();
}
log.info("Upgrade finished successfully!");

13
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -713,6 +713,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.error("Failed updating schema!!!", e);
}
break;
case "3.5.0":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
if (isOldSchema(conn, 3005000)) {
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.0", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005001;");
}
log.info("Schema updated.");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}

3
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -84,7 +84,8 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
break;
case "3.4.4":
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
clearCacheByName("deviceProfiles");
clearAll();
break;
default:
//Do nothing, since cache cleanup is optional.
}

4
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java

@ -20,6 +20,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.DeviceActivityNotificationInfo;
@ -41,6 +42,9 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) {
if (trigger.getMsg().getOriginator().getEntityType() != EntityType.DEVICE) {
return false;
}
DeviceEvent event = trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE;
if (!triggerConfig.getNotifyOn().contains(event)) {
return false;

4
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -16,8 +16,6 @@
package org.thingsboard.server.service.resource;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.DDFFileParser;
import org.eclipse.leshan.core.model.DefaultDDFFileValidator;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
@ -53,11 +51,9 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource
public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService {
private final ResourceService resourceService;
private final DDFFileParser ddfFileParser;
public DefaultTbResourceService(ResourceService resourceService) {
this.resourceService = resourceService;
this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator());
}
@Override

36
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java

@ -15,16 +15,30 @@
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.util.SerializationUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
@Slf4j
public class CookieUtils {
private static final ObjectMapper OBJECT_MAPPER;
static {
ClassLoader loader = CookieUtils.class.getClassLoader();
OBJECT_MAPPER = new ObjectMapper();
OBJECT_MAPPER.registerModules(SecurityJackson2Modules.getModules(loader));
}
public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
@ -50,7 +64,7 @@ public class CookieUtils {
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie: cookies) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
cookie.setValue("");
cookie.setPath("/");
@ -62,12 +76,22 @@ public class CookieUtils {
}
public static String serialize(Object object) {
return Base64.getUrlEncoder()
.encodeToString(SerializationUtils.serialize(object));
try {
return Base64.getUrlEncoder()
.encodeToString(OBJECT_MAPPER.writeValueAsBytes(object));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("The given Json object value: "
+ object + " cannot be transformed to a String", e);
}
}
public static <T> T deserialize(Cookie cookie, Class<T> cls) {
return cls.cast(SerializationUtils.deserialize(
Base64.getUrlDecoder().decode(cookie.getValue())));
byte[] decodedBytes = Base64.getUrlDecoder().decode(cookie.getValue());
try {
return OBJECT_MAPPER.readValue(decodedBytes, cls);
} catch (IOException e) {
throw new IllegalArgumentException("The given string value: "
+ Arrays.toString(decodedBytes) + " cannot be transformed to Json object", e);
}
}
}

3
application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.lwm2m.LwM2mInstance;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve;
import org.thingsboard.server.common.data.util.TbDDFFileParser;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.io.ByteArrayInputStream;
@ -41,7 +42,7 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA
@Slf4j
public class LwM2mObjectModelUtils {
private static final DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator());
private static final TbDDFFileParser ddfFileParser = new TbDDFFileParser();
public static void toLwm2mResource (TbResource resource) throws ThingsboardException {
try {

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

@ -159,7 +159,7 @@ ui:
# Help parameters
help:
# Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.5}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.5.1}"
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records
@ -607,6 +607,7 @@ spring:
username: "${SPRING_DATASOURCE_USERNAME:postgres}"
password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
hikari:
leakDetectionThreshold: "${SPRING_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD:0}"
maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}"
registerMbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" # true - enable MBean to diagnose pools state via JMX

96
application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class BaseQueueControllerTest extends AbstractControllerTest {
@Test
public void testQueueWithServiceTypeRE() throws Exception {
loginSysAdmin();
// create queue
Queue queue = new Queue();
queue.setName("qwerty");
queue.setTopic("tb_rule_engine.qwerty");
queue.setPollInterval(25);
queue.setPartitions(10);
queue.setTenantId(TenantId.SYS_TENANT_ID);
queue.setConsumerPerPartition(false);
queue.setPackProcessingTimeout(2000);
SubmitStrategy submitStrategy = new SubmitStrategy();
submitStrategy.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR);
queue.setSubmitStrategy(submitStrategy);
ProcessingStrategy processingStrategy = new ProcessingStrategy();
processingStrategy.setType(ProcessingStrategyType.RETRY_ALL);
processingStrategy.setRetries(3);
processingStrategy.setFailurePercentage(0.7);
processingStrategy.setPauseBetweenRetries(3);
processingStrategy.setMaxPauseBetweenRetries(5);
queue.setProcessingStrategy(processingStrategy);
// create queue
Queue queue2 = new Queue();
queue2.setName("qwerty2");
queue2.setTopic("tb_rule_engine.qwerty2");
queue2.setPollInterval(25);
queue2.setPartitions(10);
queue2.setTenantId(TenantId.SYS_TENANT_ID);
queue2.setConsumerPerPartition(false);
queue2.setPackProcessingTimeout(2000);
submitStrategy.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR);
queue2.setSubmitStrategy(submitStrategy);
processingStrategy.setType(ProcessingStrategyType.RETRY_ALL);
processingStrategy.setRetries(3);
processingStrategy.setFailurePercentage(0.7);
processingStrategy.setPauseBetweenRetries(3);
processingStrategy.setMaxPauseBetweenRetries(5);
queue2.setProcessingStrategy(processingStrategy);
Queue savedQueue = doPost("/api/queues?serviceType=" + "TB-RULE-ENGINE", queue, Queue.class);
Queue savedQueue2 = doPost("/api/queues?serviceType=" + "TB_RULE_ENGINE", queue2, Queue.class);
PageLink pageLink = new PageLink(10);
PageData<Queue> pageData;
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB-RULE-ENGINE&", new TypeReference<>() {
}, pageLink);
Assert.assertFalse(pageData.getData().isEmpty());
doDelete("/api/queues/" + savedQueue.getUuidId())
.andExpect(status().isOk());
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() {
}, pageLink);
Assert.assertFalse(pageData.getData().isEmpty());
doDelete("/api/queues/" + savedQueue2.getUuidId())
.andExpect(status().isOk());
}
}

85
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

@ -45,6 +45,8 @@ import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@ -75,6 +77,32 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
"</Object>\n" +
"</LWM2M>";
private static final String LWM2M_TEST_MODEL_WITH_XXE = "<!DOCTYPE replace [<!ENTITY ObjectVersion SYSTEM \"file:///etc/hostname\"> ]>" +
"<LWM2M xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://www.openmobilealliance.org/tech/profiles/LWM2M-v1_1.xsd\">\n" +
"<Object ObjectType=\"MODefinition\">\n" +
"<Name>My first resource</Name>\n" +
"<Description1></Description1>\n" +
"<ObjectID>0</ObjectID>\n" +
"<ObjectURN></ObjectURN>\n" +
"<ObjectVersion>&ObjectVersion;</ObjectVersion>\n" +
"<MultipleInstances>Multiple</MultipleInstances>\n" +
"<Mandatory>Mandatory</Mandatory>\n" +
"<Resources>\n" +
"<Item ID=\"0\">\n" +
"<Name>LWM2M</Name>\n" +
"<Operations>RW</Operations>\n" +
"<MultipleInstances>Single</MultipleInstances>\n" +
"<Mandatory>Mandatory</Mandatory>\n" +
"<Type>String</Type>\n" +
"<RangeEnumeration>0..255</RangeEnumeration>\n" +
"<Units></Units>\n" +
"<Description></Description>\n" +
"</Item>\n" +
"</Resources>\n" +
"<Description2></Description2>\n" +
"</Object>\n" +
"</LWM2M>";
private static final String DEFAULT_FILE_NAME = "test.jks";
private IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
@ -126,11 +154,11 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
loginTenantAdmin();
Assert.assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
createResource("test", DEFAULT_FILE_NAME);
Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
try {
assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME))
@ -145,19 +173,19 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
@Test
public void sumDataSizeByTenantId() throws Exception {
Assert.assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
createResource("test", DEFAULT_FILE_NAME);
Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
int maxSumDataSize = 8;
for (int i = 2; i <= maxSumDataSize; i++) {
createResource("test" + i, i + DEFAULT_FILE_NAME);
Assert.assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId));
assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId));
}
Assert.assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId));
assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId));
}
private TbResource createResource(String title, String filename) throws Exception {
@ -184,16 +212,16 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNotNull(savedResource);
Assert.assertNotNull(savedResource.getId());
Assert.assertTrue(savedResource.getCreatedTime() > 0);
Assert.assertEquals(resource.getTenantId(), savedResource.getTenantId());
Assert.assertEquals(resource.getTitle(), savedResource.getTitle());
Assert.assertEquals(resource.getResourceKey(), savedResource.getResourceKey());
Assert.assertEquals(resource.getData(), savedResource.getData());
assertEquals(resource.getTenantId(), savedResource.getTenantId());
assertEquals(resource.getTitle(), savedResource.getTitle());
assertEquals(resource.getResourceKey(), savedResource.getResourceKey());
assertEquals(resource.getData(), savedResource.getData());
savedResource.setTitle("My new resource");
resourceService.save(savedResource);
TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId());
Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle());
assertEquals(foundResource.getTitle(), savedResource.getTitle());
resourceService.delete(savedResource, null);
}
@ -211,10 +239,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNotNull(savedResource);
Assert.assertNotNull(savedResource.getId());
Assert.assertTrue(savedResource.getCreatedTime() > 0);
Assert.assertEquals(resource.getTenantId(), savedResource.getTenantId());
Assert.assertEquals("My first resource id=0 v1.0", savedResource.getTitle());
Assert.assertEquals("0_1.0", savedResource.getResourceKey());
Assert.assertEquals(resource.getData(), savedResource.getData());
assertEquals(resource.getTenantId(), savedResource.getTenantId());
assertEquals("My first resource id=0 v1.0", savedResource.getTitle());
assertEquals("0_1.0", savedResource.getResourceKey());
assertEquals(resource.getData(), savedResource.getData());
resourceService.delete(savedResource, null);
}
@ -228,7 +256,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setData("Test Data");
TbResource savedResource = resourceService.save(resource);
Assert.assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId());
assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId());
resourceService.delete(savedResource, null);
}
@ -285,6 +313,21 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
});
}
@Test
public void testSaveLwm2mTbResourceWithXXE() {
TbResource resource = new TbResource();
resource.setTenantId(tenantId);
resource.setResourceType(ResourceType.LWM2M_MODEL);
resource.setFileName("xxe_test_model.xml");
resource.setData(Base64.getEncoder().encodeToString(LWM2M_TEST_MODEL_WITH_XXE.getBytes()));
DataValidationException thrown = assertThrows(DataValidationException.class, () -> {
resourceService.save(resource);
});
assertEquals("Failed to parse file xxe_test_model.xml", thrown.getMessage());
}
@Test
public void testFindResourceById() throws Exception {
TbResource resource = new TbResource();
@ -296,7 +339,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId());
Assert.assertNotNull(foundResource);
Assert.assertEquals(savedResource, foundResource);
assertEquals(savedResource, foundResource);
resourceService.delete(savedResource, null);
}
@ -312,7 +355,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
TbResource foundResource = resourceService.getResource(tenantId, savedResource.getResourceType(), savedResource.getResourceKey());
Assert.assertNotNull(foundResource);
Assert.assertEquals(savedResource, foundResource);
assertEquals(savedResource, foundResource);
resourceService.delete(savedResource, null);
}
@ -366,7 +409,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
Assert.assertEquals(resources, loadedResources);
assertEquals(resources, loadedResources);
resourceService.deleteResourcesByTenantId(tenantId);
@ -427,14 +470,14 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
Assert.assertEquals(resources, loadedResources);
assertEquals(resources, loadedResources);
resourceService.deleteResourcesByTenantId(tenantId);
pageLink = new PageLink(100);
pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(pageData.getData().size(), 100);
assertEquals(pageData.getData().size(), 100);
resourceService.deleteResourcesByTenantId(TenantId.SYS_TENANT_ID);

58
application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtilsTest.java

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME;
public class CookieUtilsTest {
@Test
public void serializeDeserializeOAuth2AuthorizationRequestTest() {
HttpCookieOAuth2AuthorizationRequestRepository cookieRequestRepo = new HttpCookieOAuth2AuthorizationRequestRepository();
HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
Map<String, Object> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("param1", "value1");
additionalParameters.put("param2", "value2");
var request = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("testUri").clientId("testId")
.scope("read", "write")
.additionalParameters(additionalParameters).build();
Cookie cookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(request));
Mockito.when(servletRequest.getCookies()).thenReturn(new Cookie[]{cookie});
OAuth2AuthorizationRequest deserializedRequest = cookieRequestRepo.loadAuthorizationRequest(servletRequest);
assertNotNull(deserializedRequest);
assertEquals(request.getGrantType(), deserializedRequest.getGrantType());
assertEquals(request.getAuthorizationUri(), deserializedRequest.getAuthorizationUri());
assertEquals(request.getClientId(), deserializedRequest.getClientId());
}
}

2
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/client/AbstractMqttClientConnectionTest.java

@ -34,7 +34,7 @@ public abstract class AbstractMqttClientConnectionTest extends AbstractMqttInteg
try {
client.connectAndWait("wrongAccessToken");
} catch (MqttException e) {
Assert.assertEquals(MqttException.REASON_CODE_FAILED_AUTHENTICATION, e.getReasonCode());
Assert.assertEquals(MqttException.REASON_CODE_NOT_AUTHORIZED, e.getReasonCode());
}
}

2
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java

@ -124,7 +124,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest {
mqttTestClient.connectAndWait(USER_NAME3, "WRONG PASSWORD");
Assert.fail(); // This should not happens, because we have a wrong password
} catch (MqttException e) {
Assert.assertEquals(4, e.getReasonCode()); // 4 - Reason code for bad username or password in MQTT v3
Assert.assertEquals(5, e.getReasonCode()); // 4 - Reason code not authorized in MQTT v3
}
Assertions.assertThrows(MqttException.class, () -> {
testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient);

5
common/data/pom.xml

@ -116,6 +116,11 @@
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-core</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

272
common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java

@ -0,0 +1,272 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.util;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m;
import org.eclipse.leshan.core.model.DDFFileValidator;
import org.eclipse.leshan.core.model.DefaultDDFFileValidator;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.util.StringUtils;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class TbDDFFileParser {
private static final DDFFileValidator ddfFileValidator = new DefaultDDFFileValidator();
public List<ObjectModel> parse(InputStream inputStream, String streamName)
throws InvalidDDFFileException, IOException {
streamName = streamName == null ? "" : streamName;
log.debug("Parsing DDF file {}", streamName);
try {
// Parse XML file
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(inputStream);
// Get DDF file validator
LwM2m.LwM2mVersion lwm2mVersion = null;
ddfFileValidator.validate(document);
// Build list of ObjectModel
ArrayList<ObjectModel> objects = new ArrayList<>();
NodeList nodeList = document.getDocumentElement().getElementsByTagName("Object");
for (int i = 0; i < nodeList.getLength(); i++) {
objects.add(parseObject(nodeList.item(i), streamName, lwm2mVersion, true));
}
return objects;
} catch (InvalidDDFFileException | SAXException e) {
throw new InvalidDDFFileException(e, "Invalid DDF file %s", streamName);
}
catch (ParserConfigurationException e) {
throw new IllegalStateException("Unable to create Document Builder", e);
}
}
private ObjectModel parseObject(Node object, String streamName, LwM2m.LwM2mVersion schemaVersion, boolean validate)
throws InvalidDDFFileException {
Node objectType = object.getAttributes().getNamedItem("ObjectType");
if (validate && (objectType == null || !"MODefinition".equals(objectType.getTextContent()))) {
throw new InvalidDDFFileException(
"Object element in %s MUST have a ObjectType attribute equals to 'MODefinition'.", streamName);
}
Integer id = null;
String name = null;
String description = null;
String version = ObjectModel.DEFAULT_VERSION;
Boolean multiple = null;
Boolean mandatory = null;
Map<Integer, ResourceModel> resources = new HashMap<>();
String urn = null;
String description2 = null;
String lwm2mVersion = ObjectModel.DEFAULT_VERSION;
for (int i = 0; i < object.getChildNodes().getLength(); i++) {
Node field = object.getChildNodes().item(i);
if (field.getNodeType() != Node.ELEMENT_NODE)
continue;
switch (field.getNodeName()) {
case "ObjectID":
id = Integer.valueOf(field.getTextContent());
break;
case "Name":
name = field.getTextContent();
break;
case "Description1":
description = field.getTextContent();
break;
case "ObjectVersion":
if (!StringUtils.isEmpty(field.getTextContent())) {
version = field.getTextContent();
}
break;
case "MultipleInstances":
if ("Multiple".equals(field.getTextContent())) {
multiple = true;
} else if ("Single".equals(field.getTextContent())) {
multiple = false;
}
break;
case "Mandatory":
if ("Mandatory".equals(field.getTextContent())) {
mandatory = true;
} else if ("Optional".equals(field.getTextContent())) {
mandatory = false;
}
break;
case "Resources":
for (int j = 0; j < field.getChildNodes().getLength(); j++) {
Node item = field.getChildNodes().item(j);
if (item.getNodeType() != Node.ELEMENT_NODE)
continue;
if (item.getNodeName().equals("Item")) {
ResourceModel resource = parseResource(item, streamName);
if (validate && resources.containsKey(resource.id)) {
throw new InvalidDDFFileException(
"Object %s in %s contains at least 2 resources with same id %s.",
id != null ? id : "", streamName, resource.id);
} else {
resources.put(resource.id, resource);
}
}
}
break;
case "ObjectURN":
urn = field.getTextContent();
break;
case "LWM2MVersion":
if (!StringUtils.isEmpty(field.getTextContent())) {
lwm2mVersion = field.getTextContent();
if (schemaVersion != null && !schemaVersion.toString().equals(lwm2mVersion)) {
throw new InvalidDDFFileException(
"LWM2MVersion is not consistent with xml shema(xsi:noNamespaceSchemaLocation) in %s : %s expected but was %s.",
streamName, schemaVersion, lwm2mVersion);
}
}
break;
case "Description2":
description2 = field.getTextContent();
break;
default:
break;
}
}
return new ObjectModel(id, name, description, version, multiple, mandatory, resources.values(), urn,
lwm2mVersion, description2);
}
private ResourceModel parseResource(Node item, String streamName) throws DOMException, InvalidDDFFileException {
Integer id = Integer.valueOf(item.getAttributes().getNamedItem("ID").getTextContent());
String name = null;
ResourceModel.Operations operations = null;
Boolean multiple = false;
Boolean mandatory = false;
ResourceModel.Type type = null;
String rangeEnumeration = null;
String units = null;
String description = null;
for (int i = 0; i < item.getChildNodes().getLength(); i++) {
Node field = item.getChildNodes().item(i);
if (field.getNodeType() != Node.ELEMENT_NODE)
continue;
switch (field.getNodeName()) {
case "Name":
name = field.getTextContent();
break;
case "Operations":
String strOp = field.getTextContent();
if (strOp != null && !strOp.isEmpty()) {
operations = ResourceModel.Operations.valueOf(strOp);
} else {
operations = ResourceModel.Operations.NONE;
}
break;
case "MultipleInstances":
if ("Multiple".equals(field.getTextContent())) {
multiple = true;
} else if ("Single".equals(field.getTextContent())) {
multiple = false;
}
break;
case "Mandatory":
if ("Mandatory".equals(field.getTextContent())) {
mandatory = true;
} else if ("Optional".equals(field.getTextContent())) {
mandatory = false;
}
break;
case "Type":
switch (field.getTextContent()) {
case "String":
type = ResourceModel.Type.STRING;
break;
case "Integer":
type = ResourceModel.Type.INTEGER;
break;
case "Float":
type = ResourceModel.Type.FLOAT;
break;
case "Boolean":
type = ResourceModel.Type.BOOLEAN;
break;
case "Opaque":
type = ResourceModel.Type.OPAQUE;
break;
case "Time":
type = ResourceModel.Type.TIME;
break;
case "Objlnk":
type = ResourceModel.Type.OBJLNK;
break;
case "Unsigned Integer":
type = ResourceModel.Type.UNSIGNED_INTEGER;
break;
case "Corelnk":
type = ResourceModel.Type.CORELINK;
break;
case "":
type = ResourceModel.Type.NONE;
break;
default:
break;
}
break;
case "RangeEnumeration":
rangeEnumeration = field.getTextContent();
break;
case "Units":
units = field.getTextContent();
break;
case "Description":
description = field.getTextContent();
break;
default:
break;
}
}
return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description);
}
}

4
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -326,7 +326,7 @@ public class HashPartitionService implements PartitionService {
final Map<QueueKey, List<ServiceInfo>> currentMap = new HashMap<>();
services.forEach(serviceInfo -> {
for (String serviceTypeStr : serviceInfo.getServiceTypesList()) {
ServiceType serviceType = ServiceType.valueOf(serviceTypeStr.toUpperCase());
ServiceType serviceType = ServiceType.of(serviceTypeStr);
if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) {
partitionTopicsMap.keySet().forEach(queueKey ->
currentMap.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(serviceInfo));
@ -389,7 +389,7 @@ public class HashPartitionService implements PartitionService {
private void addNode(Map<QueueKey, List<ServiceInfo>> queueServiceList, ServiceInfo instance) {
for (String serviceTypeStr : instance.getServiceTypesList()) {
ServiceType serviceType = ServiceType.valueOf(serviceTypeStr.toUpperCase());
ServiceType serviceType = ServiceType.of(serviceTypeStr);
if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) {
partitionTopicsMap.keySet().forEach(key -> {
if (key.getType().equals(ServiceType.TB_RULE_ENGINE)) {

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

@ -25,6 +25,7 @@ import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.util.TbDDFFileParser;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -142,9 +143,9 @@ public class LwM2mTransportServerHelper {
.build();
}
public ObjectModel parseFromXmlToObjectModel(byte[] xmlByte, String streamName, DefaultDDFFileValidator ddfValidator) {
public ObjectModel parseFromXmlToObjectModel(byte[] xmlByte, String streamName) {
try {
DDFFileParser ddfFileParser = new DDFFileParser(ddfValidator);
TbDDFFileParser ddfFileParser = new TbDDFFileParser();
return ddfFileParser.parse(new ByteArrayInputStream(xmlByte), streamName).get(0);
} catch (IOException | InvalidDDFFileException e) {
log.error("Could not parse the XML file [{}]", streamName, e);

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

@ -154,8 +154,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
Optional<TbResource> tbResource = context.getTransportResourceCache().get(this.tenantId, LWM2M_MODEL, key);
return tbResource.map(resource -> helper.parseFromXmlToObjectModel(
Base64.getDecoder().decode(resource.getData()),
key + ".xml",
new DefaultDDFFileValidator())).orElse(null);
key + ".xml")).orElse(null);
}
}
}

1
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/ReturnCodeResolver.java

@ -27,7 +27,6 @@ public class ReturnCodeResolver {
if (!MqttVersion.MQTT_5.equals(mqttVersion) && !ReturnCode.SUCCESS.equals(returnCode)) {
switch (returnCode) {
case BAD_USERNAME_OR_PASSWORD:
return MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD;
case NOT_AUTHORIZED_5:
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
case SERVER_UNAVAILABLE_5:

16
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -19,7 +19,6 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.dao.model.sql.AlarmEntity;
@ -316,7 +315,9 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId")
AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId);
@Procedure(procedureName = "create_or_update_active_alarm")
@Query(value = "SELECT create_or_update_active_alarm(:t_id, :c_id, :a_id, :a_created_ts, :a_o_id, :a_o_type, :a_type, :a_severity, " +
":a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " +
":a_propagate_to_tenant, :a_propagation_types, :a_creation_enabled)", nativeQuery = true)
String createOrUpdateActiveAlarm(@Param("t_id") UUID tenantId, @Param("c_id") UUID customerId,
@Param("a_id") UUID alarmId, @Param("a_created_ts") long createdTime,
@Param("a_o_id") UUID originatorId, @Param("a_o_type") int originatorType,
@ -326,21 +327,22 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes,
@Param("a_creation_enabled") boolean alarmCreationEnabled);
@Procedure(procedureName = "update_alarm")
@Query(value = "SELECT update_alarm(:t_id, :a_id, :a_severity, :a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " +
":a_propagate_to_tenant, :a_propagation_types)", nativeQuery = true)
String updateAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_severity") String severity,
@Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString,
@Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner,
@Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes);
@Procedure(procedureName = "acknowledge_alarm")
@Query(value = "SELECT acknowledge_alarm(:t_id, :a_id, :a_ts)", nativeQuery = true)
String acknowledgeAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts);
@Procedure(procedureName = "clear_alarm")
@Query(value = "SELECT clear_alarm(:t_id, :a_id, :a_ts, :a_details)", nativeQuery = true)
String clearAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts, @Param("a_details") String details);
@Procedure(procedureName = "assign_alarm")
@Query(value = "SELECT assign_alarm(:t_id, :a_id, :u_id, :a_ts)", nativeQuery = true)
String assignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("u_id") UUID userId, @Param("a_ts") long assignTime);
@Procedure(procedureName = "unassign_alarm")
@Query(value = "SELECT unassign_alarm(:t_id, :a_id, :a_ts)", nativeQuery = true)
String unassignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long unassignTime);
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -348,7 +348,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) {
return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, getDetailsAsString(details)));
return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, details != null ? getDetailsAsString(details) : null));
}
@Override

9
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java

@ -110,9 +110,12 @@ public class SqlPartitioningRepository {
String tablePartition = table + "_" + partitionTs;
String detachPsqlStmtStr = "ALTER TABLE " + table + " DETACH PARTITION " + tablePartition;
if (getCurrentServerVersion() >= PSQL_VERSION_14) {
detachPsqlStmtStr += " CONCURRENTLY";
}
// hotfix of ERROR: partition "integration_debug_event_1678323600000" already pending detach in partitioned table "public.integration_debug_event"
// https://github.com/thingsboard/thingsboard/issues/8271
// if (getCurrentServerVersion() >= PSQL_VERSION_14) {
// detachPsqlStmtStr += " CONCURRENTLY";
// }
String dropStmtStr = "DROP TABLE " + tablePartition;
try {

8
dao/src/main/resources/sql/schema-views-and-functions.sql

@ -23,7 +23,7 @@ SELECT d.*
, COALESCE(da.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN attribute_kv da ON da.entity_id = d.id and da.attribute_key = 'active';
LEFT JOIN attribute_kv da ON da.entity_type = 'DEVICE' AND da.entity_id = d.id AND da.attribute_type = 'SERVER_SCOPE' AND da.attribute_key = 'active';
DROP VIEW IF EXISTS device_info_active_ts_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_ts_view AS
@ -226,7 +226,11 @@ BEGIN
END IF;
IF NOT(existing.cleared) THEN
cleared = TRUE;
UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id;
IF a_details IS NULL THEN
UPDATE alarm a SET cleared = true, clear_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
ELSE
UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text;

30
dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java

@ -202,7 +202,34 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest {
@Test
public void testClearAlarmProcedure() {
UUID tenantId = UUID.randomUUID();
;
UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b");
UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b");
Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM");
long clearTs = System.currentTimeMillis();
var details = JacksonUtil.newObjectNode().put("test", 123);
AlarmApiCallResult result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs, details);
AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId());
assertNotNull(result);
assertTrue(result.isSuccessful());
assertTrue(result.isCleared());
assertNotNull(result.getAlarm());
assertEquals(afterSave, result.getAlarm());
assertEquals(clearTs, result.getAlarm().getClearTs());
assertTrue(result.getAlarm().isCleared());
assertEquals(details, result.getAlarm().getDetails());
result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode());
assertNotNull(result);
assertNotNull(result.getAlarm());
assertEquals(afterSave, result.getAlarm());
assertTrue(result.isSuccessful());
assertFalse(result.isCleared());
assertEquals(clearTs, result.getAlarm().getClearTs());
assertTrue(result.getAlarm().isCleared());
}
@Test
public void testClearAlarmWithoutDetailsProcedure() {
UUID tenantId = UUID.randomUUID();
UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b");
UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b");
Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM");
@ -216,6 +243,7 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest {
assertEquals(afterSave, result.getAlarm());
assertEquals(clearTs, result.getAlarm().getClearTs());
assertTrue(result.getAlarm().isCleared());
assertEquals(alarm.getDetails(), result.getAlarm().getDetails());
result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode());
assertNotNull(result);
assertNotNull(result.getAlarm());

2
msa/js-executor/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
"version": "3.5.0",
"version": "3.5.1",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",

2
msa/tb/docker-cassandra/Dockerfile

@ -47,7 +47,7 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends wget nmap procps gnupg2 \
&& echo "deb http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null \
&& wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | apt-key add - \
&& echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null \
&& echo "deb https://debian.cassandra.apache.org 40x main" | tee -a /etc/apt/sources.list.d/cassandra.sources.list > /dev/null \
&& wget -q https://downloads.apache.org/cassandra/KEYS -O- | apt-key add - \
&& apt-get update \
&& apt-get install -y --no-install-recommends cassandra cassandra-tools postgresql-${PG_MAJOR} \

2
msa/tb/pom.xml

@ -38,7 +38,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.5.0</pkg.upgradeVersion>
<pkg.upgradeVersion>3.5.1</pkg.upgradeVersion>
</properties>
<dependencies>

2
msa/web-ui/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
"version": "3.5.0",
"version": "3.5.1",
"description": "ThingsBoard Web UI Microservice",
"main": "server.ts",
"bin": "server.js",

2
pom.xml

@ -74,7 +74,7 @@
<gson.version>2.9.0</gson.version>
<freemarker.version>2.3.30</freemarker.version>
<mail.version>1.6.2</mail.version>
<curator.version>4.2.0</curator.version>
<curator.version>5.5.0</curator.version>
<zookeeper.version>3.8.1</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version>

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

17
security.md

@ -0,0 +1,17 @@
# Security Policy
## Reporting a Vulnerability
Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to Thingsboard privately,
to minimize attacks against current users of Thingsboard before they are fixed. Vulnerabilities will be investigated and release as soon as possible.
To report a vulnerability or a security-related issue, please email the private address security@thingsboard.io with the details of the vulnerability.
Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime.
Do not report non-security-impacting bugs through this channel. Use GitHub issues instead.
**Proposed Email Content**
Provide a descriptive subject line and in the body of the email include the following information:
- Basic identity information, such as your name and your affiliation or company.
- Detailed steps to reproduce the vulnerability (log errors, screenshots are all helpful to us).
- Description of the effects of the vulnerability on Thingsboard.

32
ui-ngx/package.json

@ -1,6 +1,6 @@
{
"name": "thingsboard",
"version": "3.5.0",
"version": "3.5.1",
"scripts": {
"ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",
@ -14,17 +14,17 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.5",
"@angular/cdk": "^15.2.5",
"@angular/common": "^15.2.5",
"@angular/compiler": "^15.2.5",
"@angular/core": "^15.2.5",
"@angular/animations": "^15.2.9",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.2.9",
"@angular/compiler": "^15.2.9",
"@angular/core": "^15.2.9",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.2.5",
"@angular/material": "^15.2.5",
"@angular/platform-browser": "^15.2.5",
"@angular/platform-browser-dynamic": "^15.2.5",
"@angular/router": "^15.2.5",
"@angular/forms": "^15.2.9",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.2.9",
"@angular/platform-browser-dynamic": "^15.2.9",
"@angular/router": "^15.2.9",
"@auth0/angular-jwt": "^5.1.2",
"@date-io/core": "1.3.7",
"@date-io/date-fns": "1.3.7",
@ -108,16 +108,16 @@
},
"devDependencies": {
"@angular-builders/custom-webpack": "~15.0.0",
"@angular-devkit/build-angular": "^15.2.4",
"@angular-devkit/build-angular": "^15.2.8",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^15.2.4",
"@angular/compiler-cli": "^15.2.5",
"@angular/language-service": "^15.2.5",
"@ngtools/webpack": "^15.1.6",
"@angular/cli": "^15.2.8",
"@angular/compiler-cli": "^15.2.9",
"@angular/language-service": "^15.2.9",
"@ngtools/webpack": "15.2.1",
"@types/ace-diff": "^2.1.1",
"@types/canvas-gauges": "^2.1.4",
"@types/flot": "^0.0.32",

20
ui-ngx/patches/@angular+core+15.2.9.patch

@ -0,0 +1,20 @@
diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs
index 3e93015..9efcb96 100755
--- a/node_modules/@angular/core/fesm2020/core.mjs
+++ b/node_modules/@angular/core/fesm2020/core.mjs
@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) {
if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) {
matches || (matches = []);
if (isComponentDef(def)) {
- if (ngDevMode) {
+ // if (ngDevMode) {
assertTNodeType(tNode, 2 /* TNodeType.Element */, `"${tNode.value}" tags cannot be used as component hosts. ` +
`Please use a different tag to activate the ${stringify(def.type)} component.`);
if (isComponentHost(tNode)) {
throwMultipleComponentError(tNode, matches.find(isComponentDef).type, def.type);
}
- }
+ // }
// Components are inserted at the front of the matches array so that their lifecycle
// hooks run before any directive lifecycle hooks. This appears to be for ViewEngine
// compatibility. This logic doesn't make sense with host directives, because it

2
ui-ngx/src/app/core/http/widget.service.ts

@ -280,7 +280,7 @@ export class WidgetService {
this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID);
}
private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) {
public deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) {
const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
this.widgetsInfoInMemoryCache.delete(key);
}

12
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -30,7 +30,14 @@ import {
WidgetLayout
} from '@shared/models/dashboard.models';
import { isDefined, isString, isUndefined } from '@core/utils';
import { Datasource, DatasourceType, Widget, WidgetConfig, widgetType } from '@app/shared/models/widget.models';
import {
Datasource,
datasourcesHasOnlyComparisonAggregation,
DatasourceType,
Widget,
WidgetConfig,
widgetType
} from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id';
@ -227,7 +234,8 @@ export class DashboardUtilsService {
}
});
if (type === widgetType.latest) {
widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, this.timeService);
const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources);
widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService);
}
if (type === widgetType.alarm) {
if (!widgetConfig.alarmFilterConfig) {

18
ui-ngx/src/app/core/services/dialog.service.ts

@ -29,6 +29,7 @@ import {
} from '@shared/components/dialog/material-icons-dialog.component';
import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component';
import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component';
@Injectable(
@ -78,6 +79,23 @@ export class DialogService {
return dialogRef.afterClosed();
}
errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable<any> {
const dialogConfig: MatDialogConfig = {
disableClose: true,
data: {
title,
message,
error,
ok: ok || this.translate.instant('action.ok')
}
};
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(ErrorAlertDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
colorPicker(color: string): Observable<string> {
return this.dialog.open<ColorPickerDialogComponent, ColorPickerDialogData, string>(ColorPickerDialogComponent,
{

4
ui-ngx/src/app/core/services/utils.service.ts

@ -188,6 +188,10 @@ export class UtilsService {
public processWidgetException(exception: any): ExceptionData {
const data = this.parseException(exception, -6);
if (data.message?.startsWith('NG0')) {
data.message = `${this.translate.instant('widget.widget-template-error')}<br/>
<br/><i>${this.translate.instant('dialog.error-message-title')}</i><br/><br/>${data.message}`;
}
if (this.widgetEditMode) {
const message: WindowMessage = {
type: 'widgetException',

6
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts

@ -35,6 +35,7 @@ import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UtilsService } from '@core/services/utils.service';
export interface AlarmDetailsDialogData {
alarmId?: string;
@ -79,6 +80,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
private translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData,
private alarmService: AlarmService,
private utils: UtilsService,
public dialogRef: MatDialogRef<AlarmDetailsDialogComponent, boolean>,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
@ -136,7 +138,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
}
this.alarmFormGroup.get('duration').patchValue(duration);
}
this.alarmFormGroup.get('type').patchValue(alarm.type);
this.alarmFormGroup.get('type').patchValue(this.utils.customTranslation(alarm.type, alarm.type));
this.alarmFormGroup.get('alarmStatus')
.patchValue(this.translate.instant(alarmStatusTranslations.get(alarm.status)));
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);
@ -175,7 +177,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
onReassign(): void {
this.alarmUpdated = true;
this.loadAlarm()
this.loadAlarm();
this.alarmCommentComponent.loadAlarmComments();
}
}

5
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -86,7 +86,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
private utilsService: UtilsService,
pageMode = false) {
super();
this.loadDataOnInit = false;
this.loadDataOnInit = pageMode;
this.tableTitle = '';
this.useTimePageLink = true;
this.forAllTimeEnabled = true;
@ -116,7 +116,8 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%',
(entity) => entity.originatorName, entity => ({}), false));
this.columns.push(
new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%'));
new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%',
entity => this.utilsService.customTranslation(entity.type, entity.type)));
this.columns.push(
new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%',
(entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)),

2
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -139,7 +139,7 @@
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">
<mat-header-cell *matHeaderCellDef style="width: 40px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
[indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">

2
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form (ngSubmit)="add()" style="min-width: 400px; width: 700px;" [ngStyle]="entitiesTableConfig.addDialogStyle">
<form (ngSubmit)="add()" style="min-width: 400px; width: 750px;" [ngStyle]="entitiesTableConfig.addDialogStyle">
<mat-toolbar color="primary">
<h2 translate>{{ translations.add }}</h2>
<span fxFlex></span>

13
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -14,7 +14,16 @@
/// limitations under the License.
///
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { MatDialog } from '@angular/material/dialog';
@ -32,7 +41,7 @@ import { Subscription } from 'rxjs';
templateUrl: './event-table.component.html',
styleUrls: ['./event-table.component.scss']
})
export class EventTableComponent implements OnInit, AfterViewInit {
export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
@Input()
tenantId: string;

7
ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -53,7 +53,7 @@ import { takeUntil } from 'rxjs/operators';
}
]
})
export class TenantProfileQueuesComponent implements ControlValueAccessor, Validator, OnDestroy {
export class TenantProfileQueuesComponent implements ControlValueAccessor, Validator, OnDestroy, OnInit {
tenantProfileQueuesFormGroup: UntypedFormGroup;
newQueue = false;
@ -107,6 +107,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.newQueue = false;
if (this.disabled) {
this.tenantProfileQueuesFormGroup.disable({emitEvent: false});
} else {
@ -115,7 +116,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
}
writeValue(queues: Array<QueueInfo> | null): void {
if (queues.length === this.queuesFormArray.length) {
if (queues?.length === this.queuesFormArray.length) {
this.queuesFormArray.patchValue(queues, {emitEvent: false});
} else {
const queuesControls: Array<AbstractControl> = [];

64
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -21,7 +21,7 @@
{{ 'tenant-profile.entities' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-devices</mat-label>
<input matInput required min="0" step="1"
formControlName="maxDevices"
@ -32,8 +32,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDevices').hasError('min')">
{{ 'tenant-profile.maximum-devices-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-dashboards</mat-label>
<input matInput required min="0" step="1"
formControlName="maxDashboards"
@ -44,10 +45,11 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDashboards').hasError('min')">
{{ 'tenant-profile.maximum-dashboards-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-assets</mat-label>
<input matInput required min="0" step="1"
formControlName="maxAssets"
@ -58,8 +60,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxAssets').hasError('min')">
{{ 'tenant-profile.maximum-assets-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-users</mat-label>
<input matInput required min="0" step="1"
formControlName="maxUsers"
@ -70,6 +73,7 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxUsers').hasError('min')">
{{ 'tenant-profile.maximum-users-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<mat-expansion-panel class="configuration-panel">
@ -80,7 +84,7 @@
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-customers</mat-label>
<input matInput required min="0" step="1"
formControlName="maxCustomers"
@ -91,8 +95,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCustomers').hasError('min')">
{{ 'tenant-profile.maximum-customers-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-rule-chains</mat-label>
<input matInput required min="0" step="1"
formControlName="maxRuleChains"
@ -103,6 +108,7 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleChains').hasError('min')">
{{ 'tenant-profile.maximum-rule-chains-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
</ng-template>
@ -114,7 +120,7 @@
{{ 'tenant-profile.rule-engine' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-r-e-executions</mat-label>
<input matInput required min="0" step="1"
formControlName="maxREExecutions"
@ -125,8 +131,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxREExecutions').hasError('min')">
{{ 'tenant-profile.max-r-e-executions-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-transport-messages</mat-label>
<input matInput required min="0" step="1"
formControlName="maxTransportMessages"
@ -137,6 +144,7 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportMessages').hasError('min')">
{{ 'tenant-profile.max-transport-messages-required' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<mat-expansion-panel class="configuration-panel">
@ -147,7 +155,7 @@
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-j-s-executions</mat-label>
<input matInput required min="0" step="1"
formControlName="maxJSExecutions"
@ -158,8 +166,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxJSExecutions').hasError('min')">
{{ 'tenant-profile.max-j-s-executions-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-transport-data-points</mat-label>
<input matInput required min="0" step="1"
formControlName="maxTransportDataPoints"
@ -170,10 +179,11 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportDataPoints').hasError('min')">
{{ 'tenant-profile.max-transport-data-points-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-rule-node-executions-per-message</mat-label>
<input matInput required min="0" step="1"
formControlName="maxRuleNodeExecutionsPerMessage"
@ -184,6 +194,7 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleNodeExecutionsPerMessage').hasError('min')">
{{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div fxFlex></div>
</div>
@ -196,7 +207,7 @@
{{ 'tenant-profile.time-to-live' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-d-p-storage-days</mat-label>
<input matInput required min="0" step="1"
formControlName="maxDPStorageDays"
@ -207,8 +218,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDPStorageDays').hasError('min')">
{{ 'tenant-profile.max-d-p-storage-days-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.alarms-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="alarmsTtlDays"
@ -219,10 +231,11 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('min')">
{{ 'tenant-profile.alarms-ttl-days-days-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.default-storage-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="defaultStorageTtlDays"
@ -233,8 +246,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('defaultStorageTtlDays').hasError('min')">
{{ 'tenant-profile.default-storage-ttl-days-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.rpc-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="rpcTtlDays"
@ -245,6 +259,7 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('rpcTtlDays').hasError('min')">
{{ 'tenant-profile.rpc-ttl-days-days-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
</fieldset>
@ -254,7 +269,7 @@
{{ 'tenant-profile.alarms-and-notifications' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-emails</mat-label>
<input matInput required min="0" step="1"
formControlName="maxEmails"
@ -265,8 +280,9 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxEmails').hasError('min')">
{{ 'tenant-profile.max-emails-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-created-alarms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxCreatedAlarms"
@ -277,10 +293,11 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCreatedAlarms').hasError('min')">
{{ 'tenant-profile.max-created-alarms-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-sms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSms"
@ -291,6 +308,7 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('min')">
{{ 'tenant-profile.max-sms-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div fxFlex></div>
</div>
@ -301,7 +319,7 @@
{{ 'tenant-profile.ota-files-in-bytes' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-resources-sum-data-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxResourcesInBytes"
@ -312,18 +330,20 @@
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('min')">
{{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-ota-packages-sum-data-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxOtaPackagesInBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('required')">
{{ 'tenant-profile.maximum-ota-packages-sum-data-size-required' | translate}}
{{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('min')">
{{ 'tenant-profile.maximum-ota-packages-sum-data-size-range' | translate}}
{{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
</fieldset>

8
ui-ngx/src/app/modules/home/components/queue/queue-form.component.html

@ -37,10 +37,10 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="16px" formGroupName="submitStrategy">
<div fxFlex fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="16px" formGroupName="submitStrategy">
<div fxFlex>
<label class="group-label" translate>queue.submit-strategy</label>
<mat-radio-group fxFlex fxLayout="column" fxLayoutGap="15px" formControlName="type" required>
<mat-radio-group fxFlex fxLayout="column" formControlName="type" required>
<mat-radio-button *ngFor="let strategy of submitStrategies" [value]="strategy">
{{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }}
<mat-icon
@ -75,10 +75,10 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="16px" formGroupName="processingStrategy">
<div fxFlex fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="16px" formGroupName="processingStrategy">
<div fxFlex>
<label class="group-label" translate>queue.processing-strategy</label>
<mat-radio-group fxFlex fxLayout="column" fxLayoutGap="15px" formControlName="type" required>
<mat-radio-group fxFlex fxLayout="column" formControlName="type" required>
<mat-radio-button *ngFor="let strategy of processingStrategies" [value]="strategy">
{{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).label | translate }}
<mat-icon

13
ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts

@ -129,11 +129,6 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr
this.queueFormGroup.get('submitStrategy').get('type').valueChanges.subscribe(() => {
this.submitStrategyTypeChanged();
});
if (this.newQueue) {
this.queueFormGroup.get('name').enable({emitEvent: false});
} else {
this.queueFormGroup.get('name').disable({emitEvent: false});
}
}
ngOnDestroy() {
@ -149,7 +144,11 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr
this.queueFormGroup.disable({emitEvent: false});
} else {
this.queueFormGroup.enable({emitEvent: false});
this.queueFormGroup.get('name').disable({emitEvent: false});
if (this.newQueue) {
this.queueFormGroup.get('name').enable({emitEvent: false});
} else {
this.queueFormGroup.get('name').disable({emitEvent: false});
}
}
}
@ -203,7 +202,7 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr
const type: QueueSubmitStrategyTypes = form.get('type').value;
const batchSizeField = form.get('batchSize');
if (type === QueueSubmitStrategyTypes.BATCH) {
batchSizeField.patchValue(1000, {emitEvent: false});
batchSizeField.patchValue(batchSizeField.value ?? 1000, {emitEvent: false});
batchSizeField.setValidators([Validators.min(1), Validators.required]);
batchSizeField.updateValueAndValidity({emitEvent: false});
this.hideBatchSize = true;

18
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts

@ -33,6 +33,8 @@ import {
CustomDialogComponent,
CustomDialogData
} from '@home/components/widget/dialog/custom-dialog.component';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
export interface CustomDialogContainerData {
controller: (instance: CustomDialogComponent) => void;
@ -54,6 +56,8 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
protected router: Router,
public viewContainerRef: ViewContainerRef,
public dialogRef: MatDialogRef<CustomDialogContainerComponent>,
private dialogService: DialogService,
private translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: CustomDialogContainerData) {
super(store, router, dialogRef);
let customDialogData: CustomDialogData = {
@ -72,7 +76,19 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
useValue: dialogRef
}]
});
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentFactory, 0, injector);
try {
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentFactory, 0, injector);
} catch (e: any) {
let message;
if (e.message?.startsWith('NG0')) {
message = this.translate.instant('widget-action.custom-pretty-template-error');
} else {
message = this.translate.instant('widget-action.custom-pretty-controller-error');
}
dialogRef.close();
console.error(e);
this.dialogService.errorAlert(this.translate.instant('widget-action.custom-pretty-error-title'), message, e);
}
}
ngOnDestroy(): void {

1
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -106,6 +106,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
}
ngOnDestroy(): void {
super.ngOnDestroy();
if (this.ctx.telemetrySubscribers) {
this.ctx.telemetrySubscribers.forEach(item => item.unsubscribe());
}

26
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -36,7 +36,13 @@ import { Observable, of } from 'rxjs';
import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { Circle } from './circle';
import { createTooltip, entitiesParseName, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils';
import {
createTooltip,
entitiesParseName,
isCutPolygon,
isJSON,
isValidLatLng
} from '@home/components/widget/lib/maps/maps-utils';
import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import {
@ -148,7 +154,8 @@ export default abstract class LeafletMap {
if (isDefinedAndNotNull(markerColor) && tinycolor(markerColor).isValid()) {
const parsedColor = tinycolor(markerColor);
return L.divIcon({
html: `<div style="background-color: ${parsedColor.setAlpha(0.4).toRgbString()};" class="marker-cluster tb-cluster-marker-element">` +
html: `<div style="background-color: ${parsedColor.setAlpha(0.4).toRgbString()};" ` +
`class="marker-cluster tb-cluster-marker-element">` +
`<div style="background-color: ${parsedColor.setAlpha(0.9).toRgbString()};"><span>` + childCount + '</span></div></div>',
iconSize: new L.Point(40, 40),
className: 'tb-cluster-marker-container'
@ -378,7 +385,8 @@ export default abstract class LeafletMap {
});
},
});
this.map.pm.Toolbar.changeControlOrder(['tbMarker', 'tbRectangle', 'tbPolygon', 'tbCircle', 'editMode', 'dragMode', 'tbCut', 'removalMode', 'rotateMode']);
this.map.pm.Toolbar.changeControlOrder(['tbMarker', 'tbRectangle', 'tbPolygon', 'tbCircle',
'editMode', 'dragMode', 'tbCut', 'removalMode', 'rotateMode']);
}
this.map.pm.setLang('en', this.translateService.instant('widgets.maps'), 'en');
@ -643,19 +651,19 @@ export default abstract class LeafletMap {
}
}
extractPosition(data: FormattedData): {x: number, y: number} {
extractPosition(data: FormattedData): {x: number; y: number} {
if (!data) {
return null;
}
const lat = data[this.options.latKeyName];
const lng = data[this.options.lngKeyName];
if (!isDefinedAndNotNull(lat) || isString(lat) || isNaN(lat) || !isDefinedAndNotNull(lng) || isString(lng) || isNaN(lng)) {
if (!isValidLatLng(lat, lng)) {
return null;
}
return {x: lat, y: lng};
}
positionToLatLng(position: {x: number, y: number}): L.LatLng {
positionToLatLng(position: {x: number; y: number}): L.LatLng {
return L.latLng(position.x, position.y) as L.LatLng;
}
@ -936,7 +944,7 @@ export default abstract class LeafletMap {
return;
}
this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe();
}
};
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: Partial<WidgetMarkersSettings>,
updateBounds = true, callback?, snappable = false): Marker {
@ -1141,7 +1149,7 @@ export default abstract class LeafletMap {
}
}
this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {});
}
};
createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: Partial<WidgetPolygonSettings>,
updateBounds = true, snappable = false) {
@ -1218,7 +1226,7 @@ export default abstract class LeafletMap {
const center = e.layer.getLatLng();
const radius = e.layer.getRadius();
this.saveLocation(data, this.convertCircleToCustomFormat(center, radius)).subscribe(() => {});
}
};
updateCircle(circlesData: FormattedData[], updateBounds = true) {
const toDelete = new Set(Array.from(this.circles.keys()));

15
ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts

@ -20,7 +20,7 @@ import {
ShowTooltipAction, WidgetToolipSettings
} from './map-models';
import { Datasource, FormattedData } from '@app/shared/models/widget.models';
import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils';
import { fillDataPattern, isDefinedAndNotNull, isString, processDataPattern, safeExecute } from '@core/utils';
import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils';
export function createTooltip(target: L.Layer,
@ -110,3 +110,16 @@ export function entitiesParseName(entities: FormattedData[], labelSettings: labe
}
return entities;
}
export const isValidLatitude = (latitude: any): boolean =>
isDefinedAndNotNull(latitude) &&
!isString(latitude) &&
!isNaN(latitude) && isFinite(latitude) && Math.abs(latitude) <= 90;
export const isValidLongitude = (longitude: any): boolean =>
isDefinedAndNotNull(longitude) &&
!isString(longitude) &&
!isNaN(longitude) && isFinite(longitude) && Math.abs(longitude) <= 180;
export const isValidLatLng = (latitude: any, longitude: any): boolean =>
isValidLatitude(latitude) && isValidLongitude(longitude);

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html

@ -290,7 +290,7 @@
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.custom-legend-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('customLegendEnabled').value">
<mat-expansion-panel-header fxLayout="row" style="height: 100%; min-height: 48px;">
<mat-expansion-panel-header fxLayout="row" style="height: max-content; min-height: 48px;">
<mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="customLegendEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts

@ -72,8 +72,8 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent
protected prepareOutputSettings(settings: any): WidgetSettings {
return {
title: settings.title,
...settings.switchRpcSettings
...settings.switchRpcSettings,
title: settings.title
};
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts

@ -78,10 +78,10 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent
protected prepareOutputSettings(settings: any): WidgetSettings {
return {
...settings.switchRpcSettings,
title: settings.title,
labelPosition: settings.labelPosition,
sliderColor: settings.sliderColor,
...settings.switchRpcSettings
sliderColor: settings.sliderColor
};
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts

@ -75,9 +75,9 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen
protected prepareOutputSettings(settings: any): WidgetSettings {
return {
...settings.switchRpcSettings,
title: settings.title,
showOnOffLabels: settings.showOnOffLabels,
...settings.switchRpcSettings
showOnOffLabels: settings.showOnOffLabels
};
}
}

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

@ -224,6 +224,11 @@ export class WidgetComponentService {
);
}
public clearWidgetInfo(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): void {
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(widgetInfo.componentFactory);
this.widgetService.deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
}
private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
const widgetInfoSubject = new ReplaySubject<WidgetInfo>();
const widgetInfo = this.widgetService.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);

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

@ -34,7 +34,7 @@
</tb-legend>
</div>
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>
<span [innerHtml]="('Widget Error:<br/><br/>' + widgetErrorData.message) | safe:'html'"></span>
</div>
<div class="tb-absolute-fill tb-widget-no-data" *ngIf="displayNoData">
<span fxLayoutAlign="center center"

26
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -516,6 +516,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.onInit();
},
(err) => {
this.widgetContext.inited = true;
// console.log(err);
}
);
@ -739,9 +740,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.createDefaultSubscription().subscribe(
() => {
this.subscriptionInited = true;
this.configureDynamicWidgetComponent();
initSubject.next();
initSubject.complete();
try {
this.configureDynamicWidgetComponent();
initSubject.next();
initSubject.complete();
} catch (err) {
initSubject.error(err);
}
},
(err) => {
this.subscriptionInited = true;
@ -751,9 +756,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} else {
this.loadingData = false;
this.subscriptionInited = true;
this.configureDynamicWidgetComponent();
initSubject.next();
initSubject.complete();
try {
this.configureDynamicWidgetComponent();
initSubject.next();
initSubject.complete();
} catch (err) {
initSubject.error(err);
}
}
return initSubject.asObservable();
}
@ -807,12 +816,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector);
this.cd.detectChanges();
} catch (e) {
console.error(e);
if (this.dynamicWidgetComponentRef) {
this.dynamicWidgetComponentRef.destroy();
this.dynamicWidgetComponentRef = null;
}
this.widgetContentContainer.clear();
this.handleWidgetException(e);
this.widgetComponentService.clearWidgetInfo(this.widgetInfo, this.widget.bundleAlias, this.widget.typeAlias,
this.widget.isSystemType);
throw e;
}
if (this.dynamicWidgetComponentRef) {

2
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html

@ -164,6 +164,8 @@
<mat-card-title>
<span class="mat-headline-5" translate>admin.jwt.security-settings</span>
</mat-card-title>
<span fxFlex></span>
<div tb-help="jwtSecuritySettings"></div>
</mat-card-header>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="jwtSecuritySettingsFormGroup" (ngSubmit)="saveJwtSettings()" autocomplete="off">

30
ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw

@ -32,8 +32,8 @@
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
"usePostProcessing": true,
"postFuncBody": "return value ? value : 'ENABLED';"
},
{
"name": "jsExecutionLimit",
@ -72,7 +72,7 @@
"decimals": null,
"funcBody": null,
"usePostProcessing": true,
"postFuncBody": "return value ? value.toLowerCase() : '';"
"postFuncBody": "return value ? value.toLowerCase() : 'enabled';"
},
{
"name": "jsExecutionApiState",
@ -190,8 +190,8 @@
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
"usePostProcessing": true,
"postFuncBody": "return value ? value : 'ENABLED';"
},
{
"name": "storageDataPointsLimit",
@ -230,7 +230,7 @@
"decimals": null,
"funcBody": null,
"usePostProcessing": true,
"postFuncBody": "return value ? value.toLowerCase() : '';"
"postFuncBody": "return value ? value.toLowerCase() : 'enabled';"
},
{
"name": "dbApiState",
@ -348,8 +348,8 @@
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
"usePostProcessing": true,
"postFuncBody": "return value ? value : 'ENABLED';"
},
{
"name": "ruleEngineExecutionLimit",
@ -388,7 +388,7 @@
"decimals": null,
"funcBody": null,
"usePostProcessing": true,
"postFuncBody": "return value ? value.toLowerCase() : '';"
"postFuncBody": "return value ? value.toLowerCase() : 'enabled';"
},
{
"name": "ruleEngineApiState",
@ -516,8 +516,8 @@
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
"usePostProcessing": true,
"postFuncBody": "return value ? value : 'ENABLED';"
},
{
"name": "transportMsgLimit",
@ -556,7 +556,7 @@
"decimals": null,
"funcBody": null,
"usePostProcessing": true,
"postFuncBody": "return value ? value.toLowerCase() : '';"
"postFuncBody": "return value ? value.toLowerCase() : 'enabled';"
},
{
"name": "transportApiState",
@ -687,8 +687,8 @@
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
"usePostProcessing": true,
"postFuncBody": "return value ? value : 'ENABLED';"
},
{
"name": "createdAlarmsLimit",
@ -727,7 +727,7 @@
"decimals": null,
"funcBody": null,
"usePostProcessing": true,
"postFuncBody": "return value ? value.toLowerCase() : '';"
"postFuncBody": "return value ? value.toLowerCase() : 'enabled';"
},
{
"name": "alarmApiState",

3
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -489,9 +489,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
if (!this.gotError) {
this.gotError = true;
let errorInfo = 'Error:';
if (details.name) {
errorInfo += ' ' + details.name + ':';
}
if (details.message) {
errorInfo += ' ' + details.message;
}

2
ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html

@ -16,7 +16,7 @@
-->
<h2 mat-dialog-title>{{data.title}}</h2>
<div mat-dialog-content [innerHTML]="data.message"></div>
<div mat-dialog-content [innerHTML]="data.message | safe:'html'"></div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary" [mat-dialog-close]="true" cdkFocusInitial>{{data.ok}}</button>
</div>

30
ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<h2 mat-dialog-title>{{title}}</h2>
<div mat-dialog-content class="tb-error-alert-dialog-content">
<div>{{ message }}</div>
<div class="error-message-title" translate>dialog.error-message-title</div>
<div class="error-message-content"> {{ errorMessage }} </div>
<mat-expansion-panel *ngIf="errorDetails">
<mat-expansion-panel-header>{{ 'dialog.error-details-title' | translate }}</mat-expansion-panel-header>
<small class="error-details-content" [innerHTML]="errorDetails"></small>
</mat-expansion-panel>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary" [mat-dialog-close]="true" cdkFocusInitial>{{data.ok}}</button>
</div>

37
ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.mat-mdc-dialog-content {
padding: 0 24px 24px;
}
.tb-error-alert-dialog-content {
.error-message-title {
font-style: italic;
}
.error-message-content {
color: red;
}
.error-details-content {
display: block;
border: solid 1px #d3d3d3;
padding: 8px;
border-radius: 4px;
}
& > *:not(:last-child) {
padding-bottom: 16px;
}
}
}

49
ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts

@ -0,0 +1,49 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
export interface ErrorAlertDialogData {
title: string;
message: string;
error: any;
ok: string;
}
@Component({
selector: 'tb-error-alert-dialog',
templateUrl: './error-alert-dialog.component.html',
styleUrls: ['./error-alert-dialog.component.scss']
})
export class ErrorAlertDialogComponent {
title: string;
message: string;
errorMessage: string;
errorDetails?: string;
constructor(public dialogRef: MatDialogRef<ErrorAlertDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ErrorAlertDialogData) {
this.title = this.data.title;
this.message = this.data.message;
this.errorMessage = this.data.error.message ? this.data.error.message : JSON.stringify(this.data.error);
if (this.data.error.stack) {
this.errorDetails = this.data.error.stack.replaceAll('\n', '<br/>');
}
}
}

2
ui-ngx/src/app/shared/components/file-input.component.scss

@ -87,7 +87,7 @@ $previewSize: 100px !default;
button.browse-file {
padding: 0;
font-size: 16px;
span.mdc-button__label {
label {
display: block;
cursor: pointer;
padding: 0 16px;

2
ui-ngx/src/app/shared/components/image-input.component.scss

@ -141,7 +141,7 @@ $previewSize: 96px !default;
button.browse-file {
padding: 0;
font-size: 16px;
span.mdc-button__label {
label {
display: block;
cursor: pointer;
padding: 0 16px;

2
ui-ngx/src/app/shared/components/multiple-image-input.component.scss

@ -157,7 +157,7 @@ $previewSize: 64px !default;
button.browse-file {
padding: 0;
font-size: 16px;
span.mdc-button__label {
label {
display: block;
cursor: pointer;
padding: 0 16px;

4
ui-ngx/src/app/shared/components/notification/notification.component.html

@ -28,8 +28,8 @@
</mat-icon>
</ng-template>
<div class="content" fxFlex>
<div class="title" [innerHTML]="(notification.subject | safe: 'html' )"></div>
<div class="message" [innerHTML]="(notification.text | safe: 'html' )"></div>
<div class="title" [innerHTML]="(title | safe: 'html' )"></div>
<div class="message" [innerHTML]="(message | safe: 'html' )"></div>
<button *ngIf="showButton" mat-stroked-button class="button" color="primary"
(click)="navigate($event)">
{{ buttonLabel }}

5
ui-ngx/src/app/shared/components/notification/notification.component.ts

@ -62,6 +62,9 @@ export class NotificationComponent implements OnInit {
currentDate = Date.now();
title = '';
message = '';
constructor(
private utils: UtilsService,
private router: Router
@ -72,6 +75,8 @@ export class NotificationComponent implements OnInit {
this.showIcon = this.notification.additionalConfig?.icon?.enabled;
this.showButton = this.notification.additionalConfig?.actionButtonConfig?.enabled;
this.hideMarkAsReadButton = this.notification.status === NotificationStatus.READ;
this.title = this.utils.customTranslation(this.notification.subject, this.notification.subject);
this.message = this.utils.customTranslation(this.notification.text, this.notification.text);
if (this.showButton) {
this.buttonLabel = this.utils.customTranslation(this.notification.additionalConfig.actionButtonConfig.text,
this.notification.additionalConfig.actionButtonConfig.text);

2
ui-ngx/src/app/shared/components/snack-bar-component.scss

@ -51,7 +51,7 @@
padding: 0 18px;
margin: 8px;
.toast-text {
padding: 0 6px;
padding: 8px;
width: 100%;
}
button {

2
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -46,7 +46,7 @@
(click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span>
{{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue?.displayTimezoneAbbr}}</span></span>
</span>
<button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
type="button"

93
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -21,8 +21,6 @@ import {
forwardRef,
Injector,
Input,
OnDestroy,
OnInit,
StaticProvider,
ViewContainerRef
} from '@angular/core';
@ -68,7 +66,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}
]
})
export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAccessor {
export class TimewindowComponent implements ControlValueAccessor {
historyOnlyValue = false;
@ -91,71 +89,29 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
@coerceBoolean()
forAllTimeEnabled = false;
alwaysDisplayTypePrefixValue = false;
@Input()
set alwaysDisplayTypePrefix(val) {
this.alwaysDisplayTypePrefixValue = coerceBooleanProperty(val);
}
get alwaysDisplayTypePrefix() {
return this.alwaysDisplayTypePrefixValue;
}
quickIntervalOnlyValue = false;
@coerceBoolean()
alwaysDisplayTypePrefix = false;
@Input()
set quickIntervalOnly(val) {
this.quickIntervalOnlyValue = coerceBooleanProperty(val);
}
get quickIntervalOnly() {
return this.quickIntervalOnlyValue;
}
aggregationValue = false;
@coerceBoolean()
quickIntervalOnly = false;
@Input()
set aggregation(val) {
this.aggregationValue = coerceBooleanProperty(val);
}
get aggregation() {
return this.aggregationValue;
}
timezoneValue = false;
@coerceBoolean()
aggregation = false;
@Input()
set timezone(val) {
this.timezoneValue = coerceBooleanProperty(val);
}
get timezone() {
return this.timezoneValue;
}
isToolbarValue = false;
@coerceBoolean()
timezone = false;
@Input()
set isToolbar(val) {
this.isToolbarValue = coerceBooleanProperty(val);
}
get isToolbar() {
return this.isToolbarValue;
}
asButtonValue = false;
@coerceBoolean()
isToolbar = false;
@Input()
set asButton(val) {
this.asButtonValue = coerceBooleanProperty(val);
}
get asButton() {
return this.asButtonValue;
}
@coerceBoolean()
asButton = false;
@Input()
@coerceBoolean()
@ -179,7 +135,9 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
@Input()
tooltipPosition: TooltipPosition = 'above';
@Input() disabled: boolean;
@Input()
@coerceBoolean()
disabled: boolean;
innerValue: Timewindow;
@ -198,12 +156,6 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
public breakpointObserver: BreakpointObserver) {
}
ngOnInit(): void {
}
ngOnDestroy(): void {
}
toggleTimewindow($event: Event) {
if ($event) {
$event.stopPropagation();
@ -215,7 +167,6 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
maxHeight: '80vh',
height: 'min-content'
});
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
@ -262,12 +213,10 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
}
private onHistoryOnlyChanged(): boolean {
if (this.historyOnlyValue && this.innerValue) {
if (this.innerValue.selectedTab !== TimewindowType.HISTORY) {
this.innerValue.selectedTab = TimewindowType.HISTORY;
this.updateDisplayValue();
return true;
}
if (this.historyOnlyValue && this.innerValue && this.innerValue.selectedTab !== TimewindowType.HISTORY) {
this.innerValue.selectedTab = TimewindowType.HISTORY;
this.updateDisplayValue();
return true;
}
return false;
}
@ -285,7 +234,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
}
writeValue(obj: Timewindow): void {
this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.timeService);
this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService);
this.timewindowDisabled = this.isTimewindowDisabled();
if (this.onHistoryOnlyChanged()) {
setTimeout(() => {

1
ui-ngx/src/app/shared/components/value-input.component.ts

@ -80,6 +80,7 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
if (res) {
this.modelValue = res;
this.inputForm.control.patchValue({value: this.modelValue});
this.updateView();
}
}
);

2
ui-ngx/src/app/shared/components/widgets-bundle-search.component.html

@ -23,7 +23,7 @@
(focus)="toggleFocus()"
(blur)="toggleFocus()"
[placeholder]=placeholder>
<button mat-button type="button" *ngIf="searchText" mat-icon-button (click)="clear($event)">
<button type="button" *ngIf="searchText" mat-icon-button (click)="clear($event)">
<mat-icon class="close">close</mat-icon>
</button>
</div>

24
ui-ngx/src/app/shared/models/ace/tbel/worker-tbel.js

@ -5026,6 +5026,21 @@ var JSHINT = (function() {
return that;
}, 20);
}
function nullSafeProperty(s) {
console.log("test " + s);
symbol(s, 20).exps = true;
return infix(s, function(context, left, that) {
if (state.option.bitwise) {
warning("W016", that, that.id);
}
checkLeftSideAssign(context, left, that);
that.right = expression(context, 10);
return that;
}, 20);
}
function suffix(s) {
var x = symbol(s, 150);
@ -5539,6 +5554,9 @@ var JSHINT = (function() {
bitwiseassignop("<<=");
bitwiseassignop(">>=");
bitwiseassignop(">>>=");
nullSafeProperty(".?");
infix(",", function(context, left, that) {
if (state.option.nocomma) {
warning("W127", that);
@ -9450,6 +9468,12 @@ Lexer.prototype = {
switch (ch1) {
case ".":
if (ch1 === "." && this.peek(1) === "?") {
return {
type: Token.Punctuator,
value: ".?"
};
}
if ((/^[0-9]$/).test(this.peek(1))) {
return null;
}

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

@ -166,6 +166,7 @@ export const HelpLinks = {
templateNotifications: helpBaseUrl + '/docs/user-guide/notifications/#templates',
recipientNotifications: helpBaseUrl + '/docs/user-guide/notifications/#recipients',
ruleNotifications: helpBaseUrl + '/docs/user-guide/notifications/#rules',
jwtSecuritySettings: helpBaseUrl + '/docs/user-guide/ui/jwt-security-settings/',
}
};

6
ui-ngx/src/app/shared/models/time/time.models.ts

@ -245,7 +245,8 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => {
}
};
export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow => {
export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, timeService: TimeService): Timewindow => {
const model = defaultTimewindow(timeService);
if (value) {
model.hideInterval = value.hideInterval;
@ -316,6 +317,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
if (quickIntervalOnly) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
}
if (historyOnly) {
model.selectedTab = TimewindowType.HISTORY;
}
return model;
};

3
ui-ngx/src/app/shared/shared.module.ts

@ -123,6 +123,7 @@ import { JsFuncComponent } from '@shared/components/js-func.component';
import { JsonFormComponent } from '@shared/components/json-form/json-form.component';
import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component';
import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component';
import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component';
import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component';
@ -305,6 +306,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
WidgetsBundleSelectComponent,
ConfirmDialogComponent,
AlertDialogComponent,
ErrorAlertDialogComponent,
TodoDialogComponent,
ColorPickerDialogComponent,
MaterialIconsDialogComponent,
@ -528,6 +530,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
MarkdownModule,
ConfirmDialogComponent,
AlertDialogComponent,
ErrorAlertDialogComponent,
TodoDialogComponent,
ColorPickerDialogComponent,
MaterialIconsDialogComponent,

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

@ -1789,7 +1789,9 @@
}
},
"dialog": {
"close": "Close dialog"
"close": "Close dialog",
"error-message-title": "Error message:",
"error-details-title": "Error details"
},
"direction": {
"column": "Column",
@ -3991,7 +3993,8 @@
"alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities",
"search": "Search widget",
"filter": "Widget filter type",
"loading-widgets": "Loading widgets..."
"loading-widgets": "Loading widgets...",
"widget-template-error": "Invalid widget HTML template."
},
"widget-action": {
"header-button": "Widget header button",
@ -4000,6 +4003,9 @@
"open-dashboard": "Navigate to other dashboard",
"custom": "Custom action",
"custom-pretty": "Custom action (with HTML template)",
"custom-pretty-error-title": "Custom dialog error",
"custom-pretty-template-error": "Invalid custom dialog template.",
"custom-pretty-controller-error": "Error occurred while evaluating custom dialog function.",
"mobile-action": "Mobile action",
"target-dashboard-state": "Target dashboard state",
"target-dashboard-state-required": "Target dashboard state is required",

1143
ui-ngx/src/assets/locale/locale.constant-es_ES.json

File diff suppressed because it is too large

4
ui-ngx/src/styles.scss

@ -567,6 +567,10 @@ mat-label {
}
}
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
z-index: 1;
}
.mat-toolbar.mat-mdc-table-toolbar .mat-mdc-form-field.mat-form-field-appearance-fill,
.mat-mdc-form-field.mat-form-field-appearance-fill.tb-appearance-transparent {
.mdc-text-field--filled {

188
ui-ngx/yarn.lock

@ -31,12 +31,12 @@
"@angular-devkit/core" "15.2.1"
rxjs "6.6.7"
"@angular-devkit/architect@0.1502.4":
version "0.1502.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.4.tgz#a2ceef215f75b03ec275f5ff68e87fc43e44b113"
integrity sha512-bDBcaRMBfXFfK9MpvfNO926F1rL0PEw+mveXxq3/SSql+1XP/hrc5TVGwnoim4g6DqsGmu9upS5DyJ6PnL/hHA==
"@angular-devkit/architect@0.1502.8":
version "0.1502.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.8.tgz#9fd3fd27b3a7fc5f8eb65c92500b4d9d15b879e8"
integrity sha512-rTltw2ABHrcKc8EGimALvXmrDTP5hlNbEy6nYolJoXEI9EwHgriWrVLVPs3OEF+/ed47dbJi9EGOXUOgzgpB5A==
dependencies:
"@angular-devkit/core" "15.2.4"
"@angular-devkit/core" "15.2.8"
rxjs "6.6.7"
"@angular-devkit/build-angular@^15.0.0":
@ -107,15 +107,15 @@
optionalDependencies:
esbuild "0.17.8"
"@angular-devkit/build-angular@^15.2.4":
version "15.2.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.4.tgz#ac9230fef0d57d1402796ec0e9be3b3afd90ee51"
integrity sha512-wt0S4oz0vxuW0/Ak5X0vQ7s7TSPynmktVNJblu9SFRgwCD3kplV2B693F+M6t8eLzSy0UCSbZp9h3Ae8gLEiEw==
"@angular-devkit/build-angular@^15.2.8":
version "15.2.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.8.tgz#5412125b810fee084eb8afc20b9911606ad66170"
integrity sha512-TGDnXhhOG6h6TOrWWzfnkha7wYBOXi7iJc1o1w1VKCayE3T6TZZdF847aK66vL9KG7AKYVdGhWEGw2WBHUBUpg==
dependencies:
"@ampproject/remapping" "2.2.0"
"@angular-devkit/architect" "0.1502.4"
"@angular-devkit/build-webpack" "0.1502.4"
"@angular-devkit/core" "15.2.4"
"@angular-devkit/architect" "0.1502.8"
"@angular-devkit/build-webpack" "0.1502.8"
"@angular-devkit/core" "15.2.8"
"@babel/core" "7.20.12"
"@babel/generator" "7.20.14"
"@babel/helper-annotate-as-pure" "7.18.6"
@ -127,7 +127,7 @@
"@babel/runtime" "7.20.13"
"@babel/template" "7.20.7"
"@discoveryjs/json-ext" "0.5.7"
"@ngtools/webpack" "15.2.4"
"@ngtools/webpack" "15.2.8"
ansi-colors "4.1.3"
autoprefixer "10.4.13"
babel-loader "9.1.2"
@ -183,12 +183,12 @@
"@angular-devkit/architect" "0.1502.1"
rxjs "6.6.7"
"@angular-devkit/build-webpack@0.1502.4":
version "0.1502.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.4.tgz#0fa89e684692d2444d4e7996f1d2c974be2b6e2b"
integrity sha512-Bs/pxcY3517QAVyAalDxJgjc93KWQos+dFdgEQrKxj/VTs1BTYnLbb2M8Y7MoxVnfH4S+qqxGe5B57T+TlB3Eg==
"@angular-devkit/build-webpack@0.1502.8":
version "0.1502.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.8.tgz#1b375480deef1b0920e1a63d952795bd33bbfb38"
integrity sha512-jWtNv+S03FFLDe/C8SPCcRvkz3bSb2R+919IT086Q9axIPQ1VowOEwzt2k3qXPSSrC7GSYuASM+X92dB47NTQQ==
dependencies:
"@angular-devkit/architect" "0.1502.4"
"@angular-devkit/architect" "0.1502.8"
rxjs "6.6.7"
"@angular-devkit/core@15.2.1", "@angular-devkit/core@^15.0.0":
@ -202,10 +202,10 @@
rxjs "6.6.7"
source-map "0.7.4"
"@angular-devkit/core@15.2.4":
version "15.2.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.4.tgz#f7696f09c66d01568a07f0e71672e887fdf57280"
integrity sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==
"@angular-devkit/core@15.2.8":
version "15.2.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.8.tgz#ff494ae7af137b0f0109deb8ee34f1550ed5cc1d"
integrity sha512-Lo4XrbDMtXarKnMrFgWLmQdSX+3QPNAg4otG8cmp/U4jJyjV4dAYKEAsb1sCNGUSM4h4v09EQU/5ugVjDU29lQ==
dependencies:
ajv "8.12.0"
ajv-formats "2.1.1"
@ -213,12 +213,12 @@
rxjs "6.6.7"
source-map "0.7.4"
"@angular-devkit/schematics@15.2.4":
version "15.2.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.4.tgz#85129ebabcdb362f4b65a6e290bb2ae846f3d64c"
integrity sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==
"@angular-devkit/schematics@15.2.8":
version "15.2.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.8.tgz#c7dfc692e3f54e43085a8845d8c9f390a2519aa3"
integrity sha512-w6EUGC96kVsH9f8sEzajzbONMawezyVBiSo+JYp5r25rQArAz/a+KZntbuETWHQ0rQOEsKmUNKxwmr11BaptSQ==
dependencies:
"@angular-devkit/core" "15.2.4"
"@angular-devkit/core" "15.2.8"
jsonc-parser "3.2.0"
magic-string "0.29.0"
ora "5.4.1"
@ -281,31 +281,31 @@
"@angular-eslint/bundled-angular-compiler" "15.2.1"
"@typescript-eslint/utils" "5.48.2"
"@angular/animations@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.5.tgz#d42e5f185ed3d8044d38b1c938c14044aa0345a8"
integrity sha512-areHbwTrJNEYWe+1xND7SEI8fY7Pi2wckuLtbGonjtoaAc8MPs0ov56TnpXHS7bIXKVszqUlBTBAElQSdtjCyw==
"@angular/animations@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.9.tgz#f0773d2071a5a17c03478d5838029b03bbab9a03"
integrity sha512-GQujLhI0cQFcl4Q8y0oSYKSRnW23GIeSL+Arl4eFufziJ9hGAAQNuesaNs/7i+9UlTHDMkPH3kd5ScXuYYz6wg==
dependencies:
tslib "^2.3.0"
"@angular/cdk@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.5.tgz#53208ea5e654b678af011b3ef6034e1382339630"
integrity sha512-6/vA4xATcx//AULwXnRRH0Of0aaNlfrFxdlhvRVfnfmxi/hZAkjjEtil/wv8uBw+dAwXBx5yZMsAol9nUFS/BQ==
"@angular/cdk@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.9.tgz#e22df07b296fec6dccf66d569c3acc3c504c2058"
integrity sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q==
dependencies:
tslib "^2.3.0"
optionalDependencies:
parse5 "^7.1.2"
"@angular/cli@^15.2.4":
version "15.2.4"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.4.tgz#5163efb3f3e5b2a8ab0598d46d4dc200a51e7197"
integrity sha512-nQFnrt9aWwqkyhJMaZvjHXXuxeR1pZNcpZEzTA5nXtpVeNs8U7EBFCyJ+cYecFX0LHe36SoxXcbfnNEKHt3NVQ==
"@angular/cli@^15.2.8":
version "15.2.8"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.8.tgz#612ffd69591aea0109db0a6dd8faec8044a4b80d"
integrity sha512-3VlTfm6DUZfFHBY43vQSAaqmFTxy3VtRd/iDBCHcEPhHwYLWBvNwReJuJfNja8O105QQ6DBiYVBExEBtPmjQ4w==
dependencies:
"@angular-devkit/architect" "0.1502.4"
"@angular-devkit/core" "15.2.4"
"@angular-devkit/schematics" "15.2.4"
"@schematics/angular" "15.2.4"
"@angular-devkit/architect" "0.1502.8"
"@angular-devkit/core" "15.2.8"
"@angular-devkit/schematics" "15.2.8"
"@schematics/angular" "15.2.8"
"@yarnpkg/lockfile" "1.1.0"
ansi-colors "4.1.3"
ini "3.0.1"
@ -321,17 +321,17 @@
symbol-observable "4.0.0"
yargs "17.6.2"
"@angular/common@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.5.tgz#7ae960f17c260a92e939ad82fb115d908094aeda"
integrity sha512-6Wl1ak96NZvnL8p0eVsxHhaddv0/qYq2pQniKaKBfL9MVOQtAe07sPUDEZ6w0ApkmV63Giu4zFakaQMsmvxU0Q==
"@angular/common@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.9.tgz#5e1d47ce831935bcf545b172f88307aedacf1535"
integrity sha512-LM9/UHG2dRrOzlu2KovrFwWIziFMjRxHzSP3Igw6Symw/wIl0kXGq8Fn6RpFP78zmLqnv+IQOoRiby9MCXsI4g==
dependencies:
tslib "^2.3.0"
"@angular/compiler-cli@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.5.tgz#3c1882cc837a71fd0f93b880bfec6e851ec213e8"
integrity sha512-NNrnvL0JMg6dZMuTgBTb/IVIFJwy2my5XTNLngTNBfDCVu0SL1TKrYnpqp2qRi2ip9XdNqCtNF7JFDEklVbK2A==
"@angular/compiler-cli@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.9.tgz#d9e6013d6a8658e4a210aca7997e70d06f6976a8"
integrity sha512-zsbI8G2xHOeYWI0hjFzrI//ZhZV9il/uQW5dAimfwJp06KZDeXZ3PdwY9JQslf6F+saLwOObxy6QMrIVvfjy9w==
dependencies:
"@babel/core" "7.19.3"
"@jridgewell/sourcemap-codec" "^1.4.14"
@ -344,17 +344,17 @@
tslib "^2.3.0"
yargs "^17.2.1"
"@angular/compiler@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.5.tgz#4902db0699fd9ff8af15a3cd0d8be9aacef09b7f"
integrity sha512-0mAFF6Ud4ojsb8qGLQCWYh/LgKKrnn1Fz788LzfYcLYhi7UZPiCFrQJrEcCXBKtQsk8sG335CA9Qk0I/qc35zg==
"@angular/compiler@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.9.tgz#3f55e206b0e380c28336d2a233b7132f21d72644"
integrity sha512-MoKugbjk+E0wRBj12uvIyDLELlVLonnqjA2+XiF+7FxALIeyds3/qQeEoMmYIqAbN3NnTT5pV92RxWwG4tHFwA==
dependencies:
tslib "^2.3.0"
"@angular/core@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.5.tgz#761d339551a3daf97d9b9786882a5dd42ee19445"
integrity sha512-GtmjJmwtzCuF4FDXIY+9UFMAcfPlJzJDBDF7mgEmD2YKf/HV5PSyU91lfv9yDPnAkdzlDAL9u4YxnGgoURY8/g==
"@angular/core@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.9.tgz#7cb12cc83fcc92f23196ceac82e07b67b2e02203"
integrity sha512-w46Z1yUXCQfKV7XfnamOoLA2VD0MVUUYVrUjO73mHSskDXSXxfZAEHO9kfUS71Cj35PvhP3mbkqWscpea2WeYg==
dependencies:
tslib "^2.3.0"
@ -365,22 +365,22 @@
dependencies:
tslib "^2.3.0"
"@angular/forms@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.5.tgz#e40cf0bc27eae6e7ae1409ff618a3fe7cea19cfc"
integrity sha512-F4YLh0S1VyWp4IyfORTpzkZJcg7nB4q/wIirVZocR9sK3FJMEKYcAGwLbkWDYGZ66UCDw7EEqCTXkjr99/6sPg==
"@angular/forms@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.9.tgz#c3b4b0108f4eb4966ddc5a7ec9913c2ca2c94f00"
integrity sha512-sk0pC2EFi2Ohg5J0q0NYptbT+2WOkoiERSMYA39ncDvlSZBWsNlxpkbGUSck7NIxjK2QfcVN1ldGbHlZTFvtqg==
dependencies:
tslib "^2.3.0"
"@angular/language-service@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.5.tgz#fff42544e1dad609293a33ae93371a11e2df0c44"
integrity sha512-knXe4D0gc2KiU3Y3LZCRvcIkddxUMHI6/44/QAGZD3UDx6ZnpizS0eb8q8AO1pWiBTUY5ETUAlihaGWqDgQ+Wg==
"@angular/language-service@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.9.tgz#7a94e3394093a425c757f7b385b4a94edb09178a"
integrity sha512-B7lP4q/eHge2lZezOXS96EYzVf4stMCWfOnz7+pUUi0HbF+A5QCV65SWQddS/M+NM2jj8N2L/j+6UCH8lJjTQA==
"@angular/material@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/material/-/material-15.2.5.tgz#a0531b629334dfd2997dd01e549bd0c40680f4e9"
integrity sha512-Lk6l1VsBA2ActVN9YsX2W5lVFYHJEVwswbbUdBPjWaXJzp7TdgghTFczac7vsNA6y3DM3Rd+CvRsJPD2kK4g4A==
"@angular/material@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/material/-/material-15.2.9.tgz#d49f18660053bf9eae008e4738ebc65334a28491"
integrity sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ==
dependencies:
"@material/animation" "15.0.0-canary.684e33d25.0"
"@material/auto-init" "15.0.0-canary.684e33d25.0"
@ -431,24 +431,24 @@
"@material/typography" "15.0.0-canary.684e33d25.0"
tslib "^2.3.0"
"@angular/platform-browser-dynamic@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.5.tgz#f715dafd227606142295fd717a7c3fb0b67ae79f"
integrity sha512-NoGVeaR7K+RMcLpS2gI4hyMVeUqP057rw8Yfk15dHy3cM8icj5zVfyez3AADcO1XthNhE1sI1d+2LD4/GxwIKQ==
"@angular/platform-browser-dynamic@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.9.tgz#aa31ba63d535ee49fdf3a60fe771503565b4e3c9"
integrity sha512-ZIYDM6MShblb8OyV1m4+18lJJ2LCeICmeg2uSbpFYptYBSOClrTiYOOFVDJvn7HLvNzljLs16XPrgyaYVqNpcw==
dependencies:
tslib "^2.3.0"
"@angular/platform-browser@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.5.tgz#b5e1a4da920577d147f162463b2448588543a172"
integrity sha512-D19HL3UsvPX4s8yC4C4gphTNyQU75VuzcyGZ+6y1o1SQbVjF6UwSrztmb//4MCkpclb+avv669z5AEiTuBTnVQ==
"@angular/platform-browser@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.9.tgz#9150645843cc18b084fb5bf7025e6e320c2abe1e"
integrity sha512-ufCHeSX+U6d43YOMkn3igwfqtlozoCXADcbyfUEG8m2y9XASobqmCKvdSk/zfl62oyiA8msntWBJVBE2l4xKXg==
dependencies:
tslib "^2.3.0"
"@angular/router@^15.2.5":
version "15.2.5"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.5.tgz#879839960d9a3d52a20cdffc51062e5d288c3440"
integrity sha512-NSEfx2wgPCS8EdXLbKUjNCXC29cMqEGDUrSv8+P/O+tY21Szkqp6uzGTOZJuSTOoKQmDEw9CRmuwxMKm2l6y/Q==
"@angular/router@^15.2.9":
version "15.2.9"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.9.tgz#c3879be22bda236eacf97a18a1e8619b51a53d47"
integrity sha512-UCbh5DLSDhybv0xKYT7kGQMfOVdyhHIHOZz5EYVebbhste6S+W1LE57vTHq7QtxJsyKBa/WSkaUkCLXD6ntCAg==
dependencies:
tslib "^2.3.0"
@ -2619,15 +2619,15 @@
dependencies:
tslib "^2.0.0"
"@ngtools/webpack@15.2.1", "@ngtools/webpack@^15.1.6":
"@ngtools/webpack@15.2.1":
version "15.2.1"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.1.tgz#439ac075b2dcb9f304f0b7009182cc5049c7988a"
integrity sha512-YtA8rWAglPuf4CSStrFAxaprTSYE+DREGrJFc3WvZLcF5XrwVK+H4CC4Pmz07iYsG1TXShR4bWp1fbGw1cmBKw==
"@ngtools/webpack@15.2.4":
version "15.2.4"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.4.tgz#bb8afe2800e79120985657180d2b33f64ecd763d"
integrity sha512-cQ7MsRoGJgPOVnpvFgWhygeSe6zJ0ITiUhjmmuOgpNDfYkrgYxN3Ot/qvQefFei+oGZ1JJ9bRb8lcPKL/apoBQ==
"@ngtools/webpack@15.2.8":
version "15.2.8"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.8.tgz#df8fb9300ccf94cab8f8ad69fb16fd31181e6c82"
integrity sha512-BJexeT4FxMtToVBGa3wdl6rrkYXgilP0kkSH4Qzu4MPlLPbeBSr4XQalQriewlpC2uzG0r2SJfrAe2eDhtSykA==
"@ngx-translate/core@^14.0.0":
version "14.0.0"
@ -2733,13 +2733,13 @@
read-package-json-fast "^3.0.0"
which "^3.0.0"
"@schematics/angular@15.2.4":
version "15.2.4"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.4.tgz#b61b9cb3becf601e904f60490846c1d95bd63700"
integrity sha512-P9axwKDXEDrGXYcoCuhwv4guhvtIjDCvyZMsQw8IMpfAacTYajka+T3mZrzyTe96Uhu0JwcAW5iUlOZAJhPEdQ==
"@schematics/angular@15.2.8":
version "15.2.8"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.8.tgz#d845903f1cc477d299f968eb5bc40a9855cfd911"
integrity sha512-F49IEzCFxQlpaMIgTO/wF1l/CLQKif7VaiDdyiTKOeT22IMmyd61FUmWDyZYfCBqMlvBmvDGx64HaHWes1HYCg==
dependencies:
"@angular-devkit/core" "15.2.4"
"@angular-devkit/schematics" "15.2.4"
"@angular-devkit/core" "15.2.8"
"@angular-devkit/schematics" "15.2.8"
jsonc-parser "3.2.0"
"@socket.io/component-emitter@~3.1.0":

Loading…
Cancel
Save