diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index d2e1123d04..8436463afb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/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(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 1d429e9157..3fdc1bc236 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/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( diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 9fa8429f78..f001cc4822 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -95,6 +95,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); @Autowired + @Lazy private WebSocketService wsService; @Autowired diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java new file mode 100644 index 0000000000..8c4ca82783 --- /dev/null +++ b/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 implements SystemInfoService { + + public static final FutureCallback 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 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 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 telemetry) { + ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID); + telemetryService.saveAndNotifyInternal(TenantId.SYS_TENANT_ID, apiUsageState.getId(), telemetry, CALLBACK); + } + + private List getSystemData(ServiceInfo serviceInfo) { + List 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; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java new file mode 100644 index 0000000000..1ca0fb8fe0 --- /dev/null +++ b/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(); +} diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index ed4d43a5fd..d9f49ae4d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/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() diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index aeebc7bfee..f29827fbe2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/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 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 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 devices = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java new file mode 100644 index 0000000000..14fdfc4fac --- /dev/null +++ b/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 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 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 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 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 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 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 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 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(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java new file mode 100644 index 0000000000..5aa50c9336 --- /dev/null +++ b/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 { +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 08ac826c51..3cb99f5555 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/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(); diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 93e1327050..4907d0f015 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/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; } /** diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java new file mode 100644 index 0000000000..480e29a6a4 --- /dev/null +++ b/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; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java new file mode 100644 index 0000000000..ea46c91239 --- /dev/null +++ b/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 systemData; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java new file mode 100644 index 0000000000..e4510e0f48 --- /dev/null +++ b/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; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java index 203bd578ce..b5ecdcbf79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java +++ b/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; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 0a9e00e409..47e3af6a24 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/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(); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DiscoveryService.java index 4bc65c5e1e..e6aa604d00 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DiscoveryService.java +++ b/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 getOtherServers(); + + boolean isMonolith(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java index 7b04f3d0f6..8e2599ae34 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java +++ b/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 getOtherServers() { + return Collections.emptyList(); + } + + @Override + public boolean isMonolith() { + return true; + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 3fc1626f52..776d42a334 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/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> newPartitions = new ConcurrentHashMap<>(); + final ConcurrentMap> newPartitions = new ConcurrentHashMap<>(); partitionSizesMap.forEach((queueKey, size) -> { for (int i = 0; i < size; i++) { ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index f19375984e..2ff65ef07b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/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; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java index 4b03056253..e49cbbcfd9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java +++ b/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(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index 63f91935d3..fcf80bcf3d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/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 getOtherServers() { + @Override + public List 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; diff --git a/common/util/pom.xml b/common/util/pom.xml index a222724acd..18b08621ec 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -88,6 +88,10 @@ org.thingsboard.common data + + com.github.dblock + oshi-core + diff --git a/common/util/src/main/java/org/thingsboard/common/util/SystemUtil.java b/common/util/src/main/java/org/thingsboard/common/util/SystemUtil.java new file mode 100644 index 0000000000..686cfb28f2 --- /dev/null +++ b/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 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 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 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 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 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 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 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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index cbce9e48a1..a7c918f9ee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/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; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index 01edf184fd..65ab189081 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -92,7 +92,7 @@ public abstract class DataValidator> { } } - private static boolean doValidateEmail(String email) { + public static boolean doValidateEmail(String email) { if (email == null) { return false; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 821dbcc011..2885776a6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/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()) { diff --git a/pom.xml b/pom.xml index f910edd655..b577bf4142 100755 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,7 @@ 2.21.0 2.12.0 1.12.1 + 3.4.0 @@ -1994,6 +1995,11 @@ exp4j ${exp4j.version} + + com.github.dblock + oshi-core + ${oshi.version} + diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 1c6f589cd8..d2cebe3c35 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/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 getAlarmById(AlarmId alarmId) { try { ResponseEntity alarm = restTemplate.getForEntity(baseURL + "/api/alarm/{alarmId}", Alarm.class, alarmId.getId()); diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 9d07c751a5..440e61148e 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/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; diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index ea4c85eb64..813127cbc5 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/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 = ['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); + } + } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html index 12a3c542e1..e70b2d3cc8 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html @@ -42,7 +42,7 @@
+ *ngFor="let predicateControl of predicatesFormArray.controls; let $index = index">
{{ complexOperationTranslations.get(operation) | translate }}
@@ -68,7 +68,7 @@
- filter.no-filters diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index 809cd553c4..78e7252a72 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/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(); private propagateChange = null; - private valueChangeSubscription: Subscription = null; - constructor(private fb: UntypedFormBuilder, @Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType, 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): void { - if (this.valueChangeSubscription) { - this.valueChangeSubscription.unsubscribe(); - } - const predicateControls: Array = []; - 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 = []; + 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) { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 23135cd862..2d24bc479f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -64,6 +64,15 @@ matTooltipPosition="above"> edit + - - - -
-
- - -
- {{ 'device-profile.device-profile-details' | translate }} -
- - device-profile.name - - - {{ 'device-profile.name-required' | translate }} - - - {{ 'device-profile.name-max-length' | translate }} - - - - - - {{'device-profile.mobile-dashboard-hint' | translate}} - - - - - {{'device-profile.default-edge-rule-chain-hint' | translate}} - - - device-profile.type - - - {{deviceProfileTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.type-required' | translate }} - - - - - - device-profile.description - - -
-
-
- -
- {{ 'device-profile.transport-configuration' | translate }} + +

device-profile.add

+ +
+ +
+ + +
+
+ + + + {{ 'device-profile.device-profile-details' | translate }} +
- device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + {{ 'device-profile.name-max-length' | translate }} + + + + + + {{'device-profile.mobile-dashboard-hint' | translate}} + + + + + {{'device-profile.default-edge-rule-chain-hint' | translate}} + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} - - {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} + + {{ 'device-profile.type-required' | translate }} - - - - - -
- {{'device-profile.alarm-rules-with-count' | translate: - {count: alarmRulesFormGroup.get('alarms').value ? - alarmRulesFormGroup.get('alarms').value.length : 0} }} - - -
-
- -
- {{ 'device-profile.device-provisioning' | translate }} - - -
-
- -
-
+ + + + device-profile.description + + + + + + +
+ {{ 'device-profile.transport-configuration' | translate }} + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + + + +
+
+ +
+ {{'device-profile.alarm-rules-with-count' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+ +
+ {{ 'device-profile.device-provisioning' | translate }} + + +
+
+ +
+
+
@@ -143,8 +147,8 @@ [disabled]="(isLoading$ | async)" (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}
- -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss index eaef363192..ca54ae2053 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss +++ b/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; } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index c9b50727ea..3426593aed 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/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 implements AfterViewInit { @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatStepper; - + stepperOrientation: Observable; + stepperLabelPosition: Observable<'bottom' | 'end'>; selectedIndex = 0; showNext = true; @@ -102,10 +107,17 @@ export class AddDeviceProfileDialogComponent extends public dialogRef: MatDialogRef, 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)]], diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts index 40cc7e2ec6..04169214fd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts +++ b/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(); 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 = []; 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(); diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html index a7264483f3..d8c9c081f6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html @@ -59,10 +59,10 @@ - + {{ 'device-profile.propagate-alarm' | translate }} -
+
device-profile.alarm-rule-relation-types-list @@ -82,10 +82,10 @@
- + {{ 'device-profile.propagate-alarm-to-owner' | translate }} - + {{ 'device-profile.propagate-alarm-to-tenant' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html index a8f2e77fa5..57d64cbc7d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html @@ -17,7 +17,7 @@ -->
-
-
+
device-profile.no-alarm-rules
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts index 257263c2eb..a828040e9c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts +++ b/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(); private propagateChange = (v: any) => { }; constructor(private store: Store, - 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 | null): void { - if (this.valueChangeSubscription) { - this.valueChangeSubscription.unsubscribe(); - } - const alarmsControls: Array = []; - 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 = []; + 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 { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index b588a5ec34..cd711f9e12 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -31,7 +31,7 @@ device-profile.provision-device-key -
-
+
device-profile.snmp.please-add-communication-config
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts index 959fc51ec5..7e0f8dd3b5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts +++ b/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(); 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 = []; - 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 = []; + 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(); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html index d1e68b60c8..94fe06137f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html @@ -25,7 +25,7 @@
-
@@ -64,7 +64,7 @@
-
+
device-profile.snmp.please-add-mapping-config
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts index 5543bf6b11..428eb6b86e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts +++ b/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(); 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 = []; - 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 = []; + 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 { diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts index 2a29ec2e15..c3ff3ccc4c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts +++ b/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(); private propagateChange = (v: any) => { }; constructor(private store: Store, @@ -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 | null): void { - if (this.valueChangeSubscription$) { - this.valueChangeSubscription$.unsubscribe(); - } - const queuesControls: Array = []; - 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 = []; + 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) { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts index 70fb7af11f..09ae3f5139 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts +++ b/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.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
-
+
relation.type entity.entity-types  
-
+
+ *ngFor="let relationFilterControl of relationFiltersFormArray.controls; let $index = index">
-
+
relation.any-relation
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index 31e9ca2f9f..aa027c4846 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/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(); private propagateChange = null; - private valueChangeSubscription: Subscription = null; - constructor(protected store: Store, 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): void { - if (this.valueChangeSubscription) { - this.valueChangeSubscription.unsubscribe(); - } - const relationFiltersControls: Array = []; - 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 = []; + 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 { diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html index 8a6c551359..f510217d39 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html @@ -15,8 +15,10 @@ limitations under the License. --> -
- +
+ admin.repository-settings @@ -41,10 +43,10 @@ admin.default-branch - - {{ 'admin.repository-read-only' | translate }} - -
+
+ + {{ 'admin.repository-read-only' | translate }} + {{ 'admin.show-merge-commits' | translate }} @@ -74,6 +76,7 @@ +
diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss index a45a4cbfc1..80df7d9d79 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss index f0e7f857a3..e5f6c02ef7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss +++ b/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; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 5b233ba2d1..18290225b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/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); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index 7e3bb168ae..fcb90b06a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html index a02bd79458..595c32f7a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html @@ -17,7 +17,7 @@ -->
- {{ column.title }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html index 3091ac7bc6..c0680dfeb9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html @@ -51,7 +51,7 @@ -
+
{{ 'widgets.table.display-entity-name' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html index ff6b2f6bdc..743ecbf8bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html @@ -47,7 +47,7 @@
widgets.label-widget.label-position -
+
widgets.label-widget.x-pos diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.html index c1f5a26cf1..6f56ee9ecd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.html @@ -24,7 +24,7 @@
widgets.chart.border-settings -
+
widgets.chart.border-width diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html index e860995acf..307a494954 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html @@ -41,7 +41,7 @@
widgets.chart.stroke-settings -
+
widgets.chart.width-pixels diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html index 6ec910fa08..1fa1aacd92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html @@ -16,7 +16,7 @@ -->
-
+
widgets.widget-font.font-family @@ -26,7 +26,7 @@
-
+
widgets.widget-font.font-style @@ -68,7 +68,7 @@
-
+
widgets.rpc.initial-value -
+
widgets.rpc.min-value diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html index 5db8315329..34105fa0e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -33,7 +33,7 @@
-
+
{{ 'widgets.persistent-table.display-request-details' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html index 3e330e78ac..74e782c901 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html @@ -24,7 +24,7 @@ {{ 'widgets.rpc.button-primary' | translate }} -
+
widgets.rpc.slide-toggle-label -
+
widgets.rpc.label-position diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html index 0de8aabd0a..bbb7874523 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html +++ b/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> -
+
widgets.gauge.minor-ticks-count @@ -71,7 +71,7 @@ {{ 'widgets.gauge.show-plate-border' | translate }} -
+
widgets.gauge.needle-circle-size -
+
widgets.gauge.ticks-settings -
+
widgets.gauge.min-value @@ -41,7 +41,7 @@
-
+
widgets.gauge.major-ticks-count @@ -52,7 +52,7 @@ label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
-
+
widgets.gauge.minor-ticks-count @@ -127,7 +127,7 @@ widgets.gauge.value-font
-
+
-
+
widgets.gauge.needle-settings -
+
-
+
widgets.gauge.radial-gauge-settings -
+
widgets.gauge.start-ticks-angle @@ -314,7 +314,7 @@
widgets.gauge.linear-gauge-settings -
+
widgets.gauge.bar-stroke-width @@ -325,7 +325,7 @@ label="{{ 'widgets.gauge.bar-stroke-color' | translate }}" openOnInput colorClearButton>
-
+
-
+
widgets.gauge.common-settings -
+
widgets.gauge.min-value @@ -182,7 +182,7 @@
widgets.gauge.unit-title-and-timestamp-settings -
+
{{ 'widgets.gauge.show-unit-title' | translate }} @@ -191,7 +191,7 @@
-
+
{{ 'widgets.gauge.show-timestamp' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html index d03f7dc8a1..f92edfcde5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html @@ -40,7 +40,7 @@
-
+
widgets.gauge.highlight-from diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html index 472b52555c..9264b82b3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html @@ -45,7 +45,7 @@
-
+
widgets.gpio.pin @@ -55,7 +55,7 @@
-
+
widgets.gpio.row diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html index f23924c9be..a91f3af9a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html @@ -28,7 +28,7 @@
widgets.input-widgets.checkbox-settings -
+
widgets.input-widgets.true-label diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html index 19b695c4b1..512ccc9f8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html @@ -21,7 +21,7 @@
widgets.input-widgets.double-field-settings -
+
widgets.input-widgets.min-value diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html index 1ec19405bb..d5ab9c976d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html @@ -21,7 +21,7 @@
widgets.input-widgets.integer-field-settings -
+
widgets.input-widgets.min-value diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html index a80ab4e6e9..732b98dc05 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html @@ -37,7 +37,7 @@
widgets.input-widgets.attribute-settings -
+
widgets.input-widgets.widget-mode diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html index 0344b51d32..f02f840645 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html @@ -25,7 +25,7 @@ {{ 'widgets.input-widgets.show-result-message' | translate }} -
+
widgets.input-widgets.latitude-key-name @@ -47,7 +47,7 @@ {{ 'widgets.input-widgets.show-label' | translate }} -
+
widgets.input-widgets.latitude-label diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html index f6239b3e61..9c250e5968 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html @@ -45,7 +45,7 @@ {{ 'widgets.input-widgets.update-all-values' | translate }} -
+
widgets.input-widgets.save-button-label diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html index 2b0f1b4599..53ea6b67c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html @@ -21,7 +21,7 @@
widgets.input-widgets.text-field-settings -
+
widgets.input-widgets.min-length diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html index 8f9269690e..41648b5168 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html @@ -18,7 +18,7 @@
widgets.maps.common-map-settings -
+
-
+
widgets.maps.default-map-zoom-level @@ -65,7 +65,7 @@
-
+
{{ 'widgets.maps.disable-scroll-zooming' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html index 49dfc84c5a..403721f65c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html @@ -34,7 +34,7 @@ {{ 'widgets.maps.zoom-on-cluster-click' | translate }} -
+
widgets.maps.max-cluster-zoom diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html index c1445a867d..2b5de971b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html @@ -18,7 +18,7 @@
widgets.maps.markers-settings -
+
widgets.maps.marker-offset-x @@ -113,7 +113,7 @@ functionTitle="{{ 'widgets.maps.tooltip-function' | translate }}" helpId="widget/lib/map/tooltip_fn"> -
+
widgets.maps.tooltip-offset-x diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html index 9e7756d092..a22ce3e37e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html @@ -18,7 +18,7 @@
widgets.maps.route-map-settings -
+
widgets.maps.stroke-weight diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 68e89ef8d0..20cf5860b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/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' + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 0b4c433bef..a4e34029f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -19,8 +19,8 @@
-
+ fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"> +
{{ 'widget-config.use-dashboard-timewindow' | translate }} @@ -29,7 +29,7 @@
+ fxFlex.gt-xs> widget-config.timewindow
+ fxLayoutGap.gt-xs="8px"> alarm.alarm-status-list
-
+
alarm.alarm-type-list @@ -355,7 +354,7 @@ {{ 'widget-config.display-title' | translate }} -
+
widget-config.title @@ -365,12 +364,12 @@
-
+
widget-config.title-icon {{ 'widget-config.display-icon' | translate }} -
+
@@ -404,8 +403,8 @@
widget-config.widget-style -
-
+
+
-
+
widget-config.padding @@ -477,8 +476,8 @@
widget-config.mobile-mode-settings - - + + {{ 'widget-config.mobile-hide' | translate }} @@ -491,14 +490,14 @@ -
+
widget-config.order widget-config.height - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 46b56a1a36..edc5983a24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/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] }); diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 4fae225ea9..c1485cf450 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -15,172 +15,172 @@ limitations under the License. --> -
- -

device.add-device-text

- -
- -
- - -
-
- - - check - - - - {{ 'device.wizard.device-details' | translate}} -
- - device.name - - - {{ 'device.name-required' | translate }} - - - {{ 'device.name-max-length' | translate }} - - - - device.label - - - {{ 'device.label-max-length' | translate }} - - -
- - - device.wizard.existing-device-profile - - - device.wizard.new-device-profile - - -
- - - - device-profile.new-device-profile-name - - - {{ 'device-profile.new-device-profile-name-required' | translate }} - - -
-
- - -
-
- - -
+ +

device.add-device-text

+ +
+ +
+ + +
+
+ + + check + + + + {{ 'device.wizard.device-details' | translate}} +
+ + device.name + + + {{ 'device.name-required' | translate }} + + + {{ 'device.name-max-length' | translate }} + + + + device.label + + + {{ 'device.label-max-length' | translate }} + + +
+ + + device.wizard.existing-device-profile + + + device.wizard.new-device-profile + + +
+ + + + device-profile.new-device-profile-name + + + {{ 'device-profile.new-device-profile-name-required' | translate }} + +
-
- - {{ 'device.is-gateway' | translate }} - - - {{ 'device.overwrite-activity-time' | translate }} - +
+ +
- - device.description - - -
- -
- -
- {{ 'device-profile.transport-configuration' | translate }} - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - - - -
-
- -
- {{'device-profile.alarm-rules-with-count' | translate: - {count: alarmRulesFormGroup.get('alarms').value ? - alarmRulesFormGroup.get('alarms').value.length : 0} }} - - -
-
- -
- {{ 'device-profile.device-provisioning' | translate }} - - -
-
- - {{ 'device.credentials' | translate }} -
- {{ 'device.wizard.add-credentials' | translate }} - - -
-
- - {{ 'customer.customer' | translate }} -
- - -
-
-
-
-
+
+ + +
+
+
+ + {{ 'device.is-gateway' | translate }} + + + {{ 'device.overwrite-activity-time' | translate }} + +
+ + device.description + + +
+ +
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + + + +
+
+ +
+ {{'device-profile.alarm-rules-with-count' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+ +
+ {{ 'device-profile.device-provisioning' | translate }} + + +
+
+ + {{ 'device.credentials' | translate }} +
+ {{ 'device.wizard.add-credentials' | translate }} + + +
+
+ + {{ 'customer.customer' | translate }} +
+ + +
+
+
+
+
+
@@ -191,8 +191,8 @@ [disabled]="(isLoading$ | async)" (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}
- -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index 292d6b3867..0fe18467fd 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/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; } } } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 61367ab02f..3514b0d919 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -124,13 +124,13 @@
-
dashboard.mobile-app-settings
+
dashboard.mobile-app-settings
- + {{ 'dashboard.mobile-hide' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss index c0f3e21329..8fcd595570 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss +++ b/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; diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.ts b/ui-ngx/src/app/shared/components/dashboard-select.component.ts index c14e3924d2..40db63932a 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select.component.ts +++ b/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() { diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 2b69495a3c..6d8a424352 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -16,7 +16,7 @@ --> - {{ entityText | translate }} + {{ label | translate }} - {{ entityRequiredText | translate }} + {{ requiredErrorText | translate }} diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 18a79fdb51..6a7e6c087c 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -36,10 +36,10 @@ import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { BaseData } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { isEqual } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coerce-boolean'; @Component({ selector: 'tb-entity-autocomplete', @@ -61,6 +61,22 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit entitySubtypeValue: string; + entityText: string; + + noEntitiesMatchingText: string; + + entityRequiredText: string; + + filteredEntities: Observable>>; + + searchText = ''; + + private dirty = false; + + private refresh$ = new Subject>>(); + + private propagateChange = (v: any) => { }; + @Input() set entityType(entityType: EntityType) { if (this.entityTypeValue !== entityType) { @@ -100,16 +116,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @Input() appearance: MatFormFieldAppearance = 'fill'; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() + @coerceBoolean() disabled: boolean; @Output() @@ -117,19 +129,20 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @ViewChild('entityInput', {static: true}) entityInput: ElementRef; - entityText: string; - noEntitiesMatchingText: string; - entityRequiredText: string; - - filteredEntities: Observable>>; - - searchText = ''; - - private dirty = false; + get requiredErrorText(): string { + if (this.requiredText && this.requiredText.length) { + return this.requiredText; + } + return this.entityRequiredText; + } - private refresh$ = new Subject>>(); + get label(): string { + if (this.labelText && this.labelText.length) { + return this.labelText; + } + return this.entityText; + } - private propagateChange = (v: any) => { }; constructor(private store: Store, public translate: TranslateService, @@ -249,12 +262,6 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit break; } } - if (this.labelText && this.labelText.length) { - this.entityText = this.labelText; - } - if (this.requiredText && this.requiredText.length) { - this.entityRequiredText = this.requiredText; - } const currentEntity = this.getCurrentEntity(); if (currentEntity) { const currentEntityType = currentEntity.id.entityType; @@ -342,12 +349,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit map((data) => { if (data) { if (this.excludeEntityIds && this.excludeEntityIds.length) { + const excludeEntityIdsSet = new Set(this.excludeEntityIds); const entities: Array> = []; - data.forEach((entity) => { - if (this.excludeEntityIds.indexOf(entity.id.id) === -1) { - entities.push(entity); - } - }); + data.forEach(entity => !excludeEntityIdsSet.has(entity.id.id) && entities.push(entity)); return entities; } else { return data; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html index ff2be0d53e..bc03a7f61b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ entitySubtypeText | translate }} ; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef; selectEntitySubtypeText: string; @@ -238,9 +245,14 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, break; } if (subTypesObservable) { + const excludeSubTypesSet = new Set(this.excludeSubTypes); this.subTypes = subTypesObservable.pipe( catchError(() => of([] as Array)), - map(subTypes => subTypes.map(subType => subType.type)), + map(subTypes => { + const filteredSubTypes: Array = []; + subTypes.forEach(subType => !excludeSubTypesSet.has(subType.type) && filteredSubTypes.push(subType.type)); + return filteredSubTypes; + }), publishReplay(1), refCount() ); diff --git a/ui-ngx/src/app/shared/components/help.component.html b/ui-ngx/src/app/shared/components/help.component.html index c5603ad2cd..5ebac09c00 100644 --- a/ui-ngx/src/app/shared/components/help.component.html +++ b/ui-ngx/src/app/shared/components/help.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -