Browse Source

Merge remote-tracking branch 'upstream/develop/3.5' into fix/alarm-assignment-user-issues

pull/8256/head
imbeacon 4 years ago
parent
commit
2bd6a29ca4
  1. 85
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  3. 1
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  4. 213
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  5. 25
      application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java
  6. 5
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  7. 52
      application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
  8. 372
      application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java
  9. 23
      application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java
  10. 3
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  11. 11
      common/cluster-api/src/main/proto/queue.proto
  12. 27
      common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java
  13. 29
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java
  14. 43
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java
  15. 5
      common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java
  16. 38
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java
  17. 8
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/DiscoveryService.java
  18. 11
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java
  19. 3
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  20. 1
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
  21. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java
  22. 32
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java
  23. 4
      common/util/pom.xml
  24. 108
      common/util/src/main/java/org/thingsboard/common/util/SystemUtil.java
  25. 2
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  26. 2
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  27. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  28. 6
      pom.xml
  29. 7
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  30. 5
      ui-ngx/src/app/core/api/widget-subscription.ts
  31. 25
      ui-ngx/src/app/core/services/utils.service.ts
  32. 4
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  33. 59
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  34. 9
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  35. 32
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
  36. 4
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
  37. 57
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  38. 234
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  39. 33
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss
  40. 16
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  41. 28
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts
  42. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  43. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html
  44. 54
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts
  45. 4
      ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html
  46. 17
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts
  47. 4
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html
  48. 63
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts
  49. 10
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.ts
  50. 20
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.ts
  51. 10
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts
  52. 27
      ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html
  53. 32
      ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts
  54. 4
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html
  55. 55
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts
  56. 4
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html
  57. 59
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts
  58. 63
      ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts
  59. 4
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  60. 8
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html
  61. 54
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts
  62. 15
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html
  63. 3
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss
  64. 2
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss
  65. 8
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  66. 2
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  67. 2
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html
  68. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html
  69. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html
  70. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.html
  71. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html
  72. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html
  73. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html
  74. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html
  75. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html
  76. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html
  77. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html
  78. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html
  79. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html
  80. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html
  81. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html
  82. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html
  83. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html
  84. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html
  85. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html
  86. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html
  87. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html
  88. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html
  89. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html
  90. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html
  91. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html
  92. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html
  93. 3
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  94. 31
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  95. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  96. 332
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  97. 33
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
  98. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  99. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss
  100. 116
      ui-ngx/src/app/shared/components/dashboard-select.component.ts

85
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -21,29 +21,41 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
@ -51,43 +63,30 @@ import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.system.SystemInfoService;
import org.thingsboard.server.service.update.UpdateService;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController extends BaseController {
@Autowired
private MailService mailService;
@Autowired
private SmsService smsService;
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private SystemSecurityService systemSecurityService;
private final MailService mailService;
private final SmsService smsService;
private final AdminSettingsService adminSettingsService;
private final SystemSecurityService systemSecurityService;
@Lazy
@Autowired
private JwtSettingsService jwtSettingsService;
private final JwtSettingsService jwtSettingsService;
@Lazy
@Autowired
private JwtTokenFactory tokenFactory;
@Autowired
private EntitiesVersionControlService versionControlService;
@Autowired
private TbAutoCommitSettingsService autoCommitSettingsService;
@Autowired
private UpdateService updateService;
private final JwtTokenFactory tokenFactory;
private final EntitiesVersionControlService versionControlService;
private final TbAutoCommitSettingsService autoCommitSettingsService;
private final UpdateService updateService;
private final SystemInfoService systemInfoService;
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@ -109,7 +108,6 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " +
"The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
@ -318,7 +316,6 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Check repository access (checkRepositoryAccess)",
notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -399,4 +396,24 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Get system info (getSystemInfo)",
notes = "Get main information about system. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/systemInfo", method = RequestMethod.GET)
@ResponseBody
public SystemInfo getSystemInfo() throws ThingsboardException {
return systemInfoService.getSystemInfo();
}
@ApiOperation(value = "Get features info (getFeaturesInfo)",
notes = "Get information about enabled/disabled features. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/featuresInfo", method = RequestMethod.GET)
@ResponseBody
public FeaturesInfo getFeaturesInfo() {
return systemInfoService.getFeaturesInfo();
}
}

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

@ -56,7 +56,7 @@ public class EntityQueryController extends BaseController {
private static final int MAX_PAGE_SIZE = 100;
@ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST)
@ResponseBody
public long countEntitiesByQuery(

1
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -95,6 +95,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
@Autowired
@Lazy
private WebSocketService wsService;
@Autowired

213
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -0,0 +1,213 @@
/**
* 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.system;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ProtocolStringList;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.SystemInfoData;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.annotation.Nullable;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.SystemUtil.getCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getFreeDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getFreeMemory;
import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
@TbCoreComponent
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultSystemInfoService extends TbApplicationEventListener<PartitionChangeEvent> implements SystemInfoService {
public static final FutureCallback<Integer> CALLBACK = new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Integer result) {
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to persist system info", t);
}
};
private final TbServiceInfoProvider serviceInfoProvider;
private final PartitionService partitionService;
private final DiscoveryService discoveryService;
private final TelemetrySubscriptionService telemetryService;
private final TbApiUsageStateClient apiUsageStateClient;
private final AdminSettingsService adminSettingsService;
private final OAuth2Service oAuth2Service;
private volatile ScheduledExecutorService scheduler;
@Override
protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
boolean myPartition = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition();
synchronized (this) {
if (myPartition) {
if (scheduler == null) {
scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-system-info-scheduler"));
scheduler.scheduleAtFixedRate(this::saveCurrentSystemInfo, 0, 1, TimeUnit.MINUTES);
}
} else {
destroy();
}
}
}
}
@Override
public SystemInfo getSystemInfo() {
SystemInfo systemInfo = new SystemInfo();
if (discoveryService.isMonolith()) {
systemInfo.setMonolith(true);
systemInfo.setSystemData(Collections.singletonList(createSystemInfoData(serviceInfoProvider.generateNewServiceInfoWithCurrentSystemInfo())));
} else {
systemInfo.setSystemData(getSystemData(serviceInfoProvider.getServiceInfo()));
}
return systemInfo;
}
protected void saveCurrentSystemInfo() {
if (discoveryService.isMonolith()) {
saveCurrentMonolithSystemInfo();
} else {
saveCurrentClusterSystemInfo();
}
}
@Override
public FeaturesInfo getFeaturesInfo() {
FeaturesInfo featuresInfo = new FeaturesInfo();
featuresInfo.setEmailEnabled(isEmailEnabled());
featuresInfo.setSmsEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null);
featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled());
featuresInfo.setTwoFaEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings") != null);
featuresInfo.setNotificationEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications") != null);
return featuresInfo;
}
private boolean isEmailEnabled() {
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (mailSettings != null) {
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
if (mailFrom != null) {
return DataValidator.doValidateEmail(mailFrom.asText());
}
}
return false;
}
private void saveCurrentClusterSystemInfo() {
long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Collections.singletonList(clusterDataKv));
}
private void saveCurrentMonolithSystemInfo() {
long ts = System.currentTimeMillis();
List<TsKvEntry> tsList = new ArrayList<>();
getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", v))));
getTotalMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalMemory", v))));
getFreeMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("freeMemory", v))));
getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new DoubleDataEntry("cpuUsage", v))));
getTotalCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new DoubleDataEntry("totalCpuUsage", v))));
getFreeDiscSpace().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("freeDiscSpace", v))));
getTotalDiscSpace().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalDiscSpace", v))));
doSave(tsList);
}
private void doSave(List<TsKvEntry> telemetry) {
ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID);
telemetryService.saveAndNotifyInternal(TenantId.SYS_TENANT_ID, apiUsageState.getId(), telemetry, CALLBACK);
}
private List<SystemInfoData> getSystemData(ServiceInfo serviceInfo) {
List<SystemInfoData> clusterSystemData = new ArrayList<>();
clusterSystemData.add(createSystemInfoData(serviceInfo));
this.discoveryService.getOtherServers()
.stream()
.map(this::createSystemInfoData)
.forEach(clusterSystemData::add);
return clusterSystemData;
}
private SystemInfoData createSystemInfoData(ServiceInfo serviceInfo) {
ProtocolStringList serviceTypes = serviceInfo.getServiceTypesList();
SystemInfoData infoData = new SystemInfoData();
infoData.setServiceId(serviceInfo.getServiceId());
infoData.setServiceType(serviceTypes.size() > 1 ? "MONOLITH" : serviceTypes.get(0));
infoData.setMemoryUsage(serviceInfo.getSystemInfo().getMemoryUsage());
infoData.setTotalMemory(serviceInfo.getSystemInfo().getTotalMemory());
infoData.setFreeMemory(serviceInfo.getSystemInfo().getFreeMemory());
infoData.setCpuUsage(serviceInfo.getSystemInfo().getCpuUsage());
infoData.setTotalCpuUsage(serviceInfo.getSystemInfo().getTotalCpuUsage());
infoData.setFreeDiscSpace(serviceInfo.getSystemInfo().getFreeDiscSpace());
infoData.setTotalDiscSpace(serviceInfo.getSystemInfo().getTotalDiscSpace());
return infoData;
}
@PreDestroy
private void destroy() {
if (scheduler != null) {
scheduler.shutdownNow();
scheduler = null;
}
}
}

25
application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java

@ -0,0 +1,25 @@
/**
* 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.system;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.SystemInfo;
public interface SystemInfoService {
SystemInfo getSystemInfo();
FeaturesInfo getFeaturesInfo();
}

5
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -73,7 +73,7 @@ public class DefaultUpdateService implements UpdateService {
@PostConstruct
private void init() {
updateMessage = new UpdateMessage("", false);
updateMessage = new UpdateMessage("", false, "");
if (updatesEnabled) {
try {
platform = System.getProperty("platform", "unknown");
@ -132,7 +132,8 @@ public class DefaultUpdateService implements UpdateService {
UpdateMessage prevUpdateMessage = updateMessage;
updateMessage = new UpdateMessage(
response.get("message").asText(),
response.get("updateAvailable").asBoolean()
response.get("updateAvailable").asBoolean(),
version
);
if (updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) {
notificationRuleProcessingService.process(TenantId.SYS_TENANT_ID, NewPlatformVersionTrigger.builder()

52
application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java

@ -92,7 +92,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
}
@Test
public void testCountEntitiesByQuery() throws Exception {
public void testTenantCountEntitiesByQuery() throws Exception {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Device device = new Device();
@ -139,6 +139,56 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Assert.assertEquals(97, count2.longValue());
}
@Test
public void testSysAdminCountEntitiesByQuery() throws Exception {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Device device = new Device();
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceNameFilter("");
loginSysAdmin();
EntityCountQuery countQuery = new EntityCountQuery(filter);
Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
filter.setDeviceType("unknown");
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(0, count.longValue());
filter.setDeviceType("default");
filter.setDeviceNameFilter("Device1");
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(11, count.longValue());
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.DEVICE);
entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList()));
countQuery = new EntityCountQuery(entityListFilter);
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
EntityTypeFilter filter2 = new EntityTypeFilter();
filter2.setEntityType(EntityType.DEVICE);
EntityCountQuery countQuery2 = new EntityCountQuery(filter2);
Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class);
Assert.assertEquals(97, count2.longValue());
}
@Test
public void testSimpleFindEntityDataByQuery() throws Exception {
List<Device> devices = new ArrayList<>();

372
application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java

@ -0,0 +1,372 @@
/**
* 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.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Info;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo;
import org.thingsboard.server.common.data.oauth2.SchemeType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.ApiUsageStateFilter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Autowired
private TbApiUsageStateClient apiUsageStateClient;
//For system administrator
@Test
public void testTenantsCountWsCmd() throws Exception {
loginSysAdmin();
List<Tenant> tenants = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Tenant tenant = new Tenant();
tenant.setTitle("tenant" + i);
tenants.add(doPost("/api/tenant", tenant, Tenant.class));
}
EntityTypeFilter ef = new EntityTypeFilter();
ef.setEntityType(EntityType.TENANT);
EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList()));
getWsClient().send(cmd);
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(101, update.getCount());
for (Tenant tenant : tenants) {
doDelete("/api/tenant/" + tenant.getId().toString());
}
}
@Test
public void testTenantProfilesCountWsCmd() throws Exception {
loginSysAdmin();
List<TenantProfile> tenantProfiles = new ArrayList<>();
for (int i = 0; i < 100; i++) {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("tenantProfile" + i);
tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class));
}
EntityTypeFilter ef = new EntityTypeFilter();
ef.setEntityType(EntityType.TENANT_PROFILE);
EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList()));
getWsClient().send(cmd);
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(101, update.getCount());
for (TenantProfile tenantProfile : tenantProfiles) {
doDelete("/api/tenantProfile/" + tenantProfile.getId().toString());
}
}
@Test
public void testUsersCountWsCmd() throws Exception {
loginSysAdmin();
List<User> users = new ArrayList<>();
for (int i = 0; i < 100; i++) {
User user = new User();
user.setEmail(i + "user@thingsboard.org");
user.setTenantId(tenantId);
user.setAuthority(Authority.TENANT_ADMIN);
users.add(doPost("/api/user", user, User.class));
}
EntityTypeFilter ef = new EntityTypeFilter();
ef.setEntityType(EntityType.USER);
EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList()));
getWsClient().send(cmd);
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(103, update.getCount());
for (User user : users) {
doDelete("/api/user/" + user.getId().toString());
}
}
@Test
public void testCustomersCountWsCmd() throws Exception {
loginTenantAdmin();
List<Customer> customers = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Customer customer = new Customer();
customer.setTitle("customer" + i);
customers.add(doPost("/api/customer", customer, Customer.class));
}
loginSysAdmin();
EntityTypeFilter ef = new EntityTypeFilter();
ef.setEntityType(EntityType.CUSTOMER);
EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList()));
getWsClient().send(cmd);
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(101, update.getCount());
loginTenantAdmin();
for (Customer customer : customers) {
doDelete("/api/customer/" + customer.getId().toString());
}
}
@Test
public void testDevicesCountWsCmd() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Device device = new Device();
device.setName("device" + i);
devices.add(doPost("/api/device", device, Device.class));
}
loginSysAdmin();
EntityTypeFilter ef = new EntityTypeFilter();
ef.setEntityType(EntityType.DEVICE);
EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList()));
getWsClient().send(cmd);
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(100, update.getCount());
loginTenantAdmin();
for (Device device : devices) {
doDelete("/api/device/" + device.getId().toString());
}
}
@Test
public void testAssetsCountWsCmd() throws Exception {
loginTenantAdmin();
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Asset asset = new Asset();
asset.setName("asset" + i);
assets.add(doPost("/api/asset", asset, Asset.class));
}
loginSysAdmin();
EntityTypeFilter ef = new EntityTypeFilter();
ef.setEntityType(EntityType.ASSET);
EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList()));
getWsClient().send(cmd);
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(100, update.getCount());
loginTenantAdmin();
for (Asset asset : assets) {
doDelete("/api/asset/" + asset.getId().toString());
}
}
@Test
public void testSystemInfoTimeSeriesWsCmd() throws Exception {
ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID);
Assert.assertNotNull(apiUsageState);
loginSysAdmin();
long now = System.currentTimeMillis();
EntityDataUpdate update = getWsClient().sendEntityDataQuery(new ApiUsageStateFilter());
Assert.assertEquals(1, update.getCmdId());
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(apiUsageState.getId(), pageData.getData().get(0).getEntityId());
update = getWsClient().subscribeTsUpdate(
List.of("memoryUsage", "totalMemory", "freeMemory", "cpuUsage", "totalCpuUsage", "freeDiscSpace", "totalDiscSpace"),
now, TimeUnit.HOURS.toMillis(1));
Assert.assertEquals(1, update.getCmdId());
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(apiUsageState.getId(), listData.get(0).getEntityId());
Assert.assertEquals(7, listData.get(0).getTimeseries().size());
for (TsValue[] tsv : listData.get(0).getTimeseries().values()) {
Assert.assertTrue(tsv.length > 0);
}
}
@Test
public void testGetFeaturesInfo() throws Exception {
loginSysAdmin();
FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
Assert.assertNotNull(featuresInfo);
Assert.assertFalse(featuresInfo.isEmailEnabled());
Assert.assertFalse(featuresInfo.isSmsEnabled());
Assert.assertFalse(featuresInfo.isTwoFaEnabled());
Assert.assertFalse(featuresInfo.isNotificationEnabled());
Assert.assertFalse(featuresInfo.isOauthEnabled());
AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
JsonNode jsonValue = mailSettings.getJsonValue();
((ObjectNode) jsonValue).put("mailFrom", "test@thingsboard.org");
mailSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", mailSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
Assert.assertTrue(featuresInfo.isEmailEnabled());
Assert.assertFalse(featuresInfo.isSmsEnabled());
Assert.assertFalse(featuresInfo.isTwoFaEnabled());
Assert.assertFalse(featuresInfo.isNotificationEnabled());
Assert.assertFalse(featuresInfo.isOauthEnabled());
AdminSettings smsSettings = new AdminSettings();
smsSettings.setKey("sms");
smsSettings.setJsonValue(JacksonUtil.newObjectNode());
doPost("/api/admin/settings", smsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
Assert.assertTrue(featuresInfo.isEmailEnabled());
Assert.assertTrue(featuresInfo.isSmsEnabled());
Assert.assertFalse(featuresInfo.isTwoFaEnabled());
Assert.assertFalse(featuresInfo.isNotificationEnabled());
Assert.assertFalse(featuresInfo.isOauthEnabled());
AdminSettings twoFaSettingsSettings = new AdminSettings();
twoFaSettingsSettings.setKey("twoFaSettings");
twoFaSettingsSettings.setJsonValue(JacksonUtil.newObjectNode());
doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
Assert.assertTrue(featuresInfo.isEmailEnabled());
Assert.assertTrue(featuresInfo.isSmsEnabled());
Assert.assertTrue(featuresInfo.isTwoFaEnabled());
Assert.assertFalse(featuresInfo.isNotificationEnabled());
Assert.assertFalse(featuresInfo.isOauthEnabled());
AdminSettings notificationsSettings = new AdminSettings();
notificationsSettings.setKey("notifications");
notificationsSettings.setJsonValue(JacksonUtil.newObjectNode());
doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
Assert.assertTrue(featuresInfo.isEmailEnabled());
Assert.assertTrue(featuresInfo.isSmsEnabled());
Assert.assertTrue(featuresInfo.isTwoFaEnabled());
Assert.assertTrue(featuresInfo.isNotificationEnabled());
Assert.assertFalse(featuresInfo.isOauthEnabled());
OAuth2Info oAuth2Info = createDefaultOAuth2Info();
doPost("/api/oauth2/config", oAuth2Info).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
Assert.assertNotNull(featuresInfo);
Assert.assertTrue(featuresInfo.isEmailEnabled());
Assert.assertTrue(featuresInfo.isSmsEnabled());
Assert.assertTrue(featuresInfo.isTwoFaEnabled());
Assert.assertTrue(featuresInfo.isNotificationEnabled());
Assert.assertTrue(featuresInfo.isOauthEnabled());
}
private OAuth2Info createDefaultOAuth2Info() {
return new OAuth2Info(true, Lists.newArrayList(
OAuth2ParamsInfo.builder()
.domainInfos(Lists.newArrayList(
OAuth2DomainInfo.builder().name("domain").scheme(SchemeType.MIXED).build()
))
.mobileInfos(Collections.emptyList())
.clientRegistrations(Lists.newArrayList(
validRegistrationInfo()
))
.build()
));
}
private OAuth2RegistrationInfo validRegistrationInfo() {
return OAuth2RegistrationInfo.builder()
.clientId(UUID.randomUUID().toString())
.clientSecret(UUID.randomUUID().toString())
.authorizationUri(UUID.randomUUID().toString())
.accessTokenUri(UUID.randomUUID().toString())
.scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString()))
.platforms(Collections.emptyList())
.userInfoUri(UUID.randomUUID().toString())
.userNameAttributeName(UUID.randomUUID().toString())
.jwkSetUri(UUID.randomUUID().toString())
.clientAuthenticationMethod(UUID.randomUUID().toString())
.loginButtonLabel(UUID.randomUUID().toString())
.mapperConfig(
OAuth2MapperConfig.builder()
.type(MapperType.CUSTOM)
.custom(
OAuth2CustomMapperConfig.builder()
.url(UUID.randomUUID().toString())
.build()
)
.build()
)
.build();
}
}

23
application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java

@ -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.
*/
package org.thingsboard.server.controller.sql;
import org.thingsboard.server.controller.BaseHomePageApiTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class HomePageApiSqlTest extends BaseHomePageApiTest {
}

3
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.assertj.core.data.Offset;
import org.java_websocket.client.WebSocketClient;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.rule.engine.api.NotificationCenter;
@ -476,6 +477,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
}
@Test
@Ignore
public void testNotificationsForALotOfUsers() throws Exception {
int usersCount = 5000;
@ -521,6 +523,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
}
@Test
@Ignore
public void testSlackNotifications() throws Exception {
NotificationSettings settings = new NotificationSettings();
SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig();

11
common/cluster-api/src/main/proto/queue.proto

@ -27,6 +27,17 @@ message ServiceInfo {
string serviceId = 1;
repeated string serviceTypes = 2;
repeated string transports = 6;
SystemInfoProto systemInfo = 10;
}
message SystemInfoProto {
double cpuUsage = 1;
double totalCpuUsage = 2;
int64 memoryUsage = 3;
int64 totalMemory = 4;
int64 freeMemory = 5;
int64 freeDiscSpace = 6;
int64 totalDiscSpace = 7;
}
/**

27
common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java

@ -0,0 +1,27 @@
/**
* 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;
import lombok.Data;
@Data
public class FeaturesInfo {
boolean isEmailEnabled;
boolean isSmsEnabled;
boolean isNotificationEnabled;
boolean isOauthEnabled;
boolean isTwoFaEnabled;
}

29
common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java

@ -0,0 +1,29 @@
/**
* 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;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class SystemInfo {
@ApiModelProperty(position = 1, value = "Is monolith.")
private boolean isMonolith;
@ApiModelProperty(position = 2, value = "System data.")
private List<SystemInfoData> systemData;
}

43
common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java

@ -0,0 +1,43 @@
/**
* 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;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Map;
@Data
public class SystemInfoData {
@ApiModelProperty(position = 1, value = "Service Id.")
private String serviceId;
@ApiModelProperty(position = 2, value = "Service type.")
private String serviceType;
@ApiModelProperty(position = 3, value = "CPU usage.")
private Double cpuUsage;
@ApiModelProperty(position = 4, value = "Total CPU usage.")
private Double totalCpuUsage;
@ApiModelProperty(position = 5, value = "Memory usage in bytes.")
private Long memoryUsage;
@ApiModelProperty(position = 6, value = "Total memory in bytes.")
private Long totalMemory;
@ApiModelProperty(position = 6, value = "Free memory in bytes.")
private Long freeMemory;
@ApiModelProperty(position = 7, value = "Free disc space in bytes.")
private Long freeDiscSpace;
@ApiModelProperty(position = 7, value = "Total disc space in bytes.")
private Long totalDiscSpace;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java

@ -25,7 +25,8 @@ public class UpdateMessage {
@ApiModelProperty(position = 1, value = "The message about new platform update available.")
private final String message;
@ApiModelProperty(position = 1, value = "'True' if new platform update is available.")
@ApiModelProperty(position = 2, value = "'True' if new platform update is available.")
private final boolean isUpdateAvailable;
@ApiModelProperty(position = 3, value = "Current ThingsBoard version.")
private final String currentVersion;
}

38
common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java

@ -24,6 +24,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbTransportService;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
import org.thingsboard.server.queue.util.AfterContextReady;
@ -36,6 +37,14 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.SystemUtil.getCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getFreeDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getFreeMemory;
import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
@Component
@Slf4j
public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider {
@ -69,11 +78,8 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider {
} else {
serviceTypes = Collections.singletonList(ServiceType.of(serviceType));
}
ServiceInfo.Builder builder = ServiceInfo.newBuilder()
.setServiceId(serviceId)
.addAllServiceTypes(serviceTypes.stream().map(ServiceType::name).collect(Collectors.toList()));
serviceInfo = builder.build();
generateNewServiceInfoWithCurrentSystemInfo();
}
@AfterContextReady
@ -99,4 +105,28 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider {
return serviceTypes.contains(serviceType);
}
@Override
public ServiceInfo generateNewServiceInfoWithCurrentSystemInfo() {
ServiceInfo.Builder builder = ServiceInfo.newBuilder()
.setServiceId(serviceId)
.addAllServiceTypes(serviceTypes.stream().map(ServiceType::name).collect(Collectors.toList()))
.setSystemInfo(getCurrentSystemInfoProto());
return serviceInfo = builder.build();
}
private TransportProtos.SystemInfoProto getCurrentSystemInfoProto() {
TransportProtos.SystemInfoProto.Builder builder = TransportProtos.SystemInfoProto.newBuilder();
getMemoryUsage().ifPresent(builder::setMemoryUsage);
getTotalMemory().ifPresent(builder::setTotalMemory);
getFreeMemory().ifPresent(builder::setFreeMemory);
getCpuUsage().ifPresent(builder::setCpuUsage);
getTotalCpuUsage().ifPresent(builder::setTotalCpuUsage);
getFreeDiscSpace().ifPresent(builder::setFreeDiscSpace);
getTotalDiscSpace().ifPresent(builder::setTotalDiscSpace);
return builder.build();
}
}

8
common/queue/src/main/java/org/thingsboard/server/queue/discovery/DiscoveryService.java

@ -15,6 +15,14 @@
*/
package org.thingsboard.server.queue.discovery;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.List;
public interface DiscoveryService {
List<TransportProtos.ServiceInfo> getOtherServers();
boolean isMonolith();
}

11
common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java

@ -22,9 +22,11 @@ import org.springframework.context.annotation.DependsOn;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.AfterStartUp;
import java.util.Collections;
import java.util.List;
@Service
@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "false", matchIfMissing = true)
@ -46,4 +48,13 @@ public class DummyDiscoveryService implements DiscoveryService {
partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), Collections.emptyList());
}
@Override
public List<TransportProtos.ServiceInfo> getOtherServers() {
return Collections.emptyList();
}
@Override
public boolean isMonolith() {
return true;
}
}

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

@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -217,7 +216,7 @@ public class HashPartitionService implements PartitionService {
}
queueServicesMap.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId)));
final ConcurrentMap<QueueKey, List<Integer>> newPartitions = new ConcurrentHashMap<>();
final ConcurrentMap<QueueKey, List<Integer>> newPartitions = new ConcurrentHashMap<>();
partitionSizesMap.forEach((queueKey, size) -> {
for (int i = 0; i < size; i++) {
ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i);

1
common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.queue.discovery;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java

@ -28,4 +28,6 @@ public interface TbServiceInfoProvider {
boolean isService(ServiceType serviceType);
ServiceInfo generateNewServiceInfoWithCurrentSystemInfo();
}

32
common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.queue.discovery;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
@ -33,21 +34,19 @@ import org.apache.zookeeper.KeeperException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_REMOVED;
@ -71,7 +70,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
private final TbServiceInfoProvider serviceInfoProvider;
private final PartitionService partitionService;
private ExecutorService reconnectExecutorService;
private ScheduledExecutorService zkExecutorService;
private CuratorFramework client;
private PathChildrenCache cache;
private String nodePath;
@ -93,7 +92,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
Assert.notNull(zkConnectionTimeout, missingProperty("zk.connection_timeout_ms"));
Assert.notNull(zkSessionTimeout, missingProperty("zk.session_timeout_ms"));
reconnectExecutorService = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("zk-discovery"));
zkExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("zk-discovery"));
log.info("Initializing discovery service using ZK connect string: {}", zkUrl);
@ -101,7 +100,8 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
initZkClient();
}
private List<TransportProtos.ServiceInfo> getOtherServers() {
@Override
public List<TransportProtos.ServiceInfo> getOtherServers() {
return cache.getCurrentData().stream()
.filter(cd -> !cd.getPath().equals(nodePath))
.map(cd -> {
@ -115,6 +115,11 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
.collect(Collectors.toList());
}
@Override
public boolean isMonolith() {
return false;
}
@AfterStartUp(order = AfterStartUp.DISCOVERY_SERVICE)
public void onApplicationEvent(ApplicationReadyEvent event) {
if (stopped) {
@ -128,15 +133,17 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
return;
}
log.info("Going to publish current server...");
publishCurrentServer();
zkExecutorService.scheduleAtFixedRate(this::publishCurrentServer, 0, 1, TimeUnit.MINUTES);
log.info("Going to recalculate partitions...");
recalculatePartitions();
}
@SneakyThrows
public synchronized void publishCurrentServer() {
TransportProtos.ServiceInfo self = serviceInfoProvider.getServiceInfo();
if (currentServerExists()) {
log.info("[{}] ZK node for current instance already exists, NOT created new one: {}", self.getServiceId(), nodePath);
log.trace("[{}] Updating ZK node for current instance: {}", self.getServiceId(), nodePath);
client.setData().forPath(nodePath, serviceInfoProvider.generateNewServiceInfoWithCurrentSystemInfo().toByteArray());
} else {
try {
log.info("[{}] Creating ZK node for current instance", self.getServiceId());
@ -175,7 +182,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
return (client, newState) -> {
log.info("[{}] ZK state changed: {}", self.getServiceId(), newState);
if (newState == ConnectionState.LOST) {
reconnectExecutorService.submit(this::reconnect);
zkExecutorService.submit(this::reconnect);
}
};
}
@ -240,7 +247,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
@PreDestroy
public void destroy() {
destroyZkClient();
reconnectExecutorService.shutdownNow();
zkExecutorService.shutdownNow();
log.info("Stopped discovery service");
}
@ -280,10 +287,9 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
log.error("Failed to decode server instance for node {}", data.getPath(), e);
throw e;
}
log.info("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), instance.getServiceId());
log.debug("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), instance.getServiceId());
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
case CHILD_UPDATED:
case CHILD_REMOVED:
recalculatePartitions();
break;

4
common/util/pom.xml

@ -88,6 +88,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
</dependency>
<dependency>
<groupId>com.github.dblock</groupId>
<artifactId>oshi-core</artifactId>
</dependency>
</dependencies>
<build>

108
common/util/src/main/java/org/thingsboard/common/util/SystemUtil.java

@ -0,0 +1,108 @@
/**
* 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.common.util;
import lombok.extern.slf4j.Slf4j;
import oshi.SystemInfo;
import oshi.hardware.HardwareAbstractionLayer;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Optional;
@Slf4j
public class SystemUtil {
private static final HardwareAbstractionLayer HARDWARE;
static {
SystemInfo si = new SystemInfo();
HARDWARE = si.getHardware();
}
public static Optional<Long> getMemoryUsage() {
try {
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
return Optional.of(memoryMXBean.getHeapMemoryUsage().getUsed());
} catch (Exception e) {
log.debug("Failed to get memory usage!!!", e);
}
return Optional.empty();
}
public static Optional<Long> getTotalMemory() {
try {
return Optional.of(HARDWARE.getMemory().getTotal());
} catch (Exception e) {
log.debug("Failed to get total memory!!!", e);
}
return Optional.empty();
}
public static Optional<Long> getFreeMemory() {
try {
return Optional.of(HARDWARE.getMemory().getAvailable());
} catch (Exception e) {
log.debug("Failed to get free memory!!!", e);
}
return Optional.empty();
}
public static Optional<Double> getCpuUsage() {
try {
return Optional.of(prepare(HARDWARE.getProcessor().getSystemLoadAverage()));
} catch (Exception e) {
log.debug("Failed to get cpu usage!!!", e);
}
return Optional.empty();
}
public static Optional<Double> getTotalCpuUsage() {
try {
return Optional.of(prepare(HARDWARE.getProcessor().getSystemCpuLoad() * 100));
} catch (Exception e) {
log.debug("Failed to get total cpu usage!!!", e);
}
return Optional.empty();
}
public static Optional<Long> getFreeDiscSpace() {
try {
FileStore store = Files.getFileStore(Paths.get("/"));
return Optional.of(store.getUsableSpace());
} catch (Exception e) {
log.debug("Failed to get free disc space!!!", e);
}
return Optional.empty();
}
public static Optional<Long> getTotalDiscSpace() {
try {
FileStore store = Files.getFileStore(Paths.get("/"));
return Optional.of(store.getTotalSpace());
} catch (Exception e) {
log.debug("Failed to get total disc space!!!", e);
}
return Optional.empty();
}
private static Double prepare(Double d) {
return (int) (d * 100) / 100.0;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -25,10 +25,10 @@ import org.thingsboard.server.common.data.HasLabel;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTitle;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityCountQuery;

2
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -92,7 +92,7 @@ public abstract class DataValidator<D extends BaseData<?>> {
}
}
private static boolean doValidateEmail(String email) {
public static boolean doValidateEmail(String email) {
if (email == null) {
return false;
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -241,6 +241,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
entityTableMap.put(EntityType.RULE_CHAIN, "rule_chain");
entityTableMap.put(EntityType.DEVICE_PROFILE, "device_profile");
entityTableMap.put(EntityType.ASSET_PROFILE, "asset_profile");
entityTableMap.put(EntityType.TENANT_PROFILE, "tenant_profile");
}
public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{
@ -311,12 +312,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
@Override
public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) {
EntityType entityType = resolveEntityType(query.getEntityFilter());
QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType));
QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType, TenantId.SYS_TENANT_ID.equals(tenantId)));
if (query.getKeyFilters() == null || query.getKeyFilters().isEmpty()) {
ctx.append("select count(e.id) from ");
ctx.append(addEntityTableQuery(ctx, query.getEntityFilter()));
ctx.append(" e where ");
ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList()));
return transactionTemplate.execute(status -> {
long startTs = System.currentTimeMillis();
try {
@ -514,7 +516,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
}
private String buildPermissionQuery(QueryContext ctx, EntityFilter entityFilter) {
if(ctx.isIgnorePermissionCheck()){
if (ctx.isIgnorePermissionCheck()) {
return "1=1";
}
switch (entityFilter.getType()) {

6
pom.xml

@ -150,6 +150,7 @@
<allure-testng.version>2.21.0</allure-testng.version>
<allure-maven.version>2.12.0</allure-maven.version>
<slack-api.version>1.12.1</slack-api.version>
<oshi.version>3.4.0</oshi.version>
</properties>
<modules>
@ -1994,6 +1995,11 @@
<artifactId>exp4j</artifactId>
<version>${exp4j.version}</version>
</dependency>
<dependency>
<groupId>com.github.dblock</groupId>
<artifactId>oshi-core</artifactId>
<version>${oshi.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

7
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -32,7 +32,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.thingsboard.server.common.data.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -56,6 +55,8 @@ import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
@ -395,6 +396,10 @@ public class RestClient implements Closeable {
}
}
public SystemInfo getSystemInfo() {
return restTemplate.getForEntity(baseURL + "/api/admin/systemInfo", SystemInfo.class).getBody();
}
public Optional<Alarm> getAlarmById(AlarmId alarmId) {
try {
ResponseEntity<Alarm> alarm = restTemplate.getForEntity(baseURL + "/api/alarm/{alarmId}", Alarm.class, alarmId.getId());

5
ui-ngx/src/app/core/api/widget-subscription.ts

@ -582,12 +582,9 @@ export class WidgetSubscription implements IWidgetSubscription {
const data = this.alarms.data[0];
entityId = data.originator;
entityName = data.originatorName;
entityLabel = data.originatorLabel;
if (data.latest && data.latest[EntityKeyType.ENTITY_FIELD]) {
const entityFields = data.latest[EntityKeyType.ENTITY_FIELD];
const labelValue = entityFields.label;
if (labelValue) {
entityLabel = labelValue.value;
}
const additionalInfoValue = entityFields.additionalInfo;
if (additionalInfoValue) {
const additionalInfo = additionalInfoValue.value;

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

@ -29,7 +29,11 @@ import {
isDefined,
isDefinedAndNotNull,
isString,
isUndefined
isUndefined,
objToBase64,
objToBase64URI,
base64toString,
base64toObj
} from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
@ -87,7 +91,7 @@ const commonMaterialIcons: Array<string> = ['more_horiz', 'more_vert', 'open_in_
'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
'share', 'add', 'edit', 'done'];
'share', 'add', 'edit', 'done', 'delete'];
// @dynamic
@Injectable({
@ -518,4 +522,21 @@ export class UtilsService {
refCount()
);
}
public objToBase64(obj: any): string {
return objToBase64(obj);
}
public base64toString(b64Encoded: string): string {
return base64toString(b64Encoded);
}
public objToBase64URI(obj: any): string {
return objToBase64URI(obj);
}
public base64toObj(b64Encoded: string): any {
return base64toObj(b64Encoded);
}
}

4
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html

@ -42,7 +42,7 @@
<div class="predicate-list">
<div fxLayout="row" fxLayoutAlign="start" style="height: 71px;"
formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray().controls; let $index = index">
*ngFor="let predicateControl of predicatesFormArray.controls; let $index = index">
<div fxFlex="8" fxLayout="row" fxLayoutAlign="center" class="filters-operation">
<span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
@ -68,7 +68,7 @@
</div>
</div>
</div>
<span [fxShow]="!predicatesFormArray().length"
<span [fxShow]="!predicatesFormArray.length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-filters</span>
</div>

59
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Inject, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -27,7 +27,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs';
import { Observable, of, Subject } from 'rxjs';
import {
ComplexFilterPredicateInfo,
ComplexOperation,
@ -37,7 +37,7 @@ import {
KeyFilterPredicateInfo
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators';
import { map, takeUntil } from 'rxjs/operators';
import { ComponentType } from '@angular/cdk/portal';
import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens';
import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models';
@ -59,7 +59,7 @@ import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter
}
]
})
export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit {
export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@Input() disabled: boolean;
@ -81,22 +81,29 @@ export class FilterPredicateListComponent implements ControlValueAccessor, Valid
complexOperationTranslations = complexOperationTranslationMap;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(private fb: UntypedFormBuilder,
@Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType<any>,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.filterListFormGroup = this.fb.group({});
this.filterListFormGroup.addControl('predicates',
this.fb.array([]));
this.filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
predicatesFormArray(): UntypedFormArray {
get predicatesFormArray(): UntypedFormArray {
return this.filterListFormGroup.get('predicates') as UntypedFormArray;
}
@ -123,28 +130,26 @@ export class FilterPredicateListComponent implements ControlValueAccessor, Valid
}
writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const predicateControls: Array<AbstractControl> = [];
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls));
this.valueChangeSubscription = this.filterListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
if (predicates.length === this.predicatesFormArray.length) {
this.predicatesFormArray.patchValue(predicates, {emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
const predicateControls: Array<AbstractControl> = [];
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls), {emitEvent: false});
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
}
}
}
public removePredicate(index: number) {
(this.filterListFormGroup.get('predicates') as UntypedFormArray).removeAt(index);
this.predicatesFormArray.removeAt(index);
}
public addPredicate(complex: boolean) {

9
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -64,6 +64,15 @@
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 48px;"
type="button"
(click)="duplicateFilter($index)"
matTooltip="{{ 'filter.duplicate-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>content_copy</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"

32
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts

@ -64,6 +64,8 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
filterToWidgetsMap: {[filterId: string]: Array<string>} = {};
filterNames: Set<string> = new Set<string>();
filtersFormGroup: UntypedFormGroup;
submitted = false;
@ -112,6 +114,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
if (isUndefined(filter.editable)) {
filter.editable = true;
}
this.filterNames.add(filter.filter);
filterControls.push(this.createFilterFormControl(filterId, filter));
}
@ -158,10 +161,37 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
message, this.translate.instant('action.close'), true);
} else {
(this.filtersFormGroup.get('filters') as UntypedFormArray).removeAt(index);
this.filterNames.delete(filter.filter);
this.filtersFormGroup.markAsDirty();
}
}
private getNextDuplicatedName(filterName: string): string {
const suffix = ` - ${this.translate.instant('action.copy')} `;
let counter = 0;
while (++counter < Number.MAX_SAFE_INTEGER) {
const newName = `${filterName}${suffix}${counter}`;
if (!this.filterNames.has(newName)) {
return newName;
}
}
return null;
}
duplicateFilter(index: number) {
const originalFilter = (this.filtersFormGroup.get('filters').value as any[])[index];
const newFilterName = this.getNextDuplicatedName(originalFilter.filter);
if (newFilterName) {
const duplicatedFilter = deepClone(originalFilter);
duplicatedFilter.id = this.utils.guid();
duplicatedFilter.filter = newFilterName;
(this.filtersFormGroup.get('filters') as UntypedFormArray).
insert(index + 1, this.createFilterFormControl(duplicatedFilter.id, duplicatedFilter));
this.filterNames.add(duplicatedFilter.filter);
}
}
public addFilter() {
this.openFilterDialog(-1);
}
@ -176,6 +206,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
const filtersArray = this.filtersFormGroup.get('filters').value as any[];
if (!isAdd) {
filter = filtersArray[index];
this.filterNames.delete(filter.filter);
}
this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
@ -197,6 +228,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
filterFormControl.get('editable').patchValue(result.editable);
filterFormControl.get('keyFilters').patchValue(result.keyFilters);
}
this.filterNames.add(result.filter);
this.filtersFormGroup.markAsDirty();
}
});

4
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html

@ -36,7 +36,7 @@
<div class="key-filter-list">
<div fxLayout="row" fxLayoutAlign="start" style="max-height: 71px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
*ngFor="let keyFilterControl of keyFiltersFormArray.controls; let $index = index">
<div fxFlex="8" class="filters-operation">
<span *ngIf="$index > 0" translate>filter.operation.and</span>
</div>
@ -63,7 +63,7 @@
<mat-divider></mat-divider>
</div>
</div>
<span [fxShow]="!keyFiltersFormArray().length"
<span [fxShow]="!keyFiltersFormArray.length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-key-filters</span>
</div>

57
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -28,7 +28,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import {
EntityKeyType,
entityKeyTypeTranslationMap,
@ -39,6 +39,7 @@ import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-key-filter-list',
@ -57,7 +58,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}
]
})
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@Input() disabled: boolean;
@ -75,22 +76,30 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
keyFiltersControl: UntypedFormControl;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.keyFilterListFormGroup = this.fb.group({});
this.keyFilterListFormGroup.addControl('keyFilters',
this.fb.array([]));
this.keyFilterListFormGroup = this.fb.group({
keyFilters: this.fb.array([])
});
this.keyFiltersControl = this.fb.control(null);
this.keyFilterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
keyFiltersFormArray(): UntypedFormArray {
get keyFiltersFormArray(): UntypedFormArray {
return this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray;
}
@ -119,23 +128,21 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
}
writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const keyFilterControls: Array<AbstractControl> = [];
if (keyFilters) {
for (const keyFilter of keyFilters) {
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required]));
}
}
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls));
this.valueChangeSubscription = this.keyFilterListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
if (keyFilters.length === this.keyFiltersFormArray.length) {
this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
const keyFilterControls: Array<AbstractControl> = [];
if (keyFilters) {
for (const keyFilter of keyFilters) {
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required]));
}
}
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls), {emitEvent: false});
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
}
}
const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});

234
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -15,124 +15,128 @@
limitations under the License.
-->
<div>
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
<div [tb-help]="'deviceProfiles'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" #addDeviceProfileStepper (selectionChange)="changeStep($event)">
<mat-step [stepControl]="deviceProfileDetailsFormGroup">
<form [formGroup]="deviceProfileDetailsFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('maxlength')">
{{ 'device-profile.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-rule-chain-autocomplete
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
label="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<span tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</span>
</tb-dashboard-autocomplete>
<tb-queue-autocomplete
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="device-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<span tb-hint>{{'device-profile.default-edge-rule-chain-hint' | translate}}</span>
</tb-rule-chain-autocomplete>
<mat-form-field fxHide class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
label="{{'device-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
<div [tb-help]="'deviceProfiles'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true"
#addDeviceProfileStepper
[labelPosition]="(stepperLabelPosition | async)"
[orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<mat-step [stepControl]="deviceProfileDetailsFormGroup">
<form [formGroup]="deviceProfileDetailsFormGroup">
<ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('maxlength')">
{{ 'device-profile.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-rule-chain-autocomplete
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
label="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<span tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</span>
</tb-dashboard-autocomplete>
<tb-queue-autocomplete
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="device-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<span tb-hint>{{'device-profile.default-edge-rule-chain-hint' | translate}}</span>
</tb-rule-chain-autocomplete>
<mat-form-field fxHide class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="row">
<tb-image-input fxFlex
label="{{'device-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions style="padding: 0">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -143,8 +147,8 @@
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<mat-divider style="width: 100%"></mat-divider>
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end center">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>

33
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss

@ -15,6 +15,15 @@
*/
@import "../../../../../scss/constants";
:host {
height: 100%;
display: grid;
.dialog-actions-row {
padding: 8px;
}
}
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) {
@media #{$mat-lt-sm} {
.mat-mdc-dialog-content {
@ -32,31 +41,21 @@
.mat-stepper-horizontal {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
@media #{$mat-lt-sm} {
.mat-step-label {
white-space: normal;
overflow: visible;
.mat-step-text-label {
overflow: visible;
}
}
.mat-horizontal-stepper-wrapper {
flex: 1 1 100%;
}
.mat-horizontal-content-container {
height: 530px;
height: 680px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
scrollbar-gutter: stable;
@media #{$mat-gt-sm} {
min-width: 800px;
}
}
.mat-horizontal-stepper-content[aria-expanded=true] {
height: 100%;
form {
height: 100%;
min-width: 500px;
}
}
}

16
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts

@ -44,13 +44,17 @@ import {
} from '@shared/models/device.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { EntityType } from '@shared/models/entity-type.models';
import { MatStepper } from '@angular/material/stepper';
import { MatStepper, StepperOrientation } from '@angular/material/stepper';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { deepTrim } from '@core/utils';
import { ServiceType } from '@shared/models/queue.models';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { Observable } from 'rxjs';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { map } from 'rxjs/operators';
export interface AddDeviceProfileDialogData {
deviceProfileName: string;
@ -67,7 +71,8 @@ export class AddDeviceProfileDialogComponent extends
DialogComponent<AddDeviceProfileDialogComponent, DeviceProfile> implements AfterViewInit {
@ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatStepper;
stepperOrientation: Observable<StepperOrientation>;
stepperLabelPosition: Observable<'bottom' | 'end'>;
selectedIndex = 0;
showNext = true;
@ -102,10 +107,17 @@ export class AddDeviceProfileDialogComponent extends
public dialogRef: MatDialogRef<AddDeviceProfileDialogComponent, DeviceProfile>,
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
private breakpointObserver: BreakpointObserver,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private deviceProfileService: DeviceProfileService,
private fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-md'])
.pipe(map(({matches}) => matches ? 'end' : 'bottom'));
this.deviceProfileDetailsFormGroup = this.fb.group(
{
name: [data.deviceProfileName, [Validators.required, Validators.maxLength(255)]],

28
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -23,15 +23,16 @@ import {
UntypedFormControl,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, ValidationErrors,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { DeviceProfileAlarmRule, alarmRuleValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-create-alarm-rules',
@ -50,7 +51,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}
]
})
export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator {
export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
@ -67,8 +68,7 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
private usedSeverities: AlarmSeverity[] = [];
private valueChangeSubscription: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
@ -86,6 +86,14 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
this.createAlarmRulesFormGroup = this.fb.group({
createAlarmRules: this.fb.array([])
});
this.createAlarmRulesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
createAlarmRulesFormArray(): UntypedFormArray {
@ -102,9 +110,6 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}
writeValue(createAlarmRules: {[severity: string]: DeviceProfileAlarmRule}): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const createAlarmRulesControls: Array<AbstractControl> = [];
if (createAlarmRules) {
Object.keys(createAlarmRules).forEach((severity) => {
@ -118,15 +123,12 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}));
});
}
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls));
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls), {emitEvent: false});
if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else {
this.createAlarmRulesFormGroup.enable({emitEvent: false});
}
this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedSeverities();
if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
this.updateModel();

8
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -59,10 +59,10 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
<mat-checkbox formControlName="propagate" style="display: block;">
{{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox>
<section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
<section *ngIf="alarmFormGroup.get('propagate').value === true">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList [disabled]="disabled">
@ -82,10 +82,10 @@
<mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
</section>
<mat-checkbox formControlName="propagateToOwner" style="display: block; padding-bottom: 16px;">
<mat-checkbox formControlName="propagateToOwner" style="display: block;">
{{ 'device-profile.propagate-alarm-to-owner' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="propagateToTenant" style="display: block; padding-bottom: 16px;">
<mat-checkbox formControlName="propagateToTenant" style="display: block;">
{{ 'device-profile.propagate-alarm-to-tenant' | translate }}
</mat-checkbox>
</ng-template>

4
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html

@ -17,7 +17,7 @@
-->
<div fxLayout="column">
<div class="tb-device-profile-alarms">
<div *ngFor="let alarmControl of alarmsFormArray().controls; trackBy: trackByAlarm;
<div *ngFor="let alarmControl of alarmsFormArray.controls; trackBy: trackByAlarm;
let $index = index; last as isLast;"
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-device-profile-alarm [formControl]="alarmControl"
@ -27,7 +27,7 @@
</tb-device-profile-alarm>
</div>
</div>
<div *ngIf="!alarmsFormArray().controls.length">
<div *ngIf="!alarmsFormArray.controls.length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-alarm-rules</span>
</div>

54
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -32,9 +32,9 @@ import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models';
import { guid } from '@core/utils';
import { Subscription } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-device-profile-alarms',
@ -53,7 +53,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}
]
})
export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator {
export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
deviceProfileAlarmsFormGroup: UntypedFormGroup;
@ -72,13 +72,11 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
@Input()
deviceProfileId: EntityId;
private valueChangeSubscription: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: UntypedFormBuilder,
private dialog: MatDialog) {
private fb: UntypedFormBuilder) {
}
registerOnChange(fn: any): void {
@ -92,9 +90,17 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
this.deviceProfileAlarmsFormGroup = this.fb.group({
alarms: this.fb.array([])
});
this.deviceProfileAlarmsFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
alarmsFormArray(): UntypedFormArray {
get alarmsFormArray(): UntypedFormArray {
return this.deviceProfileAlarmsFormGroup.get('alarms') as UntypedFormArray;
}
@ -108,24 +114,22 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
}
writeValue(alarms: Array<DeviceProfileAlarm> | null): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const alarmsControls: Array<AbstractControl> = [];
if (alarms) {
alarms.forEach((alarm) => {
alarmsControls.push(this.fb.control(alarm, [Validators.required]));
});
}
this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls));
if (this.disabled) {
this.deviceProfileAlarmsFormGroup.disable({emitEvent: false});
if (alarms?.length === this.alarmsFormArray.length) {
this.alarmsFormArray.patchValue(alarms, {emitEvent: false});
} else {
this.deviceProfileAlarmsFormGroup.enable({emitEvent: false});
const alarmsControls: Array<AbstractControl> = [];
if (alarms) {
alarms.forEach((alarm) => {
alarmsControls.push(this.fb.control(alarm, [Validators.required]));
});
}
this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls), {emitEvent: false});
if (this.disabled) {
this.deviceProfileAlarmsFormGroup.disable({emitEvent: false});
} else {
this.deviceProfileAlarmsFormGroup.enable({emitEvent: false});
}
}
this.valueChangeSubscription = this.deviceProfileAlarmsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
public trackByAlarm(index: number, alarmControl: AbstractControl): string {

4
ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html

@ -31,7 +31,7 @@
<mat-form-field fxFlex class="mat-block">
<mat-label translate>device-profile.provision-device-key</mat-label>
<input matInput formControlName="provisionDeviceKey" required/>
<button mat-button matSuffix mat-icon-button
<button matSuffix mat-icon-button
ngxClipboard
[cbContent]="provisionConfigurationFormGroup.get('provisionDeviceKey').value"
(cbOnSuccess)="onProvisionCopied(true)"
@ -46,7 +46,7 @@
<mat-form-field fxFlex class="mat-block">
<mat-label translate>device-profile.provision-device-secret</mat-label>
<input matInput formControlName="provisionDeviceSecret" required/>
<button mat-button matSuffix mat-icon-button
<button matSuffix mat-icon-button
ngxClipboard
[cbContent]="provisionConfigurationFormGroup.get('provisionDeviceSecret').value"
(cbOnSuccess)="onProvisionCopied(false)"

17
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts

@ -26,7 +26,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { Subject, Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import {
AttributeName,
AttributeNameTranslationMap,
@ -70,7 +70,6 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
attributesValueFormGroup: UntypedFormGroup;
private propagateChange = null;
private valueChange$: Subscription = null;
private destroy$ = new Subject<void>();
private usedAttributesName: AttributeName[] = [];
@ -80,6 +79,9 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
this.attributesValueFormGroup = this.fb.group({
attributesValue: this.fb.array([])
});
this.attributesValueFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnInit() {
@ -92,9 +94,6 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
@ -116,24 +115,18 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
}
writeValue(keyValMap: AttributesNameValueMap): void {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const attributesValueControls: Array<AbstractControl> = [];
if (keyValMap) {
(Object.keys(keyValMap) as AttributeName[]).forEach(name => {
attributesValueControls.push(this.createdFormGroup({name, value: keyValMap[name]}));
});
}
this.attributesValueFormGroup.setControl('attributesValue', this.fb.array(attributesValueControls));
this.attributesValueFormGroup.setControl('attributesValue', this.fb.array(attributesValueControls), {emitEvent: false});
if (this.disabled) {
this.attributesValueFormGroup.disable({emitEvent: false});
} else {
this.attributesValueFormGroup.enable({emitEvent: false});
}
this.valueChange$ = this.attributesValueFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedAttributesName();
}

4
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html

@ -17,7 +17,7 @@
-->
<div fxLayout="column">
<mat-accordion multi="true">
<div *ngFor="let serverConfig of serverConfigsFromArray().controls; trackBy: trackByParams; let $index = index;">
<div *ngFor="let serverConfig of serverConfigsFromArray.controls; trackBy: trackByParams; let $index = index;">
<tb-profile-lwm2m-device-config-server
[formControl]="serverConfig"
(removeServer)="removeServerConfig($event, $index)"
@ -25,7 +25,7 @@
</tb-profile-lwm2m-device-config-server>
</div>
</mat-accordion>
<div *ngIf="!serverConfigsFromArray().controls.length" style="margin:32px 0">
<div *ngIf="!serverConfigsFromArray.controls.length" style="margin:32px 0">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.lwm2m.no-config-servers</span>
</div>

63
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -24,13 +24,13 @@ import {
NG_VALIDATORS,
NG_VALUE_ACCESSOR
} from '@angular/forms';
import { of, Subscription } from 'rxjs';
import { of, Subject } from 'rxjs';
import { ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog';
import { Lwm2mBootstrapAddConfigServerDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component';
import { mergeMap } from 'rxjs/operators';
import { mergeMap, takeUntil } from 'rxjs/operators';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models';
@ -50,7 +50,7 @@ import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models';
}
]
})
export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValueAccessor {
export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValueAccessor, OnDestroy {
bootstrapConfigServersFormGroup: UntypedFormGroup;
@ -72,8 +72,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
}
}
private valueChangeSubscription: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(public translate: TranslateService,
@ -94,9 +93,17 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
this.bootstrapConfigServersFormGroup = this.fb.group({
serverConfigs: this.fb.array([])
});
this.bootstrapConfigServersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
serverConfigsFromArray(): UntypedFormArray {
get serverConfigsFromArray(): UntypedFormArray {
return this.bootstrapConfigServersFormGroup.get('serverConfigs') as UntypedFormArray;
}
@ -110,24 +117,22 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
}
writeValue(serverConfigs: Array<ServerSecurityConfig> | null): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const serverConfigsControls: Array<AbstractControl> = [];
if (serverConfigs) {
serverConfigs.forEach((serverConfig) => {
serverConfigsControls.push(this.fb.control(serverConfig));
});
}
this.bootstrapConfigServersFormGroup.setControl('serverConfigs', this.fb.array(serverConfigsControls));
if (this.disabled) {
this.bootstrapConfigServersFormGroup.disable({emitEvent: false});
if (serverConfigs?.length === this.serverConfigsFromArray.length) {
this.serverConfigsFromArray.patchValue(serverConfigs, {emitEvent: false});
} else {
this.bootstrapConfigServersFormGroup.enable({emitEvent: false});
const serverConfigsControls: Array<AbstractControl> = [];
if (serverConfigs) {
serverConfigs.forEach((serverConfig) => {
serverConfigsControls.push(this.fb.control(serverConfig));
});
}
this.bootstrapConfigServersFormGroup.setControl('serverConfigs', this.fb.array(serverConfigsControls), {emitEvent: false});
if (this.disabled) {
this.bootstrapConfigServersFormGroup.disable({emitEvent: false});
} else {
this.bootstrapConfigServersFormGroup.enable({emitEvent: false});
}
}
this.valueChangeSubscription = this.bootstrapConfigServersFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
trackByParams(index: number): number {
@ -147,7 +152,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
true
).subscribe((result) => {
if (result) {
this.serverConfigsFromArray().removeAt(index);
this.serverConfigsFromArray.removeAt(index);
}
});
}
@ -169,7 +174,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
addServerConfigObs.subscribe((serverConfig) => {
if (serverConfig) {
serverConfig.securityMode = Lwm2mSecurityType.NO_SEC;
this.serverConfigsFromArray().push(this.fb.control(serverConfig));
this.serverConfigsFromArray.push(this.fb.control(serverConfig));
this.updateModel();
} else {
this.isTransportWasRunWithBootstrap = false;
@ -196,7 +201,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
}
private isBootstrapAdded(): boolean {
const serverConfigsArray = this.serverConfigsFromArray().getRawValue();
const serverConfigsArray = this.serverConfigsFromArray.getRawValue();
for (let i = 0; i < serverConfigsArray.length; i++) {
if (serverConfigsArray[i].bootstrapServerIs) {
return true;
@ -207,15 +212,15 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
private removeBootstrapServerConfig(): void {
if (this.bootstrapConfigServersFormGroup) {
const bootstrapServerIndex = this.serverConfigsFromArray().getRawValue().findIndex(server => server.bootstrapServerIs === true);
const bootstrapServerIndex = this.serverConfigsFromArray.getRawValue().findIndex(server => server.bootstrapServerIs === true);
if (bootstrapServerIndex !== -1) {
this.serverConfigsFromArray().removeAt(bootstrapServerIndex);
this.serverConfigsFromArray.removeAt(bootstrapServerIndex);
}
}
}
private updateModel() {
const serverConfigs: Array<ServerSecurityConfig> = this.serverConfigsFromArray().value;
const serverConfigs: Array<ServerSecurityConfig> = this.serverConfigsFromArray.value;
this.propagateChange(serverConfigs);
}
}

10
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.ts

@ -80,6 +80,8 @@ export class Lwm2mObserveAttrTelemetryInstancesComponent implements ControlValue
this.instancesFormGroup = this.fb.group({
instances: this.fb.array([])
});
this.valueChange$ = this.instancesFormGroup.valueChanges.subscribe(value => this.updateModel(value.instances));
}
ngOnDestroy() {
@ -122,22 +124,16 @@ export class Lwm2mObserveAttrTelemetryInstancesComponent implements ControlValue
if (instances.length === this.instancesFormArray.length) {
this.instancesFormArray.patchValue(instances, {emitEvent: false});
} else {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const instancesControl: Array<AbstractControl> = [];
if (instances) {
instances.forEach((instance) => {
instancesControl.push(this.createInstanceFormGroup(instance));
});
}
this.instancesFormGroup.setControl('instances', this.fb.array(instancesControl));
this.instancesFormGroup.setControl('instances', this.fb.array(instancesControl), {emitEvent: false});
if (this.disabled) {
this.instancesFormGroup.disable({emitEvent: false});
}
this.valueChange$ = this.instancesFormGroup.valueChanges.subscribe(value => {
this.updateModel(value.instances);
});
}
}

20
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.ts

@ -29,7 +29,7 @@ import {
} from '@angular/forms';
import { ResourceLwM2M } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { combineLatest, Subject, Subscription } from 'rxjs';
import { combineLatest, Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
@Component({
@ -71,19 +71,19 @@ export class Lwm2mObserveAttrTelemetryResourcesComponent implements ControlValue
}
private destroy$ = new Subject<void>();
private valueChange$: Subscription = null;
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder) {
this.resourcesFormGroup = this.fb.group({
resources: this.fb.array([])
});
this.resourcesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel(this.resourcesFormGroup.getRawValue().resources));
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
@ -131,24 +131,18 @@ export class Lwm2mObserveAttrTelemetryResourcesComponent implements ControlValue
private updatedResources(resources: ResourceLwM2M[]): void {
if (resources.length === this.resourcesFormArray.length) {
this.resourcesFormArray.patchValue(resources, {onlySelf: true});
this.resourcesFormArray.patchValue(resources, {onlySelf: true, emitEvent: false});
} else {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const resourcesControl: Array<AbstractControl> = [];
if (resources) {
resources.forEach((resource) => {
resourcesControl.push(this.createdResourceFormGroup(resource));
});
}
this.resourcesFormGroup.setControl('resources', this.fb.array(resourcesControl));
this.resourcesFormGroup.setControl('resources', this.fb.array(resourcesControl), {emitEvent: false});
if (this.disabled) {
this.resourcesFormGroup.disable({emitEvent: false});
}
this.valueChange$ = this.resourcesFormGroup.valueChanges.subscribe(() => {
this.updateModel(this.resourcesFormGroup.getRawValue().resources);
});
}
}

10
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts

@ -86,6 +86,8 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor,
this.modelsFormGroup = this.fb.group({
models: this.fb.array([])
});
this.valueChange$ = this.modelsFormGroup.valueChanges.subscribe(value => this.updateModel(value.models));
}
ngOnDestroy() {
@ -130,20 +132,14 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor,
if (models.length === this.modelsFormArray.length) {
this.modelsFormArray.patchValue(models, {emitEvent: false});
} else {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const modelControls: Array<AbstractControl> = [];
models.forEach(model => {
modelControls.push(this.createModelFormGroup(model));
});
this.modelsFormGroup.setControl('models', this.fb.array(modelControls));
this.modelsFormGroup.setControl('models', this.fb.array(modelControls), {emitEvent: false});
if (this.disabled) {
this.modelsFormGroup.disable({emitEvent: false});
}
this.valueChange$ = this.modelsFormGroup.valueChanges.subscribe(value => {
this.updateModel(value.models);
});
}
}

27
ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html

@ -21,24 +21,15 @@
</mat-checkbox>
<div *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value"
class="tb-hint" innerHTML="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-hint' | translate }}"></div>
<mat-form-field floatLabel="always" class="mat-block" style="padding-top: 8px;"
*ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value">
<mat-label translate>device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names</mat-label>
<mat-chip-list #attrMetricNamesChipList formControlName="sparkplugAttributesMetricNames">
<mat-chip
*ngFor="let name of mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value;"
(removed)="removeAttributeMetricName(name)">
{{name}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names' | translate}}"
[matChipInputFor]="attrMetricNamesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAttributeMetricName($event)">
</mat-chip-list>
<mat-hint innerHTML="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint' | translate }}"></mat-hint>
</mat-form-field>
<tb-string-items-list *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value"
editable
label="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names' | translate }}"
placeholder="{{'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names' | translate}}"
hint="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint' | translate }}"
floatLabel="always"
subscriptSizing="dynamic"
formControlName="sparkplugAttributesMetricNames">
</tb-string-items-list>
</form>
<form [formGroup]="mqttDeviceProfileTransportConfigurationFormGroup" style="padding-bottom: 16px;" *ngIf="!mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value">
<fieldset class="fields-group">

32
ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts

@ -41,8 +41,6 @@ import {
import { isDefinedAndNotNull } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
@Component({
selector: 'tb-mqtt-device-profile-transport-configuration',
@ -79,8 +77,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
private propagateChange = (v: any) => { };
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
constructor(private store: Store<AppState>,
private fb: UntypedFormBuilder) {
}
@ -175,34 +171,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
}
}
removeAttributeMetricName(name: string): void {
const names: string[] = this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value;
const index = names.indexOf(name);
if (index >= 0) {
names.splice(index, 1);
this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').setValue(names);
}
}
addAttributeMetricName(event: MatChipInputEvent): void {
const input = event.input;
let value = event.value;
if ((value || '').trim()) {
value = value.trim();
let names: string[] = this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value;
if (!names || names.indexOf(value) === -1) {
if (!names) {
names = [];
}
names.push(value);
this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').setValue(names, {emitEvent: true});
}
}
if (input) {
input.value = '';
}
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) {

4
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html

@ -16,7 +16,7 @@
-->
<div fxLayout="column">
<div *ngFor="let deviceProfileCommunication of communicationConfigFormArray().controls; let $index = index;
<div *ngFor="let deviceProfileCommunication of communicationConfigFormArray.controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication">
<div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
@ -58,7 +58,7 @@
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!communicationConfigFormArray().controls.length && !disabled">
<div *ngIf="!communicationConfigFormArray.controls.length && !disabled">
<span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-communication-config</span>
</div>
<div *ngIf="!disabled && isAddEnabled">

55
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts

@ -27,7 +27,7 @@ import {
Validators
} from '@angular/forms';
import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models';
import { Subject, Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { isUndefinedOrNull } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@ -58,7 +58,6 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
disabled: boolean;
private usedSpecType: SnmpSpecType[] = [];
private valueChange$: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
@ -68,17 +67,17 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
this.deviceProfileCommunicationConfig = this.fb.group({
communicationConfig: this.fb.array([])
});
this.deviceProfileCommunicationConfig.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
communicationConfigFormArray(): UntypedFormArray {
get communicationConfigFormArray(): UntypedFormArray {
return this.deviceProfileCommunicationConfig.get('communicationConfig') as UntypedFormArray;
}
@ -99,27 +98,27 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
}
writeValue(communicationConfig: SnmpCommunicationConfig[]) {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const communicationConfigControl: Array<AbstractControl> = [];
if (communicationConfig) {
communicationConfig.forEach((config) => {
communicationConfigControl.push(this.createdFormGroup(config));
});
}
this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl));
if (!communicationConfig || !communicationConfig.length) {
this.addCommunicationConfig();
}
if (this.disabled) {
this.deviceProfileCommunicationConfig.disable({emitEvent: false});
if (communicationConfig?.length === this.communicationConfigFormArray.length) {
this.communicationConfigFormArray.patchValue(communicationConfig, {emitEvent: false});
} else {
this.deviceProfileCommunicationConfig.enable({emitEvent: false});
const communicationConfigControl: Array<AbstractControl> = [];
if (communicationConfig) {
communicationConfig.forEach((config) => {
communicationConfigControl.push(this.createdFormGroup(config));
});
}
this.deviceProfileCommunicationConfig.setControl(
'communicationConfig', this.fb.array(communicationConfigControl), {emitEvent: false}
);
if (!communicationConfig || !communicationConfig.length) {
this.addCommunicationConfig();
}
if (this.disabled) {
this.deviceProfileCommunicationConfig.disable({emitEvent: false});
} else {
this.deviceProfileCommunicationConfig.enable({emitEvent: false});
}
}
this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedSpecType();
if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) {
this.updateModel();
@ -133,16 +132,16 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
}
public removeCommunicationConfig(index: number) {
this.communicationConfigFormArray().removeAt(index);
this.communicationConfigFormArray.removeAt(index);
}
get isAddEnabled(): boolean {
return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length;
return this.communicationConfigFormArray.length !== Object.keys(SnmpSpecType).length;
}
public addCommunicationConfig() {
this.communicationConfigFormArray().push(this.createdFormGroup());
this.communicationConfigFormArray.push(this.createdFormGroup());
this.deviceProfileCommunicationConfig.updateValueAndValidity();
if (!this.deviceProfileCommunicationConfig.valid) {
this.updateModel();

4
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html

@ -25,7 +25,7 @@
</div>
</div>
<mat-divider></mat-divider>
<div *ngFor="let mappingConfig of mappingsConfigFormArray().controls; let $index = index;
<div *ngFor="let mappingConfig of mappingsConfigFormArray.controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list">
<div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
@ -64,7 +64,7 @@
</button>
</div>
</div>
<div *ngIf="!mappingsConfigFormArray().controls.length && !disabled">
<div *ngIf="!mappingsConfigFormArray.controls.length && !disabled">
<span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-mapping-config</span>
</div>
<div *ngIf="!disabled">

59
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts

@ -18,19 +18,20 @@ import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SnmpMapping } from '@shared/models/device.models';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { DataType, DataTypeTranslationMap } from '@shared/models/constants';
import { isUndefinedOrNull } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-snmp-device-profile-mapping',
@ -60,7 +61,7 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
private readonly oidPattern: RegExp = /^\.?([0-2])((\.0)|(\.[1-9][0-9]*))*$/;
private valueChange$: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder) { }
@ -69,12 +70,14 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
this.mappingsConfigForm = this.fb.group({
mappings: this.fb.array([])
});
this.mappingsConfigForm.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any) {
@ -100,38 +103,36 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
}
writeValue(mappings: SnmpMapping[]) {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const mappingsControl: Array<AbstractControl> = [];
if (mappings) {
mappings.forEach((config) => {
mappingsControl.push(this.createdFormGroup(config));
});
}
this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl));
if (!mappings || !mappings.length) {
this.addMappingConfig();
}
if (this.disabled) {
this.mappingsConfigForm.disable({emitEvent: false});
if (mappings?.length === this.mappingsConfigFormArray.length) {
this.mappingsConfigFormArray.patchValue(mappings, {emitEvent: false});
} else {
this.mappingsConfigForm.enable({emitEvent: false});
const mappingsControl: Array<AbstractControl> = [];
if (mappings) {
mappings.forEach((config) => {
mappingsControl.push(this.createdFormGroup(config));
});
}
this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl), {emitEvent: false});
if (!mappings || !mappings.length) {
this.addMappingConfig();
}
if (this.disabled) {
this.mappingsConfigForm.disable({emitEvent: false});
} else {
this.mappingsConfigForm.enable({emitEvent: false});
}
}
this.valueChange$ = this.mappingsConfigForm.valueChanges.subscribe(() => {
this.updateModel();
});
if (!this.disabled && !this.mappingsConfigForm.valid) {
this.updateModel();
}
}
mappingsConfigFormArray(): UntypedFormArray {
get mappingsConfigFormArray(): UntypedFormArray {
return this.mappingsConfigForm.get('mappings') as UntypedFormArray;
}
public addMappingConfig() {
this.mappingsConfigFormArray().push(this.createdFormGroup());
this.mappingsConfigFormArray.push(this.createdFormGroup());
this.mappingsConfigForm.updateValueAndValidity();
if (!this.mappingsConfigForm.valid) {
this.updateModel();
@ -139,7 +140,7 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
}
public removeMappingConfig(index: number) {
this.mappingsConfigFormArray().removeAt(index);
this.mappingsConfigFormArray.removeAt(index);
}
private createdFormGroup(value?: SnmpMapping): UntypedFormGroup {

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

@ -30,10 +30,11 @@ import {
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { QueueInfo } from '@shared/models/queue.models';
import { UtilsService } from '@core/services/utils.service';
import { guid } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-tenant-profile-queues',
@ -70,8 +71,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
@Input()
disabled: boolean;
private valueChangeSubscription$: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -83,12 +83,6 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
this.propagateChange = fn;
}
ngOnDestroy() {
if (this.valueChangeSubscription$) {
this.valueChangeSubscription$.unsubscribe();
}
}
registerOnTouched(fn: any): void {
}
@ -96,6 +90,15 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
this.tenantProfileQueuesFormGroup = this.fb.group({
queues: this.fb.array([])
});
this.tenantProfileQueuesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get queuesFormArray(): UntypedFormArray {
@ -112,30 +115,28 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
}
writeValue(queues: Array<QueueInfo> | null): void {
if (this.valueChangeSubscription$) {
this.valueChangeSubscription$.unsubscribe();
}
const queuesControls: Array<AbstractControl> = [];
if (queues) {
queues.forEach((queue, index) => {
if (!queue.id) {
if (!this.idMap[index]) {
this.idMap.push(guid());
}
queue.id = this.idMap[index];
}
queuesControls.push(this.fb.control(queue, [Validators.required]));
});
}
this.tenantProfileQueuesFormGroup.setControl('queues', this.fb.array(queuesControls));
if (this.disabled) {
this.tenantProfileQueuesFormGroup.disable({emitEvent: false});
if (queues.length === this.queuesFormArray.length) {
this.queuesFormArray.patchValue(queues, {emitEvent: false});
} else {
this.tenantProfileQueuesFormGroup.enable({emitEvent: false});
const queuesControls: Array<AbstractControl> = [];
if (queues) {
queues.forEach((queue, index) => {
if (!queue.id) {
if (!this.idMap[index]) {
this.idMap.push(guid());
}
queue.id = this.idMap[index];
}
queuesControls.push(this.fb.control(queue, [Validators.required]));
});
}
this.tenantProfileQueuesFormGroup.setControl('queues', this.fb.array(queuesControls), {emitEvent: false});
if (this.disabled) {
this.tenantProfileQueuesFormGroup.disable({emitEvent: false});
} else {
this.tenantProfileQueuesFormGroup.enable({emitEvent: false});
}
}
this.valueChangeSubscription$ = this.tenantProfileQueuesFormGroup.valueChanges.subscribe(() =>
this.updateModel()
);
}
public trackByQueue(index: number, queueControl: AbstractControl) {

4
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts

@ -45,7 +45,7 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.transport-device-msg'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-device-telemetry-msg'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-device-telemetry-data-points'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.transport-tenant-msg-rate-limit'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rest-requests-for-tenant'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.customer-rest-limits'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.ws-limit-updates-per-session'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.cassandra-tenant-limits-configuration'],
@ -63,7 +63,7 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.edit-transport-device-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-device-telemetry-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-transport-tenant-msg-rate-limit-title'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-tenant-rest-limits-title'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-customer-rest-limits-title'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.rate-limits.edit-ws-limit-updates-per-session-title'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-cassandra-tenant-limits-configuration-title'],

8
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html

@ -17,17 +17,17 @@
-->
<div class="tb-relation-filters" [formGroup]="relationFiltersFormGroup">
<div class="container">
<div class="header" [fxShow]="relationFiltersFormArray().length">
<div class="header" [fxShow]="relationFiltersFormArray.length">
<div fxLayout="row" fxLayoutAlign="start center">
<span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
<span class="cell" fxFlex translate>entity.entity-types</span>
<span class="cell" style="width: 40px; min-width: 40px;">&nbsp;</span>
</div>
</div>
<div class="body" [fxShow]="relationFiltersFormArray().length">
<div class="body" [fxShow]="relationFiltersFormArray.length">
<div class="row" fxFlex fxLayout="row"
fxLayoutAlign="start center" formArrayName="relationFilters"
*ngFor="let relationFilterControl of relationFiltersFormArray().controls; let $index = index">
*ngFor="let relationFilterControl of relationFiltersFormArray.controls; let $index = index">
<div class="mat-elevation-z1" fxFlex fxLayout="row" fxLayoutAlign="start center" style="padding: 8px 0;">
<tb-relation-type-autocomplete
class="cell" style="width: 200px; min-width: 200px;"
@ -51,7 +51,7 @@
</div>
</div>
</div>
<div class="any-filter" [fxShow]="!relationFiltersFormArray().length">
<div class="any-filter" [fxShow]="!relationFiltersFormArray.length">
<span fxLayoutAlign="center center"
class="tb-prompt" translate>relation.any-relation</span>
</div>

54
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -28,7 +28,8 @@ import { RelationEntityTypeFilter } from '@shared/models/relation.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-relation-filters',
@ -42,7 +43,7 @@ import { Subscription } from 'rxjs';
}
]
})
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit {
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input() disabled: boolean;
@ -50,22 +51,32 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
relationFiltersFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
ngOnInit(): void {
this.relationFiltersFormGroup = this.fb.group({});
this.relationFiltersFormGroup.addControl('relationFilters',
this.fb.array([]));
this.relationFiltersFormGroup = this.fb.group({
relationFilters: this.fb.array([])
});
this.relationFiltersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.updateModel();
});
}
relationFiltersFormArray(): UntypedFormArray {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get relationFiltersFormArray(): UntypedFormArray {
return this.relationFiltersFormGroup.get('relationFilters') as UntypedFormArray;
}
@ -81,19 +92,17 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
}
writeValue(filters: Array<RelationEntityTypeFilter>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const relationFiltersControls: Array<AbstractControl> = [];
if (filters && filters.length) {
filters.forEach((filter) => {
relationFiltersControls.push(this.createRelationFilterFormGroup(filter));
});
if (filters?.length === this.relationFiltersFormArray.length) {
this.relationFiltersFormArray.patchValue(filters, {emitEvent: false});
} else {
const relationFiltersControls: Array<AbstractControl> = [];
if (filters && filters.length) {
filters.forEach((filter) => {
relationFiltersControls.push(this.createRelationFilterFormGroup(filter));
});
}
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls), {emitEvent: false});
}
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls));
this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
public removeFilter(index: number) {
@ -101,12 +110,11 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
}
public addFilter() {
const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as UntypedFormArray;
const filter: RelationEntityTypeFilter = {
relationType: null,
entityTypes: []
};
relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
this.relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
}
private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl {

15
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html

@ -15,8 +15,10 @@
limitations under the License.
-->
<div>
<mat-card appearance="outlined" class="repository-settings" [ngClass]="{'settings-card': !detailsMode}" [ngStyle]="popoverComponent ? {boxShadow: 'none', width: '800px'} : {}">
<div style="height: min-content; max-height: 80vh;">
<mat-card appearance="outlined" class="repository-settings"
[ngClass]="{'settings-card': !detailsMode}"
[ngStyle]="popoverComponent ? {boxShadow: 'none', maxWidth: '100%', minWidth: '100%', width: '800px'} : {}">
<mat-card-header>
<mat-card-title>
<span class="mat-headline-5" translate>admin.repository-settings</span>
@ -41,10 +43,10 @@
<mat-label translate>admin.default-branch</mat-label>
<input matInput formControlName="defaultBranch">
</mat-form-field>
<mat-checkbox formControlName="readOnly">
{{ 'admin.repository-read-only' | translate }}
</mat-checkbox>
<div>
<div fxLayout="column">
<mat-checkbox formControlName="readOnly">
{{ 'admin.repository-read-only' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMergeCommits">
{{ 'admin.show-merge-commits' | translate }}
</mat-checkbox>
@ -74,6 +76,7 @@
<input matInput formControlName="password" type="password"
placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/>
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-hint [innerHTML] = "'admin.auth-method-username-password-hint' | translate"></mat-hint>
</mat-form-field>
</section>
<section [fxShow]="repositorySettingsForm.get('authMethod').value === repositoryAuthMethod.PRIVATE_KEY" fxLayout="column">

3
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss

@ -17,9 +17,6 @@
.mat-mdc-card.repository-settings {
margin: 8px;
}
.mat-mdc-checkbox {
padding-bottom: 16px;
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;

2
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss

@ -101,7 +101,7 @@
&.comparison {
.mat-expansion-panel-header {
height: 100%;
height: fit-content;
}
}

8
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -804,11 +804,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (descriptors.length) {
let entityId;
let entityName;
let entityLabel;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
entityLabel = alarm.originatorLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm});
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm}, entityLabel);
}
}
@ -827,11 +829,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
let entityId;
let entityName;
let entityLabel;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
entityLabel = alarm.originatorLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm});
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm}, entityLabel);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts

@ -405,7 +405,7 @@ export class TbCanvasDigitalGauge {
(this.gauge.options as CanvasDigitalGaugeOptions).labelTimestamp =
filter.transform(timestamp, this.localSettings.timestampFormat);
}
const value = tvPair[1];
const value = parseFloat(tvPair[1]);
if (value !== this.gauge.value) {
if (!this.gauge.options.animation) {
this.gauge._value = value;

2
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html

@ -17,7 +17,7 @@
-->
<div fxLayout="column" class="mat-content mat-padding">
<label class="tb-title" translate>entity.columns-to-display</label>
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="column.display" *ngFor="let column of (columns | selectableColumns)"
<mat-checkbox [(ngModel)]="column.display" *ngFor="let column of (columns | selectableColumns)"
(ngModelChange)="update()">
{{ column.title }}
</mat-checkbox>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html

@ -51,7 +51,7 @@
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" fxLayoutGap="8px">
<section fxLayout="column" fxLayoutGap.gt-xs="8px">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="displayEntityName">
{{ 'widgets.table.display-entity-name' | translate }}

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

@ -47,7 +47,7 @@
</mat-form-field>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.label-widget.label-position</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widgets.label-widget.x-pos</mat-label>
<input matInput type="number" min="0" max="100" formControlName="x">

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

@ -24,7 +24,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.border-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.border-width</mat-label>
<input matInput type="number" min="0" formControlName="borderWidth">

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

@ -41,7 +41,7 @@
</fieldset>
<fieldset class="fields-group" formGroupName="stroke">
<legend class="group-title" translate>widgets.chart.stroke-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.width-pixels</mat-label>
<input matInput type="number" min="0" formControlName="width">

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html

@ -16,7 +16,7 @@
-->
<section [formGroup]="widgetFontFormGroup" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.widget-font.font-family</mat-label>
<input matInput formControlName="family">
@ -26,7 +26,7 @@
<input matInput type="number" min="1" step="1" formControlName="size">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.widget-font.font-style</mat-label>
<mat-select formControlName="style">
@ -68,7 +68,7 @@
</mat-select>
</mat-form-field>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input
fxFlex
formControlName="color"

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

@ -29,7 +29,7 @@
<mat-label translate>widgets.rpc.initial-value</mat-label>
<input matInput type="number" formControlName="initialValue">
</mat-form-field>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.min-value</mat-label>
<input required matInput type="number" formControlName="minValue">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html

@ -33,7 +33,7 @@
</mat-checkbox>
</section>
</section>
<section fxLayout="column" fxLayoutGap="8px">
<section fxLayout="column" fxLayoutGap.gt-xs="8px">
<mat-slide-toggle fxFlex formControlName="displayDetails">
{{ 'widgets.persistent-table.display-request-details' | translate }}
</mat-slide-toggle>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html

@ -24,7 +24,7 @@
<mat-slide-toggle formControlName="isPrimary" class="slide-block">
{{ 'widgets.rpc.button-primary' | translate }}
</mat-slide-toggle>
<section fxLayout="row wrap" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="textColor"
icon="format_color_fill"

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

@ -22,7 +22,7 @@
<mat-label translate>widgets.rpc.slide-toggle-label</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.label-position</mat-label>
<mat-select formControlName="labelPosition">

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html

@ -42,7 +42,7 @@
icon="format_color_fill"
label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.minor-ticks-count</mat-label>
<input matInput type="number" min="0" formControlName="minorTicks">
@ -71,7 +71,7 @@
<mat-slide-toggle formControlName="showBorder" class="slide-block">
{{ 'widgets.gauge.show-plate-border' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<tb-color-input fxFlex
formControlName="colorBorder"
icon="format_color_fill"
@ -89,7 +89,7 @@
<mat-label translate>widgets.gauge.needle-circle-size</mat-label>
<input matInput type="number" min="0" formControlName="needleCircleSize">
</mat-form-field>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<tb-color-input fxFlex
formControlName="colorNeedle"
icon="format_color_fill"

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html

@ -31,7 +31,7 @@
</tb-color-input>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.ticks-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.min-value</mat-label>
<input matInput type="number" formControlName="minValue">
@ -41,7 +41,7 @@
<input matInput type="number" formControlName="maxValue">
</mat-form-field>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="0px" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.major-ticks-count</mat-label>
<input matInput type="number" min="0" formControlName="majorTicksCount">
@ -52,7 +52,7 @@
label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="0px" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.minor-ticks-count</mat-label>
<input matInput type="number" min="0" formControlName="minorTicks">
@ -127,7 +127,7 @@
<legend class="group-title" translate>widgets.gauge.value-font</legend>
<tb-widget-font formControlName="valueFont" [hasShadowColor]="true"></tb-widget-font>
</fieldset>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorValueBoxRect"
icon="format_color_fill"
@ -139,7 +139,7 @@
label="{{ 'widgets.gauge.value-box-rect-stroke-color-end' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorValueBoxBackground"
icon="format_color_fill"
@ -168,7 +168,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.needle-settings</legend>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorNeedle"
icon="format_color_fill"
@ -180,7 +180,7 @@
label="{{ 'widgets.gauge.needle-color-end' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorNeedleShadowUp"
icon="format_color_fill"
@ -294,7 +294,7 @@
<ng-template #radialGauge let-settingsForm="settingsForm">
<fieldset class="fields-group" [formGroup]="settingsForm">
<legend class="group-title" translate>widgets.gauge.radial-gauge-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.start-ticks-angle</mat-label>
<input matInput type="number" min="0" max="360" formControlName="startAngle">
@ -314,7 +314,7 @@
<ng-template #linearGauge let-settingsForm="settingsForm">
<fieldset class="fields-group" [formGroup]="settingsForm">
<legend class="group-title" translate>widgets.gauge.linear-gauge-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.bar-stroke-width</mat-label>
<input matInput type="number" min="0" formControlName="barStrokeWidth">
@ -325,7 +325,7 @@
label="{{ 'widgets.gauge.bar-stroke-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorBar"
icon="format_color_fill"
@ -337,7 +337,7 @@
label="{{ 'widgets.gauge.bar-background-color-end' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorBarProgress"
icon="format_color_fill"

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="digitalGaugeWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.common-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.min-value</mat-label>
<input matInput type="number" formControlName="minValue">
@ -182,7 +182,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.unit-title-and-timestamp-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="showUnitTitle">
{{ 'widgets.gauge.show-unit-title' | translate }}
</mat-slide-toggle>
@ -191,7 +191,7 @@
<input matInput formControlName="unitTitle">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="showTimestamp">
{{ 'widgets.gauge.show-timestamp' | translate }}
</mat-slide-toggle>

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

@ -40,7 +40,7 @@
<div fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<section class="tb-widget-settings" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex>
<mat-label translate>widgets.gauge.highlight-from</mat-label>
<input matInput type="number" formControlName="from">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html

@ -45,7 +45,7 @@
<div fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<section class="tb-widget-settings" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gpio.pin</mat-label>
<input required matInput type="number" min="1" step="1" formControlName="pin">
@ -55,7 +55,7 @@
<input required matInput formControlName="label">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gpio.row</mat-label>
<input required matInput type="number" min="0" step="1" formControlName="row">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html

@ -28,7 +28,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.checkbox-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.true-label</mat-label>
<input matInput formControlName="trueValue">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html

@ -21,7 +21,7 @@
</tb-update-attribute-general-settings>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.double-field-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.min-value</mat-label>
<input matInput type="number" step="any" formControlName="minValue">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html

@ -21,7 +21,7 @@
</tb-update-attribute-general-settings>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.integer-field-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.min-value</mat-label>
<input matInput type="number" step="1" formControlName="minValue">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html

@ -37,7 +37,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.attribute-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.widget-mode</mat-label>
<mat-select formControlName="widgetMode">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html

@ -25,7 +25,7 @@
<mat-slide-toggle formControlName="showResultMessage" class="slide-block">
{{ 'widgets.input-widgets.show-result-message' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.latitude-key-name</mat-label>
<input matInput formControlName="latKeyName">
@ -47,7 +47,7 @@
<mat-slide-toggle formControlName="showLabel" class="slide-block">
{{ 'widgets.input-widgets.show-label' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.latitude-label</mat-label>
<input matInput formControlName="latLabel">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html

@ -45,7 +45,7 @@
<mat-checkbox formControlName="updateAllValues" style="margin-bottom: 8px;">
{{ 'widgets.input-widgets.update-all-values' | translate }}
</mat-checkbox>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.save-button-label</mat-label>
<input matInput formControlName="saveButtonLabel">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html

@ -21,7 +21,7 @@
</tb-update-attribute-general-settings>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.text-field-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.min-length</mat-label>
<input matInput type="number" step="1" min="0" formControlName="minLength">

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="commonMapSettingsFormGroup">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.common-map-settings</legend>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-datasources-key-autocomplete fxFlex
[fxShow]="provider === mapProvider.image"
required
@ -55,7 +55,7 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section [fxShow]="provider !== mapProvider.image" fxLayout="row" fxLayoutGap="8px">
<section [fxShow]="provider !== mapProvider.image" fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.default-map-zoom-level</mat-label>
<input matInput type="number" min="0" max="20" step="1" formControlName="defaultZoomLevel">
@ -65,7 +65,7 @@
<input matInput formControlName="defaultCenterPosition">
</mat-form-field>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" style="margin-bottom: 15px;">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" style="margin-bottom: 15px;">
<section fxFlex fxLayout="column">
<mat-slide-toggle formControlName="disableScrollZooming" class="slide-block">
{{ 'widgets.maps.disable-scroll-zooming' | translate }}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html

@ -34,7 +34,7 @@
<mat-slide-toggle formControlName="zoomOnClick" class="slide-block">
{{ 'widgets.maps.zoom-on-cluster-click' | translate }}
</mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.max-cluster-zoom</mat-label>
<input matInput type="number" min="0" max="18" step="1" formControlName="maxZoom">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="markersSettingsFormGroup">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.markers-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.marker-offset-x</mat-label>
<input matInput type="number" formControlName="markerOffsetX">
@ -113,7 +113,7 @@
functionTitle="{{ 'widgets.maps.tooltip-function' | translate }}"
helpId="widget/lib/map/tooltip_fn">
</tb-js-func>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.tooltip-offset-x</mat-label>
<input matInput type="number" formControlName="tooltipOffsetX">

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

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="routeMapSettingsFormGroup">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.route-map-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.stroke-weight</mat-label>
<input matInput type="number" min="0" formControlName="strokeWeight">

3
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -417,9 +417,6 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
'.mat-mdc-table .mat-mdc-header-cell {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-header-cell .mat-sort-header-arrow {\n' +
'color: ' + mdDarkDisabled + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell, .mat-mdc-table .mat-mdc-header-cell {\n' +
'border-bottom-color: ' + mdDarkDivider + ';\n' +
'}\n' +

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

@ -19,8 +19,8 @@
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div *ngIf="displayTimewindowConfig()" fxFlex="100"
fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs>
fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow">
{{ 'widget-config.use-dashboard-timewindow' | translate }}
</mat-checkbox>
@ -29,7 +29,7 @@
</mat-checkbox>
</div>
<section fxLayout="row" fxLayoutAlign="start center" fxLayout.lt-lg="column" fxLayoutAlign.lt-lg="center start"
fxFlex.gt-xs style="margin-bottom: 16px;">
fxFlex.gt-xs>
<span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate
style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true"
@ -43,7 +43,7 @@
</div>
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center"
fxLayoutGap="8px">
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" multiple
@ -63,8 +63,7 @@
</mat-select>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center"
fxLayoutGap="8px">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-grid #alarmTypeChipList formControlName="alarmTypeList">
@ -355,7 +354,7 @@
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.display-title' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title">
@ -365,12 +364,12 @@
<input matInput formControlName="titleTooltip">
</mat-form-field>
</div>
<fieldset class="fields-group" fxLayoutGap="8px" style="margin: 0">
<fieldset class="fields-group" fxLayoutGap.gt-xs="8px" style="margin: 0">
<legend class="group-title" translate>widget-config.title-icon</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row wrap" fxLayoutGap="8px">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<tb-material-icon-select fxFlex
formControlName="titleIcon">
</tb-material-icon-select>
@ -404,8 +403,8 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.widget-style</legend>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" fxFlex>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap.gt-xs="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<tb-color-input fxFlex
label="{{'widget-config.background-color' | translate}}"
icon="format_color_fill"
@ -419,7 +418,7 @@
formControlName="color">
</tb-color-input>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" fxFlex>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.padding</mat-label>
<input matInput formControlName="padding">
@ -477,8 +476,8 @@
<fieldset [formGroup]="layoutSettings" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.mobile-mode-settings</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title fxLayout.xs="column" fxLayoutAlign.xs="center start" fxLayout="row" fxLayoutGap="8px" fxFlex="70">
<mat-expansion-panel-header style="height: fit-content;">
<mat-panel-title fxLayout.xs="column" fxLayoutAlign.xs="center start" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex="70">
<mat-slide-toggle formControlName="mobileHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
@ -491,14 +490,14 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" fxFlex>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.order</mat-label>
<input matInput formControlName="mobileOrder" type="number" step="1">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.height</mat-label>
<input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1">
<input matInput formControlName="mobileHeight" type="number" min="1" step="1">
</mat-form-field>
</div>
</ng-template>

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

@ -255,7 +255,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
});
this.layoutSettings = this.fb.group({
mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]],
mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]],
mobileHide: [false],
desktopHide: [false]
});

332
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -15,172 +15,172 @@
limitations under the License.
-->
<div>
<mat-toolbar color="primary">
<h2 translate>device.add-device-text</h2>
<span fxFlex></span>
<div [tb-help]="'devices'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="deviceWizardFormGroup">
<form [formGroup]="deviceWizardFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device.wizard.device-details' | translate}}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('required')">
{{ 'device.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('maxlength')">
{{ 'device.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.label</mat-label>
<input matInput formControlName="label">
<mat-error *ngIf="deviceWizardFormGroup.get('label').hasError('maxlength')">
{{ 'device.label-max-length' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="16px">
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
<mat-radio-button [value]="0" color="primary">
<span translate>device.wizard.existing-device-profile</span>
</mat-radio-button>
<mat-radio-button [value]="1" color="primary">
<span translate>device.wizard.new-device-profile</span>
</mat-radio-button>
</mat-radio-group>
<div fxLayout="column">
<tb-device-profile-autocomplete
[required]="!createProfile"
formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
<mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
{{ 'device-profile.new-device-profile-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-rule-chain-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-queue-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
</div>
<mat-toolbar color="primary">
<h2 translate>device.add-device-text</h2>
<span fxFlex></span>
<div [tb-help]="'devices'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="deviceWizardFormGroup">
<form [formGroup]="deviceWizardFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device.wizard.device-details' | translate}}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('required')">
{{ 'device.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('maxlength')">
{{ 'device.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.label</mat-label>
<input matInput formControlName="label">
<mat-error *ngIf="deviceWizardFormGroup.get('label').hasError('maxlength')">
{{ 'device.label-max-length' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="16px">
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
<mat-radio-button [value]="0" color="primary">
<span translate>device.wizard.existing-device-profile</span>
</mat-radio-button>
<mat-radio-button [value]="1" color="primary">
<span translate>device.wizard.new-device-profile</span>
</mat-radio-button>
</mat-radio-group>
<div fxLayout="column">
<tb-device-profile-autocomplete
[required]="!createProfile"
formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
<mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
{{ 'device-profile.new-device-profile-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<mat-checkbox formControlName="gateway">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="deviceWizardFormGroup.get('gateway').value"
formControlName="overwriteActivityTime">
{{ 'device.overwrite-activity-time' | translate }}
</mat-checkbox>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-rule-chain-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
</div>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template><mat-form-field class="mat-block" style="padding-bottom: 14px;">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
<mat-step [stepControl]="credentialsFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credentials' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</form>
</mat-step>
<mat-step [stepControl]="customerFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
<form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="row">
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-queue-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
</div>
</div>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<mat-checkbox formControlName="gateway">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="deviceWizardFormGroup.get('gateway').value"
formControlName="overwriteActivityTime">
{{ 'device.overwrite-activity-time' | translate }}
</mat-checkbox>
</div>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template><mat-form-field class="mat-block" style="padding-bottom: 14px;">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
<mat-step [stepControl]="credentialsFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credentials' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</form>
</mat-step>
<mat-step [stepControl]="customerFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
<form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions style="padding: 0">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -191,8 +191,8 @@
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<mat-divider style="width: 100%"></mat-divider>
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end center">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>

33
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss

@ -15,6 +15,15 @@
*/
@import "../../../../../scss/constants";
:host {
height: 100%;
display: grid;
.dialog-actions-row {
padding: 8px;
}
}
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) {
@media #{$mat-lt-sm} {
.mat-mdc-dialog-content {
@ -36,31 +45,21 @@
.mat-stepper-horizontal {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
@media #{$mat-lt-sm} {
.mat-step-label {
white-space: normal;
overflow: visible;
.mat-step-text-label {
overflow: visible;
}
}
.mat-horizontal-stepper-wrapper {
flex: 1 1 100%;
}
.mat-horizontal-content-container {
height: 530px;
height: 680px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
scrollbar-gutter: stable;
@media #{$mat-gt-sm} {
min-width: 800px;
}
}
.mat-horizontal-stepper-content[aria-expanded=true] {
height: 100%;
form {
height: 100%;
min-width: 500px;
}
}
}

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -124,13 +124,13 @@
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
<div style="padding-bottom: 16px;" translate>dashboard.mobile-app-settings</div>
<div translate>dashboard.mobile-app-settings</div>
<tb-image-input fxFlex
label="{{'dashboard.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-checkbox fxFlex formControlName="mobileHide" style="padding-bottom: 16px; padding-top: 16px;">
<mat-checkbox fxFlex formControlName="mobileHide">
{{ 'dashboard.mobile-hide' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">

1
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss

@ -15,6 +15,7 @@
*/
:host {
button.tb-add-new-widget {
height: auto;
padding-right: 12px;
font-size: 24px;
border-style: dashed;

116
ui-ngx/src/app/shared/components/dashboard-select.component.ts

@ -16,6 +16,7 @@
import {
Component,
ElementRef,
forwardRef,
Inject,
Injector,
@ -45,8 +46,7 @@ import { WINDOW } from '@core/services/window.service';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DASHBOARD_SELECT_PANEL_DATA,
DashboardSelectPanelComponent,
DashboardSelectPanelData
DashboardSelectPanelComponent
} from './dashboard-select-panel.component';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -97,6 +97,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
private overlay: Overlay,
private breakpointObserver: BreakpointObserver,
private viewContainerRef: ViewContainerRef,
private nativeElement: ElementRef,
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
}
@ -131,77 +132,48 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
}
openDashboardSelectPanel() {
if (this.disabled) {
return;
}
const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150;
const panelWidth = 300;
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-dashboard-select-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
});
const el = this.dashboardSelectPanelOrigin.elementRef.nativeElement;
const offset = el.getBoundingClientRect();
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
}
if (leftX + panelWidth > wWidth) {
originX = 'end';
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.dashboardSelectPanelOrigin.elementRef)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injector = this._createDashboardSelectPanelInjector(
overlayRef,
{
dashboards$: this.dashboards$,
dashboardId: this.dashboardId,
onDashboardSelected: (dashboardId) => {
overlayRef.dispose();
this.dashboardId = dashboardId;
this.updateView();
if (!this.disabled) {
const config = new OverlayConfig({
panelClass: 'tb-dashboard-select-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true
});
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: DASHBOARD_SELECT_PANEL_DATA,
useValue: {
dashboards$: this.dashboards$,
dashboardId: this.dashboardId,
onDashboardSelected: (dashboardId) => {
overlayRef.dispose();
this.dashboardId = dashboardId;
this.updateView();
}
}
},
{
provide: OverlayRef,
useValue: overlayRef
}
}
);
overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
}
private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): Injector {
const providers: StaticProvider[] = [
{provide: DASHBOARD_SELECT_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
}
}
private updateView() {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save