diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json new file mode 100644 index 0000000000..fc5b13154c --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -0,0 +1,50 @@ +{ + "widgetsBundle": { + "alias": "home_page_widgets", + "title": "Home page widgets", + "image": null, + "description": null, + "externalId": null, + "name": "Home page widgets" + }, + "widgetTypes": [ + { + "alias": "documentation_links", + "name": "Documentation links", + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-doc-links-widget-settings", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true}" + } + }, + { + "alias": "getting_started", + "name": "Getting started", + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}" + } + } + ] +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 200a6c71d2..7a396ff795 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -84,6 +84,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke private long sendTimeout; @Value("${server.ws.ping_timeout:30000}") private long pingTimeout; + @Value("${server.ws.max_queue_messages_per_session:1000}") + private int wsMaxQueueMessagesPerSession; private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); private final ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); @@ -143,9 +145,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke return; } var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + int wsTenantProfileQueueLimit = tenantProfileConfiguration != null ? + tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : wsMaxQueueMessagesPerSession; internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, - tenantProfileConfiguration != null && tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ? - tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500)); + (wsTenantProfileQueueLimit > 0 && wsTenantProfileQueueLimit < wsMaxQueueMessagesPerSession) ? + wsTenantProfileQueueLimit : wsMaxQueueMessagesPerSession)); externalSessionMap.put(externalSessionId, internalSessionId); processInWebSocketService(sessionRef, SessionEvent.onEstablished()); diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java index ced982f5e6..db8247bb93 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java @@ -15,8 +15,9 @@ */ package org.thingsboard.server.service.apiusage; +import lombok.Data; import lombok.Getter; -import org.springframework.data.util.Pair; +import lombok.Setter; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; @@ -26,17 +27,17 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.tools.SchedulerUtils; -import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public abstract class BaseApiUsageState { private final Map currentCycleValues = new ConcurrentHashMap<>(); private final Map currentHourValues = new ConcurrentHashMap<>(); + private final Map> lastGaugesByServiceId = new HashMap<>(); + private final Map gaugesReportCycles = new HashMap<>(); + @Getter private final ApiUsageState apiUsageState; @Getter @@ -46,6 +47,9 @@ public abstract class BaseApiUsageState { @Getter private volatile long currentHourTs; + @Setter + private long gaugeReportInterval; + public BaseApiUsageState(ApiUsageState apiUsageState) { this.apiUsageState = apiUsageState; this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth(); @@ -53,43 +57,76 @@ public abstract class BaseApiUsageState { this.currentHourTs = SchedulerUtils.getStartOfCurrentHour(); } - public void put(ApiUsageRecordKey key, Long value) { - currentCycleValues.put(key, value); + public StatsCalculationResult calculate(ApiUsageRecordKey key, long value, String serviceId) { + long currentValue = get(key); + long currentHourlyValue = getHourly(key); + + long newValue; + long newHourlyValue; + if (key.isCounter()) { + newValue = currentValue + value; + newHourlyValue = currentHourlyValue + value; + } else { + Long newGaugeValue = calculateGauge(key, value, serviceId); + newValue = newGaugeValue != null ? newGaugeValue : currentValue; + newHourlyValue = newGaugeValue != null ? Math.max(newGaugeValue, currentHourlyValue) : currentHourlyValue; + } + set(key, newValue); + setHourly(key, newHourlyValue); + + return StatsCalculationResult.of(newValue, newHourlyValue); } - public void putHourly(ApiUsageRecordKey key, Long value) { - currentHourValues.put(key, value); + private Long calculateGauge(ApiUsageRecordKey key, long value, String serviceId) { + Map lastByServiceId = lastGaugesByServiceId.computeIfAbsent(key, k -> { + gaugesReportCycles.put(key, System.currentTimeMillis()); + return new HashMap<>(); + }); + lastByServiceId.put(serviceId, value); + + Long gaugeReportCycle = gaugesReportCycles.get(key); + if (gaugeReportCycle <= System.currentTimeMillis() - gaugeReportInterval) { + long newValue = lastByServiceId.values().stream().mapToLong(Long::longValue).sum(); + lastGaugesByServiceId.remove(key); + gaugesReportCycles.remove(key); + return newValue; + } else { + return null; + } } - public long add(ApiUsageRecordKey key, long value) { - long result = currentCycleValues.getOrDefault(key, 0L) + value; - currentCycleValues.put(key, result); - return result; + public void set(ApiUsageRecordKey key, Long value) { + currentCycleValues.put(key, value); } public long get(ApiUsageRecordKey key) { return currentCycleValues.getOrDefault(key, 0L); } - public long addToHourly(ApiUsageRecordKey key, long value) { - long result = currentHourValues.getOrDefault(key, 0L) + value; - currentHourValues.put(key, result); - return result; + public void setHourly(ApiUsageRecordKey key, Long value) { + currentHourValues.put(key, value); + } + + public long getHourly(ApiUsageRecordKey key) { + return currentHourValues.getOrDefault(key, 0L); } public void setHour(long currentHourTs) { this.currentHourTs = currentHourTs; - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - currentHourValues.put(key, 0L); - } + currentHourValues.clear(); + lastGaugesByServiceId.clear(); + gaugesReportCycles.clear(); } public void setCycles(long currentCycleTs, long nextCycleTs) { this.currentCycleTs = currentCycleTs; this.nextCycleTs = nextCycleTs; - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - currentCycleValues.put(key, 0L); - } + currentCycleValues.clear(); + } + + public void onRepartitionEvent() { + lastGaugesByServiceId.clear(); + gaugesReportCycles.clear(); } public ApiUsageStateValue getFeatureValue(ApiFeature feature) { @@ -150,4 +187,11 @@ public abstract class BaseApiUsageState { public EntityId getEntityId() { return getApiUsageState().getEntityId(); } + + @Data(staticConstructor = "of") + public static class StatsCalculationResult { + private final long newValue; + private final long newHourlyValue; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index b4a625e7de..0b3a4b8acd 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -48,12 +48,12 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.SchedulerUtils; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -62,6 +62,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.service.apiusage.BaseApiUsageState.StatsCalculationResult; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.partition.AbstractPartitionBasedService; @@ -128,6 +129,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService @Value("${usage.stats.check.cycle:60000}") private long nextCycleCheckInterval; + @Value("${usage.stats.gauge_report_interval:180000}") + private long gaugeReportInterval; + private final Lock updateLock = new ReentrantLock(); @PostConstruct @@ -155,19 +159,19 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService ToUsageStatsServiceMsg statsMsg = msg.getValue(); TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); - EntityId entityId; + EntityId ownerId; if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { - entityId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); + ownerId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); } else { - entityId = tenantId; + ownerId = tenantId; } - processEntityUsageStats(tenantId, entityId, statsMsg.getValuesList()); + processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), statsMsg.getServiceId()); callback.onSuccess(); } - private void processEntityUsageStats(TenantId tenantId, EntityId entityId, List values) { - if (deletedEntities.contains(entityId)) return; + private void processEntityUsageStats(TenantId tenantId, EntityId ownerId, List values, String serviceId) { + if (deletedEntities.contains(ownerId)) return; BaseApiUsageState usageState; List updatedEntries; @@ -175,7 +179,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService updateLock.lock(); try { - usageState = getOrFetchState(tenantId, entityId); + usageState = getOrFetchState(tenantId, ownerId); long ts = usageState.getCurrentCycleTs(); long hourTs = usageState.getCurrentHourTs(); long newHourTs = SchedulerUtils.getStartOfCurrentHour(); @@ -184,13 +188,18 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); Set apiFeatures = new HashSet<>(); - for (UsageStatsKVProto kvProto : values) { - ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey()); - long newValue = usageState.add(recordKey, kvProto.getValue()); + for (UsageStatsKVProto statsItem : values) { + ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey()); + + StatsCalculationResult calculationResult = usageState.calculate(recordKey, statsItem.getValue(), serviceId); + long newValue = calculationResult.getNewValue(); + long newHourlyValue = calculationResult.getNewHourlyValue(); + updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue))); - long newHourlyValue = usageState.addToHourly(recordKey, kvProto.getValue()); updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue))); - apiFeatures.add(recordKey.getApiFeature()); + if (recordKey.getApiFeature() != null) { + apiFeatures.add(recordKey.getApiFeature()); + } } if (usageState.getEntityType() == EntityType.TENANT && !usageState.getEntityId().equals(TenantId.SYS_TENANT_ID)) { result = ((TenantApiUsageState) usageState).checkStateUpdatedDueToThreshold(apiFeatures); @@ -418,26 +427,26 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK); } - BaseApiUsageState getOrFetchState(TenantId tenantId, EntityId entityId) { - if (entityId == null || entityId.isNullUid()) { - entityId = tenantId; + BaseApiUsageState getOrFetchState(TenantId tenantId, EntityId ownerId) { + if (ownerId == null || ownerId.isNullUid()) { + ownerId = tenantId; } - BaseApiUsageState state = myUsageStates.get(entityId); + BaseApiUsageState state = myUsageStates.get(ownerId); if (state != null) { return state; } - ApiUsageState storedState = apiUsageStateService.findApiUsageStateByEntityId(entityId); + ApiUsageState storedState = apiUsageStateService.findApiUsageStateByEntityId(ownerId); if (storedState == null) { try { - storedState = apiUsageStateService.createDefaultApiUsageState(tenantId, entityId); + storedState = apiUsageStateService.createDefaultApiUsageState(tenantId, ownerId); } catch (Exception e) { - storedState = apiUsageStateService.findApiUsageStateByEntityId(entityId); + storedState = apiUsageStateService.findApiUsageStateByEntityId(ownerId); } } - if (entityId.getEntityType() == EntityType.TENANT) { - if (!entityId.equals(TenantId.SYS_TENANT_ID)) { - state = new TenantApiUsageState(tenantProfileCache.get((TenantId) entityId), storedState); + if (ownerId.getEntityType() == EntityType.TENANT) { + if (!ownerId.equals(TenantId.SYS_TENANT_ID)) { + state = new TenantApiUsageState(tenantProfileCache.get((TenantId) ownerId), storedState); } else { state = new TenantApiUsageState(storedState); } @@ -456,26 +465,27 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService cycleEntryFound = true; boolean oldCount = tsKvEntry.getTs() == state.getCurrentCycleTs(); - state.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L); + state.set(key, oldCount ? tsKvEntry.getLongValue().get() : 0L); if (!oldCount) { newCounts.add(key); } } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) { hourlyEntryFound = true; - state.putHourly(key, tsKvEntry.getTs() == state.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); + state.setHourly(key, tsKvEntry.getTs() == state.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); } if (cycleEntryFound && hourlyEntryFound) { break; } } } - log.debug("[{}] Initialized state: {}", entityId, storedState); - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + state.setGaugeReportInterval(gaugeReportInterval); + log.debug("[{}] Initialized state: {}", ownerId, storedState); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId); if (tpi.isMyPartition()) { addEntityState(tpi, state); } else { - otherUsageStates.put(entityId, state.getApiUsageState()); + otherUsageStates.put(ownerId, state.getApiUsageState()); } saveNewCounts(state, newCounts); } catch (InterruptedException | ExecutionException e) { @@ -489,6 +499,12 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService protected void onRepartitionEvent() { otherUsageStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getValue().getTenantId(), entry.getKey()).isMyPartition()); + updateLock.lock(); + try { + myUsageStates.values().forEach(BaseApiUsageState::onRepartitionEvent); + } finally { + updateLock.unlock(); + } } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 1e30980df0..b6ed5eb664 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import io.grpc.Server; @@ -25,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; @@ -35,6 +37,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; @@ -66,6 +71,10 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT; +import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT; +import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; + @Service @Slf4j @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @@ -261,7 +270,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i newEventLock.unlock(); } save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); - save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, System.currentTimeMillis()); + long lastConnectTs = System.currentTimeMillis(); + save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); + pushRuleEngineMessage(edgeGrpcSession.getEdge().getTenantId(), edgeId, lastConnectTs, CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } @@ -365,7 +376,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void onEdgeDisconnect(EdgeId edgeId) { log.info("[{}] edge disconnected!", edgeId); - sessions.remove(edgeId); + EdgeGrpcSession removed = sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { @@ -374,7 +385,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i newEventLock.unlock(); } save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); - save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, System.currentTimeMillis()); + long lastDisconnectTs = System.currentTimeMillis(); + save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); + pushRuleEngineMessage(removed.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); } @@ -423,4 +436,26 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.warn("[{}] Failed to update attribute [{}] with value [{}]", edgeId, key, value, t); } } + + private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, String msgType) { + try { + ObjectNode edgeState = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + if (msgType.equals(CONNECT_EVENT)) { + edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); + edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); + } else { + edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false); + edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts); + } + String data = JacksonUtil.toString(edgeState); + TbMsgMetaData md = new TbMsgMetaData(); + if (!persistToTelemetry) { + md.putValue(DataConstants.SCOPE, SERVER_SCOPE); + } + TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); + clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push {}", tenantId, edgeId, msgType, e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index bd68aad316..f1183bea8b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -508,6 +508,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("entity_admin_widgets"); this.deleteSystemWidgetBundle("navigation_widgets"); this.deleteSystemWidgetBundle("edge_widgets"); + this.deleteSystemWidgetBundle("home_page_widgets"); installScripts.loadSystemWidgets(); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java index bc80a22e1b..1eafe66c8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java @@ -20,7 +20,6 @@ import ua_parser.Client; import ua_parser.Parser; import javax.servlet.http.HttpServletRequest; -import java.io.IOException; import java.io.Serializable; @Data @@ -43,11 +42,7 @@ public class RestAuthenticationDetails implements Serializable { } private static Client getUserAgent(HttpServletRequest request) { - try { - Parser uaParser = new Parser(); - return uaParser.parse(request.getHeader("User-Agent")); - } catch (IOException e) { - return new Client(null, null, null); - } + Parser uaParser = new Parser(); + return uaParser.parse(request.getHeader("User-Agent")); } } diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 60c159360b..005cc6ac4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -26,6 +26,7 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; @@ -33,6 +34,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; @@ -61,6 +63,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.sql.query.EntityQueryRepository; @@ -85,6 +88,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -151,6 +155,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService> devicesActivity = new HashMap<>(); partitionedEntities.forEach((tpi, deviceIds) -> { log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size()); for (DeviceId deviceId : deviceIds) { + DeviceStateData stateData; try { - updateInactivityStateIfExpired(ts, deviceId); + stateData = getOrFetchDeviceStateData(deviceId); + } catch (Exception e) { + log.error("[{}] Failed to get or fetch device state data", deviceId, e); + continue; + } + try { + updateInactivityStateIfExpired(ts, deviceId, stateData); } catch (Exception e) { log.warn("[{}] Failed to update inactivity state", deviceId, e); } + Pair tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(), + tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger())); + if (stateData.getState().isActive()) { + tenantDevicesActivity.getLeft().incrementAndGet(); + } else { + tenantDevicesActivity.getRight().incrementAndGet(); + } + } + }); + devicesActivity.forEach((tenantId, tenantDevicesActivity) -> { + int active = tenantDevicesActivity.getLeft().get(); + int inactive = tenantDevicesActivity.getRight().get(); + apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.ACTIVE_DEVICES, active); + apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.INACTIVE_DEVICES, inactive); + if (active > 0) { + log.info("[{}] Active devices: {}, inactive devices: {}", tenantId, active, inactive); } }); } catch (Throwable t) { - log.warn("Failed to update inactivity states", t); + log.warn("Failed to check devices states", t); } } - void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { - DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); - updateInactivityStateIfExpired(ts, deviceId, stateData); - } - void updateInactivityStateIfExpired(long ts, DeviceId deviceId, DeviceStateData stateData) { log.trace("Processing state {} for device {}", stateData, deviceId); if (stateData != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index 2da996953f..611b536808 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -22,9 +22,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ShortCustomerInfo; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; 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 index caf3abefa0..cce38cfd18 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java @@ -26,10 +26,12 @@ 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.StringUtils; 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.BooleanDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -50,6 +52,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.annotation.Nullable; import javax.annotation.PreDestroy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; @@ -143,9 +146,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener".equalsIgnoreCase(mailFrom.asText()); } return false; } @@ -154,13 +155,13 @@ public class DefaultSystemInfoService extends TbApplicationEventListener clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo()); BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData))); - doSave(Collections.singletonList(clusterDataKv)); + doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv)); } private void saveCurrentMonolithSystemInfo() { long ts = System.currentTimeMillis(); List tsList = new ArrayList<>(); - + tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false))); getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v)))); getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v)))); getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (long) v)))); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index ffa6d7bf48..b5bb4f054f 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -416,6 +416,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } private void addVoidCallback(ListenableFuture saveFuture, final FutureCallback callback) { + if (callback == null) return; Futures.addCallback(saveFuture, new FutureCallback() { @Override public void onSuccess(@Nullable S result) { 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 6b41ce00ff..0f80f0e3f6 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 @@ -21,6 +21,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.info.BuildProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -65,8 +68,8 @@ public class DefaultUpdateService implements UpdateService { private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service")); - private ScheduledFuture checkUpdatesFuture = null; - private RestTemplate restClient = new RestTemplate(); + private ScheduledFuture checkUpdatesFuture = null; + private final RestTemplate restClient = new RestTemplate(); private UpdateMessage updateMessage; @@ -77,7 +80,9 @@ public class DefaultUpdateService implements UpdateService { @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void init() { version = buildProperties != null ? buildProperties.getVersion() : "unknown"; - updateMessage = new UpdateMessage(false, version, "", "", "", ""); + updateMessage = new UpdateMessage(false, version, "", "", + "https://thingsboard.io/docs/reference/releases", + "https://thingsboard.io/docs/reference/releases"); if (updatesEnabled) { try { platform = System.getProperty("platform", "unknown"); @@ -94,7 +99,7 @@ public class DefaultUpdateService implements UpdateService { Path instanceIdPath = Paths.get(INSTANCE_ID_FILE); if (instanceIdPath.toFile().exists()) { byte[] data = Files.readAllBytes(instanceIdPath); - if (data != null && data.length > 0) { + if (data.length > 0) { try { result = UUID.fromString(new String(data)); } catch (IllegalArgumentException e) { @@ -124,13 +129,15 @@ public class DefaultUpdateService implements UpdateService { Runnable checkUpdatesRunnable = () -> { try { log.trace("Executing check update method for instanceId [{}], platform [{}] and version [{}]", instanceId, platform, version); + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); ObjectNode request = new ObjectMapper().createObjectNode(); request.put(PLATFORM_PARAM, platform); request.put(VERSION_PARAM, version); request.put(INSTANCE_ID_PARAM, instanceId.toString()); UpdateMessage prevUpdateMessage = updateMessage; - updateMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v2/thingsboard/updates", request, UpdateMessage.class); - if (updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) { + updateMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v2/thingsboard/updates", new HttpEntity<>(request.toString(), headers), UpdateMessage.class); + if (updateMessage != null && updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) { notificationRuleProcessor.process(NewPlatformVersionTrigger.builder() .updateInfo(updateMessage) .build()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 94751fc23b..7633c6feef 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -65,6 +65,7 @@ server: max_per_user: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_MAX_PER_USER:10}" max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}" max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}" + max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}" rest: server_side_rpc: # Minimum value of the server side RPC timeout. May override value provided in the REST API call. @@ -140,6 +141,7 @@ usage: interval: "${USAGE_STATS_REPORT_INTERVAL:10}" check: cycle: "${USAGE_STATS_CHECK_CYCLE:60000}" + gauge_report_interval: "${USAGE_STATS_GAUGE_REPORT_INTERVAL:180000}" # UI parameters ui: @@ -987,6 +989,13 @@ queue: print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 8f58f01225..f49e6671c9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -30,10 +30,8 @@ import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.hibernate.exception.ConstraintViolationException; import org.junit.After; -import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; @@ -78,6 +76,9 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -533,6 +534,17 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return assetProfile; } + protected Device createDevice(String name, String accessToken) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType("default"); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + } + protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration, boolean sendAckOnValidationException) { MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration(); mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_TELEMETRY_TOPIC); @@ -897,6 +909,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (T) field.get(target); } + protected void setStaticFieldValue(Class targetCls, String fieldName, Object value) throws Exception { + Field field = targetCls.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(null, value); + } + protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) { DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId); Map subscriptions = (Map) ReflectionTestUtils.getField(processor, getMapName(featureType)); 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 53960ed220..f5d8303a80 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -107,7 +107,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Thread.sleep(1); } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityCountQuery countQuery = new EntityCountQuery(filter); @@ -115,11 +115,11 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); Assert.assertEquals(97, count.longValue()); - filter.setDeviceType("unknown"); + filter.setDeviceTypes(List.of("unknown")); count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); Assert.assertEquals(0, count.longValue()); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter("Device1"); count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); @@ -341,7 +341,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -442,7 +442,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Thread.sleep(1000); DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -521,7 +521,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); KeyFilter highTemperatureFilter = new KeyFilter(); @@ -576,7 +576,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe @Test public void givenInvalidEntityDataPageLink_thenReturnError() throws Exception { DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); String invalidSortProperty = "created(Time)"; diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index d29f572758..75283b63f1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -585,7 +585,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @Test public void testGetTelemetryWhenEntityViewTimeRangeInsideTimestampRange() throws Exception { - DeviceTypeFilter dtf = new DeviceTypeFilter(testDevice.getType(), testDevice.getName()); + DeviceTypeFilter dtf = new DeviceTypeFilter(List.of(testDevice.getType()), testDevice.getName()); List tsKeys = List.of("tsKey1", "tsKey2", "tsKey3"); DeviceCredentials deviceCredentials = doGet("/api/device/" + testDevice.getId().getId() + "/credentials", DeviceCredentials.class); @@ -684,7 +684,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes } private Set putAttributesAndWait(String stringKV, Set expectedKeySet) throws Exception { - DeviceTypeFilter dtf = new DeviceTypeFilter(testDevice.getType(), testDevice.getName()); + DeviceTypeFilter dtf = new DeviceTypeFilter(List.of(testDevice.getType()), testDevice.getName()); List keysToSubscribe = expectedKeySet.stream() .map(key -> new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, key)) .collect(Collectors.toList()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index a452cbdce4..6de1bc5b35 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; @@ -87,7 +89,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { device.setType("default"); device.setLabel("testLabel" + (int) (Math.random() * 1000)); device = doPost("/api/device", device, Device.class); - dtf = new DeviceTypeFilter(device.getType(), device.getName()); + dtf = new DeviceTypeFilter(List.of(device.getType()), device.getName()); } @After @@ -195,7 +197,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(1, update1.getCmdId()); Assert.assertEquals(1, update1.getCount()); - DeviceTypeFilter dtf2 = new DeviceTypeFilter("non-existing-device-type", "D"); + DeviceTypeFilter dtf2 = new DeviceTypeFilter(List.of("non-existing-device-type"), "D"); EntityCountQuery edq2 = new EntityCountQuery(dtf2, Collections.emptyList()); EntityCountCmd cmd2 = new EntityCountCmd(2, edq2); @@ -214,7 +216,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { highTemperatureFilter.setPredicate(predicate); highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); - DeviceTypeFilter dtf3 = new DeviceTypeFilter("default", "D"); + DeviceTypeFilter dtf3 = new DeviceTypeFilter(List.of("default"), "D"); EntityCountQuery edq3 = new EntityCountQuery(dtf3, Collections.singletonList(highTemperatureFilter)); EntityCountCmd cmd3 = new EntityCountCmd(3, edq3); getWsClient().send(cmd3); @@ -231,7 +233,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { highTemperatureFilter2.setPredicate(predicate2); highTemperatureFilter2.setValueType(EntityKeyValueType.NUMERIC); - DeviceTypeFilter dtf4 = new DeviceTypeFilter("default", "D"); + DeviceTypeFilter dtf4 = new DeviceTypeFilter(List.of("default"), "D"); EntityCountQuery edq4 = new EntityCountQuery(dtf4, Collections.singletonList(highTemperatureFilter2)); EntityCountCmd cmd4 = new EntityCountCmd(4, edq4); @@ -637,6 +639,34 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue); } + @Test + public void testEntityCountCmd_filterTypeSingularCompatibilityTest() { + ObjectNode oldFormatDeviceTypeFilterSingular = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + oldFormatDeviceTypeFilterSingular.put("type", "deviceType"); + oldFormatDeviceTypeFilterSingular.put("deviceType", "default"); + oldFormatDeviceTypeFilterSingular.put("deviceNameFilter", "Device"); + + ObjectNode query = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + query.set("entityFilter", oldFormatDeviceTypeFilterSingular); + + ObjectNode entityCountCmd = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + entityCountCmd.put("cmdId", 1); + entityCountCmd.set("query", query); + + ArrayNode entityCountCmds = JacksonUtil.OBJECT_MAPPER.createArrayNode(); + entityCountCmds.add(entityCountCmd); + + ObjectNode wrapperNode = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + wrapperNode.set("entityCountCmds", entityCountCmds); + + getWsClient().send(JacksonUtil.toString(wrapperNode)); + + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + + } + private void sendTelemetry(Device device, List tsData) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback() { diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java new file mode 100644 index 0000000000..84d8a458d5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java @@ -0,0 +1,40 @@ +/** + * 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.notification; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +@Service +@Primary +public class MockNotificationSettingsService extends DefaultNotificationSettingsService { + + public MockNotificationSettingsService(AdminSettingsService adminSettingsService, NotificationTargetService notificationTargetService, NotificationTemplateService notificationTemplateService, NotificationRuleService notificationRuleService) { + super(adminSettingsService, notificationTargetService, notificationTemplateService, notificationRuleService); + } + + @Override + public void createDefaultNotificationConfigs(TenantId tenantId) { + // do nothing + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index f6111ea4b8..a8a68762d8 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -73,7 +73,7 @@ public class DefaultDeviceStateServiceTest { @Before public void setUp() { - service = spy(new DefaultDeviceStateService(tenantService, deviceService, attributesService, tsService, clusterService, partitionService, serviceInfoProvider, null, null)); + service = spy(new DefaultDeviceStateService(tenantService, deviceService, attributesService, tsService, clusterService, partitionService, serviceInfoProvider, null, null, null)); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java b/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java new file mode 100644 index 0000000000..d31d6639ac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java @@ -0,0 +1,110 @@ +/** + * 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.stats; + +import lombok.SneakyThrows; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@TestPropertySource(properties = { + "usage.stats.report.enabled=true", + "transport.http.enabled=true", + "usage.stats.report.interval=2", + "usage.stats.gauge_report_interval=1", + "state.defaultStateCheckIntervalInSec=3", + "state.defaultInactivityTimeoutInSec=10" + +}) +public class DevicesStatisticsTest extends AbstractControllerTest { + + @Autowired + private TbApiUsageStateService apiUsageStateService; + @Autowired + private TimeseriesService timeseriesService; + + private ApiUsageStateId apiUsageStateId; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + apiUsageStateId = apiUsageStateService.getApiUsageState(tenantId).getId(); + } + + @Test + public void testDevicesActivityStats() throws Exception { + int activeDevicesCount = 5; + List activeDevices = new ArrayList<>(); + for (int i = 1; i <= activeDevicesCount; i++) { + String name = "active_device_" + i; + Device device = createDevice(name, name); + activeDevices.add(device); + } + int inactiveDevicesCount = 10; + List inactiveDevices = new ArrayList<>(); + for (int i = 1; i <= inactiveDevicesCount; i++) { + String name = "inactive_device_" + i; + Device device = createDevice(name, name); + inactiveDevices.add(device); + } + + await().atMost(15, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat(getLatestStats(ApiUsageRecordKey.ACTIVE_DEVICES, false)).isZero(); + assertThat(getLatestStats(ApiUsageRecordKey.INACTIVE_DEVICES, false)).isEqualTo(activeDevicesCount + inactiveDevicesCount); + }); + + for (Device device : activeDevices) { + postTelemetry(device.getName(), "{\"dp\":1}"); + } + + await().atMost(40, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat(getLatestStats(ApiUsageRecordKey.ACTIVE_DEVICES, false)).isEqualTo(activeDevicesCount); + assertThat(getLatestStats(ApiUsageRecordKey.INACTIVE_DEVICES, false)).isEqualTo(inactiveDevicesCount); + }); + } + + @SneakyThrows + private Long getLatestStats(ApiUsageRecordKey key, boolean hourly) { + return timeseriesService.findLatest(tenantId, apiUsageStateId, List.of(key.getApiCountKey() + (hourly ? "Hourly" : ""))) + .get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null); + } + + @SneakyThrows + private void postTelemetry(String accessToken, String json) { + doPost("/api/v1/" + accessToken + "/telemetry", json, new String[0]).andExpect(status().isOk()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java index b483084258..941e97e500 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java @@ -370,7 +370,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt protected void processProtoTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - DeviceTypeFilter dtf = new DeviceTypeFilter(savedDevice.getType(), savedDevice.getName()); + DeviceTypeFilter dtf = new DeviceTypeFilter(List.of(savedDevice.getType()), savedDevice.getName()); String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java index 3880141c36..87337545fc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java @@ -190,7 +190,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { return device; } - private Device createDevice(String deviceName, String accessToken) throws Exception { + protected Device createDevice(String deviceName, String accessToken) throws Exception { Device device = new Device(); device.setName(deviceName); device.setType("default"); diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 80ccac7290..65a57a5cc5 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -1034,6 +1034,7 @@ message ToUsageStatsServiceMsg { repeated UsageStatsKVProto values = 5; int64 customerIdMSB = 6; int64 customerIdLSB = 7; + string serviceId = 8; } message ToOtaPackageStateServiceMsg { diff --git a/common/data/pom.xml b/common/data/pom.xml index e50ccd9dd2..d81203cd4d 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -100,6 +100,10 @@ org.apache.commons commons-lang3 + + commons-codec + commons-codec + io.swagger swagger-annotations diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java index dcad5c908a..64eabfdd16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java @@ -26,7 +26,9 @@ public enum ApiUsageRecordKey { JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit", "JavaScript execution"), EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit", "email message"), SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit", "SMS message"), - CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit", "alarm"); + CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit", "alarm"), + ACTIVE_DEVICES("activeDevicesCount"), + INACTIVE_DEVICES("inactiveDevicesCount"); private static final ApiUsageRecordKey[] JS_RECORD_KEYS = {JS_EXEC_COUNT}; private static final ApiUsageRecordKey[] RE_RECORD_KEYS = {RE_EXEC_COUNT}; @@ -44,12 +46,23 @@ public enum ApiUsageRecordKey { private final String apiLimitKey; @Getter private final String unitLabel; + @Getter + private final boolean counter; ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel) { + this(apiFeature, apiCountKey, apiLimitKey, unitLabel, true); + } + + ApiUsageRecordKey(String apiCountKey) { + this(null, apiCountKey, null, null, false); + } + + ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel, boolean counter) { this.apiFeature = apiFeature; this.apiCountKey = apiCountKey; this.apiLimitKey = apiLimitKey; this.unitLabel = unitLabel; + this.counter = counter; } public static ApiUsageRecordKey[] getKeys(ApiFeature feature) { 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 287d3f3b71..43210de9d0 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 @@ -38,5 +38,4 @@ public class UpdateMessage implements Serializable { @ApiModelProperty(position = 6, value = "Latest ThingsBoard version release notes URL.") private final String latestVersionReleaseNotesUrl; - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java index 9b875cf42c..f666c02777 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java @@ -15,18 +15,38 @@ */ package org.thingsboard.server.common.data.query; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.util.CollectionUtils; -@Data +import java.util.Collections; +import java.util.List; + +@ToString +@EqualsAndHashCode +@Setter public class AssetTypeFilter implements EntityFilter { + /** + * Replaced by {@link AssetTypeFilter#getAssetTypes()} instead. + */ + @Deprecated(since = "3.5", forRemoval = true) + private String assetType; + + private List assetTypes; + + public List getAssetTypes() { + return !CollectionUtils.isEmpty(assetTypes) ? assetTypes : Collections.singletonList(assetType); + } + + @Getter + private String assetNameFilter; + @Override public EntityFilterType getType() { return EntityFilterType.ASSET_TYPE; } - private String assetType; - - private String assetNameFilter; - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java index 0c502dff8b..bc25d1d158 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java @@ -16,21 +16,45 @@ package org.thingsboard.server.common.data.query; import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; @NoArgsConstructor @AllArgsConstructor -@Data +@ToString +@EqualsAndHashCode +@Setter public class DeviceTypeFilter implements EntityFilter { - @Override - public EntityFilterType getType() { - return EntityFilterType.DEVICE_TYPE; - } - + /** + * Replaced by {@link DeviceTypeFilter#getDeviceTypes()} instead. + */ + @Deprecated(since = "3.5", forRemoval = true) private String deviceType; + private List deviceTypes; + + public List getDeviceTypes() { + return !CollectionUtils.isEmpty(deviceTypes) ? deviceTypes : Collections.singletonList(deviceType); + } + + @Getter private String deviceNameFilter; + public DeviceTypeFilter(List deviceTypes, String deviceNameFilter) { + this.deviceTypes = deviceTypes; + this.deviceNameFilter = deviceNameFilter; + } + + @Override + public EntityFilterType getType() { + return EntityFilterType.DEVICE_TYPE; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java index 8a1f325354..306aae43cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java @@ -15,18 +15,38 @@ */ package org.thingsboard.server.common.data.query; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.util.CollectionUtils; -@Data +import java.util.Collections; +import java.util.List; + +@ToString +@EqualsAndHashCode +@Setter public class EdgeTypeFilter implements EntityFilter { + /** + * Replaced by {@link EdgeTypeFilter#getEdgeTypes()} instead. + */ + @Deprecated(since = "3.5", forRemoval = true) + private String edgeType; + + private List edgeTypes; + + public List getEdgeTypes() { + return !CollectionUtils.isEmpty(edgeTypes) ? edgeTypes : Collections.singletonList(edgeType); + } + + @Getter + private String edgeNameFilter; + @Override public EntityFilterType getType() { return EntityFilterType.EDGE_TYPE; } - private String edgeType; - - private String edgeNameFilter; - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java index b9c5e00767..8025cf43b5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java @@ -15,18 +15,38 @@ */ package org.thingsboard.server.common.data.query; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.util.CollectionUtils; -@Data +import java.util.Collections; +import java.util.List; + +@ToString +@EqualsAndHashCode +@Setter public class EntityViewTypeFilter implements EntityFilter { + /** + * Replaced by {@link EntityViewTypeFilter#getEntityViewTypes()} instead. + */ + @Deprecated(since = "3.5", forRemoval = true) + private String entityViewType; + + private List entityViewTypes; + + public List getEntityViewTypes() { + return !CollectionUtils.isEmpty(entityViewTypes) ? entityViewTypes : Collections.singletonList(entityViewType); + } + + @Getter + private String entityViewNameFilter; + @Override public EntityFilterType getType() { return EntityFilterType.ENTITY_VIEW_TYPE; } - private String entityViewType; - - private String entityViewNameFilter; - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java index 97ef85a5f2..b19dbbbaee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java @@ -19,7 +19,7 @@ import lombok.Getter; public enum UserSettingsType { - GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS; + GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED; @Getter private final boolean reserved; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 92c4202a79..5c63fe6e7d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.common.config.SslConfigs; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; @@ -49,6 +50,24 @@ public class TbKafkaSettings { @Value("${queue.kafka.bootstrap.servers}") private String servers; + @Value("${queue.kafka.ssl.enabled:false}") + private boolean sslEnabled; + + @Value("${queue.kafka.ssl.truststore.location}") + private String sslTruststoreLocation; + + @Value("${queue.kafka.ssl.truststore.password}") + private String sslTruststorePassword; + + @Value("${queue.kafka.ssl.keystore.location}") + private String sslKeystoreLocation; + + @Value("${queue.kafka.ssl.keystore.password}") + private String sslKeystorePassword; + + @Value("${queue.kafka.ssl.key.password}") + private String sslKeyPassword; + @Value("${queue.kafka.acks}") private String acks; @@ -115,6 +134,8 @@ public class TbKafkaSettings { props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(AdminClientConfig.RETRIES_CONFIG, retries); + configureSSL(props); + return props; } @@ -126,6 +147,8 @@ public class TbKafkaSettings { props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); + configureSSL(props); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); @@ -164,7 +187,21 @@ public class TbKafkaSettings { if (other != null) { other.forEach(kv -> props.put(kv.getKey(), kv.getValue())); } + + configureSSL(props); + return props; } + private void configureSSL(Properties props) { + if (sslEnabled) { + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocation); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePassword); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocation); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePassword); + props.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, sslKeyPassword); + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index a86147a37e..5dfaa70e12 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -16,11 +16,11 @@ package org.thingsboard.server.queue.usagestats; import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ApiUsageRecordKey; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -32,12 +32,12 @@ import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import javax.annotation.PostConstruct; import java.util.EnumMap; -import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicLong; @Component @Slf4j +@RequiredArgsConstructor public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { @Value("${usage.stats.report.enabled:true}") @@ -56,19 +57,14 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { @Value("${usage.stats.report.interval:10}") private int interval; - private final EnumMap> stats = new EnumMap<>(ApiUsageRecordKey.class); + private final EnumMap> stats = new EnumMap<>(ApiUsageRecordKey.class); private final PartitionService partitionService; + private final TbServiceInfoProvider serviceInfoProvider; private final SchedulerComponent scheduler; private final TbQueueProducerProvider producerProvider; private TbQueueProducer> msgProducer; - public DefaultTbApiUsageReportClient(PartitionService partitionService, SchedulerComponent scheduler, TbQueueProducerProvider producerProvider) { - this.partitionService = partitionService; - this.scheduler = scheduler; - this.producerProvider = producerProvider; - } - @PostConstruct private void init() { if (enabled) { @@ -87,42 +83,48 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { } private void reportStats() { - ConcurrentMap report = new ConcurrentHashMap<>(); + ConcurrentMap report = new ConcurrentHashMap<>(); for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - ConcurrentMap statsForKey = stats.get(key); - statsForKey.forEach((ownerId, statsValue) -> { + ConcurrentMap statsForKey = stats.get(key); + statsForKey.forEach((reportLevel, statsValue) -> { long value = statsValue.get(); - if (value == 0) return; + if (value == 0 && key.isCounter()) return; - ToUsageStatsServiceMsg.Builder statsMsgBuilder = report.computeIfAbsent(ownerId, id -> { - ToUsageStatsServiceMsg.Builder newStatsMsgBuilder = ToUsageStatsServiceMsg.newBuilder(); + ToUsageStatsServiceMsg.Builder statsMsg = report.computeIfAbsent(reportLevel.getParentEntity(), parent -> { + ToUsageStatsServiceMsg.Builder newStatsMsg = ToUsageStatsServiceMsg.newBuilder(); - TenantId tenantId = ownerId.getTenantId(); - newStatsMsgBuilder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); - newStatsMsgBuilder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + TenantId tenantId = parent.getTenantId(); + newStatsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + newStatsMsg.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); - EntityId entityId = ownerId.getEntityId(); - if (entityId != null && entityId.getEntityType() == EntityType.CUSTOMER) { - newStatsMsgBuilder.setCustomerIdMSB(entityId.getId().getMostSignificantBits()); - newStatsMsgBuilder.setCustomerIdLSB(entityId.getId().getLeastSignificantBits()); + CustomerId customerId = parent.getCustomerId(); + if (customerId != null) { + newStatsMsg.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + newStatsMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } - return newStatsMsgBuilder; + newStatsMsg.setServiceId(serviceInfoProvider.getServiceId()); + return newStatsMsg; }); - statsMsgBuilder.addValues(UsageStatsKVProto.newBuilder().setKey(key.name()).setValue(value).build()); + UsageStatsKVProto.Builder statsItem = UsageStatsKVProto.newBuilder() + .setKey(key.name()) + .setValue(value); + statsMsg.addValues(statsItem.build()); }); statsForKey.clear(); } - report.forEach(((ownerId, statsMsg) -> { + report.forEach(((parent, statsMsg) -> { //TODO: figure out how to minimize messages into the queue. Maybe group by 100s of messages? - - TenantId tenantId = ownerId.getTenantId(); - EntityId entityId = Optional.ofNullable(ownerId.getEntityId()).orElse(tenantId); - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId).newByTopic(msgProducer.getDefaultTopic()); - msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null); + try { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId()) + .newByTopic(msgProducer.getDefaultTopic()); + msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null); + } catch (Exception e) { + log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e); + } })); if (!report.isEmpty()) { @@ -132,16 +134,17 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { @Override public void report(TenantId tenantId, CustomerId customerId, ApiUsageRecordKey key, long value) { - if (enabled) { - ConcurrentMap statsForKey = stats.get(key); - - statsForKey.computeIfAbsent(new OwnerId(tenantId), id -> new AtomicLong()).addAndGet(value); - statsForKey.computeIfAbsent(new OwnerId(TenantId.SYS_TENANT_ID), id -> new AtomicLong()).addAndGet(value); + if (!enabled) return; - if (enabledPerCustomer && customerId != null && !customerId.isNullUid()) { - statsForKey.computeIfAbsent(new OwnerId(tenantId, customerId), id -> new AtomicLong()).addAndGet(value); - } + ReportLevel[] reportLevels = new ReportLevel[3]; + reportLevels[0] = ReportLevel.of(tenantId); + if (key.isCounter()) { + reportLevels[1] = ReportLevel.of(TenantId.SYS_TENANT_ID); + } + if (enabledPerCustomer && customerId != null && !customerId.isNullUid()) { + reportLevels[2] = ReportLevel.of(tenantId, customerId); } + report(key, value, reportLevels); } @Override @@ -149,18 +152,47 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { report(tenantId, customerId, key, 1); } + private void report(ApiUsageRecordKey key, long value, ReportLevel... levels) { + ConcurrentMap statsForKey = stats.get(key); + for (ReportLevel level : levels) { + if (level == null) continue; + + AtomicLong n = statsForKey.computeIfAbsent(level, k -> new AtomicLong()); + if (key.isCounter()) { + n.addAndGet(value); + } else { + n.set(value); + } + } + } + @Data - private static class OwnerId { - private TenantId tenantId; - private EntityId entityId; + private static class ReportLevel { + private final TenantId tenantId; + private final CustomerId customerId; + + public static ReportLevel of(TenantId tenantId) { + return new ReportLevel(tenantId, null); + } - public OwnerId(TenantId tenantId) { - this.tenantId = tenantId; + public static ReportLevel of(TenantId tenantId, CustomerId customerId) { + return new ReportLevel(tenantId, customerId); } - public OwnerId(TenantId tenantId, EntityId entityId) { - this.tenantId = tenantId; - this.entityId = entityId; + public ParentEntity getParentEntity() { + return new ParentEntity(tenantId, customerId); } + } + + @Data + private static class ParentEntity { + private final TenantId tenantId; + private final CustomerId customerId; + + public EntityId getId() { + return customerId != null ? customerId : tenantId; + } + } + } diff --git a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java index 8fff7fc0fe..dc4068edcc 100644 --- a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java +++ b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -191,7 +190,6 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { }, callbackExecutor); } - @NotNull private JsInvokeProtos.RemoteJsRequest buildJsInvokeRequest(JsScriptInfo jsInfo, Object[] args, boolean includeScriptBody, String scriptBody) { JsInvokeProtos.JsInvokeRequest.Builder jsRequestBuilder = JsInvokeProtos.JsInvokeRequest.newBuilder() .setScriptHash(jsInfo.getHash()) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java index 9e29177dcd..fb9cc870ad 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Hex; import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.Cursor; @@ -48,7 +49,12 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { if (data == null) { return null; } else { - return deserialize(data); + try { + return deserialize(data); + } catch (Exception e) { + log.warn("[{}] Failed to deserialize client from data: {}", endpoint, Hex.encodeHexString(data), e); + return null; + } } } } @@ -70,7 +76,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { scans.forEach(scan -> { scan.forEachRemaining(key -> { byte[] element = connection.get(key); - clients.add(deserialize(element)); + if (element != null) { + try { + clients.add(deserialize(element)); + } catch (Exception e) { + log.warn("[{}] Failed to deserialize client from data: {}", Hex.encodeHexString(key), Hex.encodeHexString(element), e); + } + } }); }); return clients; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index 0477ccf32f..6f8dfceede 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index e8eb0b150c..4376b4d3c2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.sql.alarm; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; @@ -303,7 +302,6 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); } - @NotNull private static String getPropagationTypes(AlarmPropagationInfo ap) { String propagateRelationTypes; if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) { 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 2885776a6d..9d4ffd58b4 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 @@ -816,36 +816,39 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } private String typeQuery(QueryContext ctx, EntityFilter filter) { - String type; + List types; String name; switch (filter.getType()) { case ASSET_TYPE: - type = ((AssetTypeFilter) filter).getAssetType(); + types = ((AssetTypeFilter) filter).getAssetTypes(); name = ((AssetTypeFilter) filter).getAssetNameFilter(); break; case DEVICE_TYPE: - type = ((DeviceTypeFilter) filter).getDeviceType(); + types = ((DeviceTypeFilter) filter).getDeviceTypes(); name = ((DeviceTypeFilter) filter).getDeviceNameFilter(); break; case ENTITY_VIEW_TYPE: - type = ((EntityViewTypeFilter) filter).getEntityViewType(); + types = ((EntityViewTypeFilter) filter).getEntityViewTypes(); name = ((EntityViewTypeFilter) filter).getEntityViewNameFilter(); break; case EDGE_TYPE: - type = ((EdgeTypeFilter) filter).getEdgeType(); + types = ((EdgeTypeFilter) filter).getEdgeTypes(); name = ((EdgeTypeFilter) filter).getEdgeNameFilter(); break; default: throw new RuntimeException("Not supported!"); } - ctx.addStringParameter("entity_filter_type_query_type", type); - ctx.addStringParameter("entity_filter_type_query_name", name); - - if (name.startsWith("%") || name.endsWith("%")) { - return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(:entity_filter_type_query_name)"; + String typesFilter = "e.type in (:entity_filter_type_query_types)"; + ctx.addStringListParameter("entity_filter_type_query_types", types); + if (!StringUtils.isEmpty(name)) { + ctx.addStringParameter("entity_filter_type_query_name", name); + if (name.startsWith("%") || name.endsWith("%")) { + return typesFilter + " and lower(e.search_text) like lower(:entity_filter_type_query_name)"; + } + return typesFilter + " and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))"; + } else { + return typesFilter; } - - return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))"; } public static EntityType resolveEntityType(EntityFilter entityFilter) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index 6151e16d0d..ba49cc964c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -126,6 +126,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A List profileThresholds = new ArrayList<>(); for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiLimitKey() == null) continue; profileThresholds.add(new BasicTsKvEntry(saved.getCreatedTime(), new LongDataEntry(key.getApiLimitKey(), configuration.getProfileThreshold(key)))); } tsService.save(tenantId, saved.getId(), profileThresholds, 0L); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 1af63c0ed7..5ec546cd4e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -131,7 +131,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityCountQuery countQuery = new EntityCountQuery(filter); @@ -139,11 +139,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(97, count); - filter.setDeviceType("unknown"); + filter.setDeviceTypes(List.of("unknown")); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(0, count); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter("Device1"); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(11, count); @@ -235,7 +235,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } EdgeTypeFilter filter = new EdgeTypeFilter(); - filter.setEdgeType("default"); + filter.setEdgeTypes(List.of("default")); filter.setEdgeNameFilter(""); EntityCountQuery countQuery = new EntityCountQuery(filter); @@ -243,11 +243,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(97, count); - filter.setEdgeType("unknown"); + filter.setEdgeTypes(List.of("unknown")); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(0, count); - filter.setEdgeType("default"); + filter.setEdgeTypes(List.of("default")); filter.setEdgeNameFilter("Edge1"); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(11, count); @@ -813,7 +813,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -1337,7 +1337,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { private EntityDataQuery createDeviceSearchQuery(String deviceField, StringOperation operation, String searchQuery) { DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter(); - deviceTypeFilter.setDeviceType("default"); + deviceTypeFilter.setDeviceTypes(List.of("default")); deviceTypeFilter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -1402,7 +1402,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Futures.allAsList(attributeFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -1502,7 +1502,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Futures.allAsList(attributeFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -1648,7 +1648,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Futures.allAsList(timeseriesFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -1780,7 +1780,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Futures.allAsList(attributeFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -1912,7 +1912,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -2029,7 +2029,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC); diff --git a/monitoring/pom.xml b/monitoring/pom.xml new file mode 100644 index 0000000000..429df521b6 --- /dev/null +++ b/monitoring/pom.xml @@ -0,0 +1,160 @@ + + + + 4.0.0 + + org.thingsboard + 3.5.0-SNAPSHOT + thingsboard + + + monitoring + ThingsBoard Monitoring Service + jar + + + UTF-8 + ${basedir}/.. + java + false + process-resources + package + tb-monitoring + false + ${project.build.directory}/windows + ThingsBoard Monitoring Service + org.thingsboard.monitoring.ThingsboardMonitoringApplication + + + + + org.thingsboard.common + data + + + org.thingsboard.common + util + + + org.thingsboard + rest-client + compile + + + org.springframework.boot + spring-boot-starter + + + + org.eclipse.californium + californium-core + + + org.eclipse.californium + scandium + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + + + org.apache.httpcomponents + httpclient + + + org.java-websocket + Java-WebSocket + compile + + + com.google.guava + guava + + + org.apache.commons + commons-lang3 + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + + + ${pkg.name}-${project.version} + + + ${project.basedir}/src/main/resources + + + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.thingsboard + gradle-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-install-plugin + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml new file mode 100644 index 0000000000..58e49c7639 --- /dev/null +++ b/monitoring/src/main/conf/logback.xml @@ -0,0 +1,46 @@ + + + + + + + ${pkg.logFolder}/${pkg.name}.log + + ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/monitoring/src/main/conf/tb-monitoring.conf b/monitoring/src/main/conf/tb-monitoring.conf new file mode 100644 index 0000000000..d1d729bd20 --- /dev/null +++ b/monitoring/src/main/conf/tb-monitoring.conf @@ -0,0 +1,22 @@ +# +# 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. +# + +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export LOG_FILENAME=tb-monitoring.out +export LOADER_PATH=/usr/share/tb-monitoring/conf diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java new file mode 100644 index 0000000000..3fa43a8175 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -0,0 +1,40 @@ +/** + * 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.monitoring; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.thingsboard.common.util.ThingsBoardThreadFactory; + +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@SpringBootApplication +@EnableScheduling +@Slf4j +public class ThingsboardMonitoringApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) + .properties(Map.of("spring.config.name", "tb-monitoring")) + .run(args); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java new file mode 100644 index 0000000000..9dcc58cf2f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.thingsboard.rest.client.RestClient; + +import java.time.Duration; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TbClient extends RestClient { + + @Value("${monitoring.rest.username}") + private String username; + @Value("${monitoring.rest.password}") + private String password; + + public TbClient(@Value("${monitoring.rest.base_url}") String baseUrl, + @Value("${monitoring.rest.request_timeout_ms}") int requestTimeoutMs) { + super(new RestTemplateBuilder() + .setConnectTimeout(Duration.ofMillis(requestTimeoutMs)) + .setReadTimeout(Duration.ofMillis(requestTimeoutMs)) + .build(), baseUrl); + } + + public String logIn() { + login(username, password); + return getToken(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java new file mode 100644 index 0000000000..20e7a900ff --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java @@ -0,0 +1,194 @@ +/** + * 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.monitoring.client; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.data.cmd.CmdsWrapper; +import org.thingsboard.monitoring.data.cmd.EntityDataCmd; +import org.thingsboard.monitoring.data.cmd.EntityDataUpdate; +import org.thingsboard.monitoring.data.cmd.LatestValueCmd; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityListFilter; + +import javax.net.ssl.SSLParameters; +import java.net.URI; +import java.nio.channels.NotYetConnectedException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +@Slf4j +public class WsClient extends WebSocketClient implements AutoCloseable { + + public volatile JsonNode lastMsg; + private CountDownLatch reply; + private CountDownLatch update; + + private final Lock updateLock = new ReentrantLock(); + + private long requestTimeoutMs; + + public WsClient(URI serverUri, long requestTimeoutMs) { + super(serverUri); + this.requestTimeoutMs = requestTimeoutMs; + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + + } + + @Override + public void onMessage(String s) { + if (s == null) { + return; + } + updateLock.lock(); + try { + lastMsg = JacksonUtil.toJsonNode(s); + log.trace("Received new msg: {}", lastMsg.toPrettyString()); + if (update != null) { + update.countDown(); + } + if (reply != null) { + reply.countDown(); + } + } finally { + updateLock.unlock(); + } + } + + @Override + public void onClose(int i, String s, boolean b) { + log.debug("WebSocket client is closed"); + } + + @Override + public void onError(Exception e) { + log.error("WebSocket client error:", e); + } + + public void registerWaitForUpdate() { + updateLock.lock(); + try { + lastMsg = null; + update = new CountDownLatch(1); + } finally { + updateLock.unlock(); + } + log.trace("Registered wait for update"); + } + + @Override + public void send(String text) throws NotYetConnectedException { + updateLock.lock(); + try { + reply = new CountDownLatch(1); + } finally { + updateLock.unlock(); + } + super.send(text); + } + + public WsClient subscribeForTelemetry(List devices, String key) { + EntityDataCmd cmd = new EntityDataCmd(); + cmd.setCmdId(RandomUtils.nextInt(0, 1000)); + + EntityListFilter devicesFilter = new EntityListFilter(); + devicesFilter.setEntityType(EntityType.DEVICE); + devicesFilter.setEntityList(devices.stream().map(UUID::toString).collect(Collectors.toList())); + EntityDataPageLink pageLink = new EntityDataPageLink(100,0, null, null); + EntityDataQuery devicesQuery = new EntityDataQuery(devicesFilter, pageLink, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + cmd.setQuery(devicesQuery); + + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(List.of(new EntityKey(EntityKeyType.TIME_SERIES, key))); + cmd.setLatestCmd(latestCmd); + + CmdsWrapper wrapper = new CmdsWrapper(); + wrapper.setEntityDataCmds(List.of(cmd)); + send(JacksonUtil.toString(wrapper)); + return this; + } + + public JsonNode waitForUpdate(long ms) { + log.trace("update latch count: {}", update.getCount()); + try { + if (update.await(ms, TimeUnit.MILLISECONDS)) { + log.trace("Waited for update"); + return getLastMsg(); + } + } catch (InterruptedException e) { + log.debug("Failed to await reply", e); + } + log.trace("No update arrived within {} ms", ms); + return null; + } + + public JsonNode waitForReply() { + try { + if (reply.await(requestTimeoutMs, TimeUnit.MILLISECONDS)) { + log.trace("Waited for reply"); + return getLastMsg(); + } + } catch (InterruptedException e) { + log.debug("Failed to await reply", e); + } + log.trace("No reply arrived within {} ms", requestTimeoutMs); + throw new IllegalStateException("No WS reply arrived within " + requestTimeoutMs + " ms"); + } + + private JsonNode getLastMsg() { + if (lastMsg != null) { + JsonNode errorMsg = lastMsg.get("errorMsg"); + if (errorMsg != null && !errorMsg.isNull() && StringUtils.isNotEmpty(errorMsg.asText())) { + throw new RuntimeException("WS error from server: " + errorMsg.asText()); + } else { + return lastMsg; + } + } else { + return null; + } + } + + public Object getTelemetryUpdate(UUID deviceId, String key) { + JsonNode lastMsg = getLastMsg(); + if (lastMsg == null || lastMsg.isNull()) return null; + EntityDataUpdate update = JacksonUtil.treeToValue(lastMsg, EntityDataUpdate.class); + return update.getLatest(deviceId, key); + } + + @Override + protected void onSetSSLParameters(SSLParameters sslParameters) { + sslParameters.setEndpointIdentificationAlgorithm(null); + } + +} \ No newline at end of file diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java new file mode 100644 index 0000000000..1bab803dc9 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java @@ -0,0 +1,59 @@ +/** + * 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.monitoring.client; + +import lombok.RequiredArgsConstructor; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.ssl.TrustStrategy; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.data.Latencies; +import org.thingsboard.monitoring.service.MonitoringReporter; +import org.thingsboard.monitoring.util.TbStopWatch; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class WsClientFactory { + + private final MonitoringReporter monitoringReporter; + private final TbStopWatch stopWatch; + @Value("${monitoring.ws.base_url}") + private String baseUrl; + @Value("${monitoring.ws.request_timeout_ms}") + private int requestTimeoutMs; + + public WsClient createClient(String accessToken) throws Exception { + URI uri = new URI(baseUrl + "/api/ws/plugins/telemetry?token=" + accessToken); + stopWatch.start(); + WsClient wsClient = new WsClient(uri, requestTimeoutMs); + if (baseUrl.startsWith("wss")) { + SSLContextBuilder builder = SSLContexts.custom(); + builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); + wsClient.setSocketFactory(builder.build().getSocketFactory()); + } + boolean connected = wsClient.connectBlocking(requestTimeoutMs, TimeUnit.MILLISECONDS); + if (!connected) { + throw new IllegalStateException("Failed to establish WS session"); + } + monitoringReporter.reportLatency(Latencies.WS_CONNECT, stopWatch.getTime()); + return wsClient; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java new file mode 100644 index 0000000000..548ad6d08b --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java @@ -0,0 +1,34 @@ +/** + * 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.monitoring.config; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +import java.util.UUID; + +@Data +public class DeviceConfig { + + private UUID id; + private DeviceCredentials credentials; + + public void setId(String id) { + this.id = StringUtils.isNotEmpty(id) ? UUID.fromString(id) : null; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java new file mode 100644 index 0000000000..5f1ab49e91 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java @@ -0,0 +1,26 @@ +/** + * 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.monitoring.config; + +import lombok.Data; + +@Data +public class MonitoringTargetConfig { + + private String baseUrl; + private DeviceConfig device; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java new file mode 100644 index 0000000000..ed0a4ea331 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java @@ -0,0 +1,35 @@ +/** + * 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.monitoring.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.thingsboard.monitoring.transport.TransportHealthChecker; +import org.thingsboard.monitoring.transport.impl.CoapTransportHealthChecker; +import org.thingsboard.monitoring.transport.impl.HttpTransportHealthChecker; +import org.thingsboard.monitoring.transport.impl.MqttTransportHealthChecker; + +@AllArgsConstructor +@Getter +public enum TransportType { + + MQTT(MqttTransportHealthChecker.class), + COAP(CoapTransportHealthChecker.class), + HTTP(HttpTransportHealthChecker.class); + + private final Class> serviceClass; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java new file mode 100644 index 0000000000..905858b4eb --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java @@ -0,0 +1,33 @@ +/** + * 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.monitoring.config.service; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.TransportType; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.coap.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.coap") +public class CoapTransportMonitoringConfig extends TransportMonitoringConfig { + + @Override + public TransportType getTransportType() { + return TransportType.COAP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java new file mode 100644 index 0000000000..3a3e8f612c --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java @@ -0,0 +1,33 @@ +/** + * 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.monitoring.config.service; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.TransportType; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.http.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.http") +public class HttpTransportMonitoringConfig extends TransportMonitoringConfig { + + @Override + public TransportType getTransportType() { + return TransportType.HTTP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java new file mode 100644 index 0000000000..7fef5b95dc --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java @@ -0,0 +1,39 @@ +/** + * 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.monitoring.config.service; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.TransportType; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.mqtt.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.mqtt") +@Data +@EqualsAndHashCode(callSuper = true) +public class MqttTransportMonitoringConfig extends TransportMonitoringConfig { + + private Integer qos; + + @Override + public TransportType getTransportType() { + return TransportType.MQTT; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java new file mode 100644 index 0000000000..0712d1d919 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java @@ -0,0 +1,33 @@ +/** + * 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.monitoring.config.service; + +import lombok.Data; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.TransportType; + +import java.util.List; + +@Data +public abstract class TransportMonitoringConfig { + + private int requestTimeoutMs; + + private List targets; + + public abstract TransportType getTransportType(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java new file mode 100644 index 0000000000..8141c34586 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data; + +import org.thingsboard.monitoring.config.TransportType; + +public class Latencies { + + public static final String WS_UPDATE = "wsUpdate"; + public static final String WS_CONNECT = "wsConnect"; + public static final String LOG_IN = "logIn"; + + public static String transportRequest(TransportType transportType) { + return String.format("%sTransportRequest", transportType.name().toLowerCase()); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java new file mode 100644 index 0000000000..c64291e30f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java @@ -0,0 +1,67 @@ +/** + * 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.monitoring.data; + +import com.google.common.util.concurrent.AtomicDouble; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.atomic.AtomicInteger; + +@RequiredArgsConstructor +public class Latency { + + private final String key; + private final AtomicDouble latencySum = new AtomicDouble(); + private final AtomicInteger counter = new AtomicInteger(); + + public synchronized void report(double latencyInMs) { + latencySum.addAndGet(latencyInMs); + counter.incrementAndGet(); + } + + public synchronized double getAvg() { + return latencySum.get() / counter.get(); + } + + public boolean isNotEmpty() { + return counter.get() > 0; + } + + public synchronized void reset() { + latencySum.set(0.0); + counter.set(0); + } + + public String getKey() { + return key; + } + + public synchronized Latency snapshot() { + Latency snapshot = new Latency(key); + snapshot.latencySum.set(latencySum.get()); + snapshot.counter.set(counter.get()); + return snapshot; + } + + @Override + public String toString() { + return "Latency{" + + "key='" + key + '\'' + + ", avgLatency=" + getAvg() + + '}'; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java new file mode 100644 index 0000000000..825162952f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java @@ -0,0 +1,22 @@ +/** + * 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.monitoring.data; + +public class MonitoredServiceKey { + + public static final String GENERAL = "Monitoring"; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java new file mode 100644 index 0000000000..8157c4af5f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java @@ -0,0 +1,28 @@ +/** + * 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.monitoring.data; + +public class TransportFailureException extends RuntimeException { + + public TransportFailureException(Throwable cause) { + super(cause.getMessage(), cause); + } + + public TransportFailureException(String message) { + super(message); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java new file mode 100644 index 0000000000..d7619ea32d --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java @@ -0,0 +1,32 @@ +/** + * 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.monitoring.data; + +import lombok.Data; +import org.thingsboard.monitoring.config.TransportType; + +@Data +public class TransportInfo { + + private final TransportType transportType; + private final String url; + + @Override + public String toString() { + return String.format("%s (%s)", transportType, url); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java new file mode 100644 index 0000000000..34227150ea --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.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.monitoring.data.cmd; + +import lombok.Data; + +import java.util.List; + +@Data +public class CmdsWrapper { + + private List entityDataCmds; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java new file mode 100644 index 0000000000..ccd6fa8f7c --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java @@ -0,0 +1,28 @@ +/** + * 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.monitoring.data.cmd; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +@Data +public class EntityDataCmd { + + private int cmdId; + private EntityDataQuery query; + private LatestValueCmd latestCmd; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java new file mode 100644 index 0000000000..5f3a466217 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java @@ -0,0 +1,44 @@ +/** + * 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.monitoring.data.cmd; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; + +import java.util.List; +import java.util.UUID; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class EntityDataUpdate { + + @JsonIgnoreProperties(ignoreUnknown = true) + private List update; + + public String getLatest(UUID entityId, String key) { + if (update == null) return null; + + return update.stream() + .filter(entityData -> entityData.getEntityId().getId().equals(entityId)).findFirst() + .map(EntityData::getLatest).map(latest -> latest.get(EntityKeyType.TIME_SERIES)) + .map(latest -> latest.get(key)).map(TsValue::getValue) + .orElse(null); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java new file mode 100644 index 0000000000..5c2cffcbb6 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java @@ -0,0 +1,28 @@ +/** + * 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.monitoring.data.cmd; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityKey; + +import java.util.List; + +@Data +public class LatestValueCmd { + + private List keys; + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java new file mode 100644 index 0000000000..939cb7440f --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java @@ -0,0 +1,42 @@ +/** + * 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.monitoring.data.notification; + +import org.thingsboard.monitoring.data.Latency; + +import java.util.Collection; + +public class HighLatencyNotification implements Notification { + + private final Collection latencies; + private final int thresholdMs; + + public HighLatencyNotification(Collection latencies, int thresholdMs) { + this.latencies = latencies; + this.thresholdMs = thresholdMs; + } + + @Override + public String getText() { + StringBuilder text = new StringBuilder(); + text.append("Some of the latencies are higher than ").append(thresholdMs).append(" ms:\n"); + latencies.forEach(latency -> { + text.append(String.format("[%s] %,.2f ms\n", latency.getKey(), latency.getAvg())); + }); + return text.toString(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java new file mode 100644 index 0000000000..21e5994d84 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java @@ -0,0 +1,22 @@ +/** + * 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.monitoring.data.notification; + +public interface Notification { + + String getText(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java new file mode 100644 index 0000000000..4b78d96a63 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +import lombok.Getter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +@Getter +public class ServiceFailureNotification implements Notification { + + private final Object serviceKey; + private final Throwable error; + private final int failuresCount; + + public ServiceFailureNotification(Object serviceKey, Throwable error, int failuresCount) { + this.serviceKey = serviceKey; + this.error = error; + this.failuresCount = failuresCount; + } + + @Override + public String getText() { + String errorMsg = error.getMessage(); + if (errorMsg == null || errorMsg.equals("null")) { + Throwable cause = ExceptionUtils.getRootCause(error); + if (cause != null) { + errorMsg = cause.getMessage(); + } + } + if (errorMsg == null) { + errorMsg = error.getClass().getSimpleName(); + } + return String.format("[%s] Failure: %s (number of subsequent failures: %s)", serviceKey, errorMsg, failuresCount); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java new file mode 100644 index 0000000000..44eaf093cc --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java @@ -0,0 +1,31 @@ +/** + * 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.monitoring.data.notification; + +public class ServiceRecoveryNotification implements Notification { + + private final Object serviceKey; + + public ServiceRecoveryNotification(Object serviceKey) { + this.serviceKey = serviceKey; + } + + @Override + public String getText() { + return String.format("[%s] is OK", serviceKey); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java new file mode 100644 index 0000000000..f882105048 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -0,0 +1,53 @@ +/** + * 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.monitoring.notification; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.data.notification.Notification; +import org.thingsboard.monitoring.notification.channels.NotificationChannel; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +@Component +@RequiredArgsConstructor +@Slf4j +public class NotificationService { + + private final List notificationChannels; + private final ExecutorService notificationExecutor = Executors.newSingleThreadExecutor(); + + public void sendNotification(Notification notification) { + forEachNotificationChannel(notificationChannel -> notificationChannel.sendNotification(notification)); + } + + private void forEachNotificationChannel(Consumer function) { + notificationChannels.forEach(notificationChannel -> { + notificationExecutor.submit(() -> { + try { + function.accept(notificationChannel); + } catch (Exception e) { + log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e); + } + }); + }); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java new file mode 100644 index 0000000000..6c1c23cc28 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java @@ -0,0 +1,24 @@ +/** + * 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.monitoring.notification.channels; + +import org.thingsboard.monitoring.data.notification.Notification; + +public interface NotificationChannel { + + void sendNotification(Notification notification); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java new file mode 100644 index 0000000000..092ea0b8c6 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.channels.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.monitoring.data.notification.Notification; +import org.thingsboard.monitoring.notification.channels.NotificationChannel; + +import javax.annotation.PostConstruct; +import java.time.Duration; +import java.util.Map; + +@Component +@ConditionalOnProperty(value = "monitoring.notification_channels.slack.enabled", havingValue = "true") +@Slf4j +public class SlackNotificationChannel implements NotificationChannel { + + @Value("${monitoring.notification_channels.slack.webhook_url}") + private String webhookUrl; + + private RestTemplate restTemplate; + + @PostConstruct + private void init() { + restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(2)) + .build(); + } + + @Override + public void sendNotification(Notification notification) { + sendNotification(notification.getText()); + } + + private void sendNotification(String message) { + restTemplate.postForObject(webhookUrl, Map.of("text", message), String.class); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java new file mode 100644 index 0000000000..4649e31ac4 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -0,0 +1,144 @@ +/** + * 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.monitoring.service; + +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.data.Latency; +import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.data.notification.HighLatencyNotification; +import org.thingsboard.monitoring.data.notification.ServiceFailureNotification; +import org.thingsboard.monitoring.data.notification.ServiceRecoveryNotification; +import org.thingsboard.monitoring.notification.NotificationService; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MonitoringReporter { + + private final NotificationService notificationService; + + private final Map latencies = new ConcurrentHashMap<>(); + private final Map failuresCounters = new ConcurrentHashMap<>(); + + @Value("${monitoring.failures_threshold}") + private int failuresThreshold; + @Value("${monitoring.send_repeated_failure_notification}") + private boolean sendRepeatedFailureNotification; + + @Value("${monitoring.latency.enabled}") + private boolean latencyReportingEnabled; + @Value("${monitoring.latency.threshold_ms}") + private int latencyThresholdMs; + @Value("${monitoring.latency.reporting_asset_id}") + private String reportingAssetId; + + public void reportLatencies(TbClient tbClient) { + List latencies = this.latencies.values().stream() + .filter(Latency::isNotEmpty) + .map(latency -> { + Latency snapshot = latency.snapshot(); + latency.reset(); + return snapshot; + }) + .collect(Collectors.toList()); + if (latencies.isEmpty()) { + return; + } + log.info("Latencies:\n{}", latencies.stream().map(latency -> latency.getKey() + ": " + latency.getAvg() + " ms") + .collect(Collectors.joining("\n"))); + + if (!latencyReportingEnabled) return; + + if (latencies.stream().anyMatch(latency -> latency.getAvg() >= (double) latencyThresholdMs)) { + HighLatencyNotification highLatencyNotification = new HighLatencyNotification(latencies, latencyThresholdMs); + notificationService.sendNotification(highLatencyNotification); + } + + try { + if (StringUtils.isBlank(reportingAssetId)) { + String assetName = "Monitoring"; + Asset monitoringAsset = tbClient.findAsset(assetName).orElseGet(() -> { + Asset asset = new Asset(); + asset.setType("Monitoring"); + asset.setName(assetName); + asset = tbClient.saveAsset(asset); + log.info("Created monitoring asset {}", asset.getId()); + return asset; + }); + reportingAssetId = monitoringAsset.getId().toString(); + } + + ObjectNode msg = JacksonUtil.newObjectNode(); + latencies.forEach(latency -> { + msg.set(latency.getKey(), new DoubleNode(latency.getAvg())); + }); + tbClient.saveEntityTelemetry(new AssetId(UUID.fromString(reportingAssetId)), "time", msg); + } catch (Exception e) { + log.error("Failed to report latencies: {}", e.getMessage()); + } + } + + public void reportLatency(String key, long latencyInNanos) { + String latencyKey = key + "Latency"; + double latencyInMs = (double) latencyInNanos / 1000_000; + log.trace("Reporting latency [{}]: {} ms", key, latencyInMs); + latencies.computeIfAbsent(latencyKey, k -> new Latency(latencyKey)).report(latencyInMs); + } + + public void serviceFailure(Object serviceKey, Throwable error) { + if (log.isDebugEnabled()) { + log.error("Error occurred", error); + } + int failuresCount = failuresCounters.computeIfAbsent(serviceKey, k -> new AtomicInteger()).incrementAndGet(); + ServiceFailureNotification notification = new ServiceFailureNotification(serviceKey, error, failuresCount); + log.error(notification.getText()); + if (failuresCount == failuresThreshold || (sendRepeatedFailureNotification && failuresCount % failuresThreshold == 0)) { + notificationService.sendNotification(notification); + } + } + + public void serviceIsOk(Object serviceKey) { + ServiceRecoveryNotification notification = new ServiceRecoveryNotification(serviceKey); + if (!serviceKey.equals(MonitoredServiceKey.GENERAL)) { + log.info(notification.getText()); + } + AtomicInteger failuresCounter = failuresCounters.get(serviceKey); + if (failuresCounter != null) { + if (failuresCounter.get() >= failuresThreshold) { + notificationService.sendNotification(notification); + } + failuresCounter.set(0); + } + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java new file mode 100644 index 0000000000..9717b98f51 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java @@ -0,0 +1,120 @@ +/** + * 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.monitoring.transport; + +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.WsClient; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.service.TransportMonitoringConfig; +import org.thingsboard.monitoring.data.Latencies; +import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.data.TransportFailureException; +import org.thingsboard.monitoring.data.TransportInfo; +import org.thingsboard.monitoring.service.MonitoringReporter; +import org.thingsboard.monitoring.util.TbStopWatch; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.UUID; + +@Slf4j +public abstract class TransportHealthChecker { + + protected final C config; + protected final MonitoringTargetConfig target; + private TransportInfo transportInfo; + + @Autowired + private MonitoringReporter reporter; + @Autowired + private TbStopWatch stopWatch; + @Value("${monitoring.check_timeout_ms}") + private int resultCheckTimeoutMs; + + public static final String TEST_TELEMETRY_KEY = "testData"; + + protected TransportHealthChecker(C config, MonitoringTargetConfig target) { + this.config = config; + this.target = target; + } + + @PostConstruct + private void init() { + transportInfo = new TransportInfo(getTransportType(), target.getBaseUrl()); + } + + public final void check(WsClient wsClient) { + log.debug("[{}] Checking", transportInfo); + try { + wsClient.registerWaitForUpdate(); + + String testValue = UUID.randomUUID().toString(); + String testPayload = JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString(); + try { + initClientAndSendPayload(testPayload); + log.trace("[{}] Sent test payload ({})", transportInfo, testPayload); + } catch (Throwable e) { + throw new TransportFailureException(e); + } + + log.trace("[{}] Waiting for WS update", transportInfo); + checkWsUpdate(wsClient, testValue); + + reporter.serviceIsOk(transportInfo); + reporter.serviceIsOk(MonitoredServiceKey.GENERAL); + } catch (TransportFailureException transportFailureException) { + reporter.serviceFailure(transportInfo, transportFailureException); + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, e); + } + } + + private void initClientAndSendPayload(String payload) throws Throwable { + initClient(); + stopWatch.start(); + sendTestPayload(payload); + reporter.reportLatency(Latencies.transportRequest(getTransportType()), stopWatch.getTime()); + } + + private void checkWsUpdate(WsClient wsClient, String testValue) { + stopWatch.start(); + wsClient.waitForUpdate(resultCheckTimeoutMs); + log.trace("[{}] Waited for WS update. Last WS msg: {}", transportInfo, wsClient.lastMsg); + Object update = wsClient.getTelemetryUpdate(target.getDevice().getId(), TEST_TELEMETRY_KEY); + if (update == null) { + throw new TransportFailureException("No WS update arrived within " + resultCheckTimeoutMs + " ms"); + } else if (!update.toString().equals(testValue)) { + throw new TransportFailureException("Was expecting value " + testValue + " but got " + update); + } + reporter.reportLatency(Latencies.WS_UPDATE, stopWatch.getTime()); + } + + + protected abstract void initClient() throws Exception; + + protected abstract void sendTestPayload(String payload) throws Exception; + + @PreDestroy + protected abstract void destroyClient() throws Exception; + + protected abstract TransportType getTransportType(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java new file mode 100644 index 0000000000..b3dd10db15 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java @@ -0,0 +1,143 @@ +/** + * 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.monitoring.transport; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.client.WsClient; +import org.thingsboard.monitoring.client.WsClientFactory; +import org.thingsboard.monitoring.config.DeviceConfig; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.service.TransportMonitoringConfig; +import org.thingsboard.monitoring.data.Latencies; +import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.service.MonitoringReporter; +import org.thingsboard.monitoring.util.TbStopWatch; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +import javax.annotation.PostConstruct; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +@Slf4j +public final class TransportMonitoringService { + + private final List configs; + private final List> transportHealthCheckers = new LinkedList<>(); + private final List devices = new LinkedList<>(); + + private final TbClient tbClient; + private final WsClientFactory wsClientFactory; + private final TbStopWatch stopWatch; + private final MonitoringReporter reporter; + private final ApplicationContext applicationContext; + private ScheduledExecutorService scheduler; + @Value("${monitoring.transports.monitoring_rate_ms}") + private int monitoringRateMs; + + @PostConstruct + private void init() { + configs.forEach(config -> { + config.getTargets().stream() + .filter(target -> StringUtils.isNotBlank(target.getBaseUrl())) + .peek(target -> checkMonitoringTarget(config, target, tbClient)) + .forEach(target -> { + TransportHealthChecker transportHealthChecker = applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); + transportHealthCheckers.add(transportHealthChecker); + devices.add(target.getDevice().getId()); + }); + }); + scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); + } + + @EventListener(ApplicationReadyEvent.class) + public void startMonitoring() { + scheduler.scheduleWithFixedDelay(() -> { + try { + log.debug("Starting transports check"); + stopWatch.start(); + String accessToken = tbClient.logIn(); + reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); + + try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + + for (TransportHealthChecker transportHealthChecker : transportHealthCheckers) { + transportHealthChecker.check(wsClient); + } + } + reporter.reportLatencies(tbClient); + log.debug("Finished transports check"); + } catch (Throwable error) { + try { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); + } catch (Throwable reportError) { + log.error("Error occurred during service failure reporting", reportError); + } + } + }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + } + + private void checkMonitoringTarget(TransportMonitoringConfig config, MonitoringTargetConfig target, TbClient tbClient) { + DeviceConfig deviceConfig = target.getDevice(); + tbClient.logIn(); + + DeviceId deviceId; + if (deviceConfig == null || deviceConfig.getId() == null) { + String deviceName = String.format("[%s] Monitoring device (%s)", config.getTransportType(), target.getBaseUrl()); + Device device = tbClient.getTenantDevice(deviceName) + .orElseGet(() -> { + log.info("Creating new device '{}'", deviceName); + Device monitoringDevice = new Device(); + monitoringDevice.setName(deviceName); + monitoringDevice.setType("default"); + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + return tbClient.saveDevice(monitoringDevice); + }); + deviceId = device.getId(); + target.getDevice().setId(deviceId.toString()); + } else { + deviceId = new DeviceId(deviceConfig.getId()); + } + + log.info("Loading credentials for device {}", deviceId); + DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(deviceId) + .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + deviceId)); + target.getDevice().setCredentials(credentials); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java new file mode 100644 index 0000000000..3e36e62c58 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java @@ -0,0 +1,78 @@ +/** + * 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.monitoring.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.service.CoapTransportMonitoringConfig; +import org.thingsboard.monitoring.transport.TransportHealthChecker; + +import java.io.IOException; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class CoapTransportHealthChecker extends TransportHealthChecker { + + private CoapClient coapClient; + + protected CoapTransportHealthChecker(CoapTransportMonitoringConfig config, MonitoringTargetConfig target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (coapClient == null) { + String accessToken = target.getDevice().getCredentials().getCredentialsId(); + String uri = target.getBaseUrl() + "/api/v1/" + accessToken + "/telemetry"; + coapClient = new CoapClient(uri); + coapClient.setTimeout((long) config.getRequestTimeoutMs()); + log.debug("Initialized CoAP client for URI {}", uri); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + CoapResponse response = coapClient.post(payload, MediaTypeRegistry.APPLICATION_JSON); + CoAP.ResponseCode code = response.getCode(); + if (code.codeClass != CoAP.CodeClass.SUCCESS_RESPONSE.value) { + throw new IOException("COAP client didn't receive success response from transport"); + } + } + + @Override + protected void destroyClient() throws Exception { + if (coapClient != null) { + coapClient.shutdown(); + coapClient = null; + log.info("Disconnected CoAP client"); + } + } + + @Override + protected TransportType getTransportType() { + return TransportType.COAP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java new file mode 100644 index 0000000000..834025b9e5 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java @@ -0,0 +1,67 @@ +/** + * 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.monitoring.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.service.HttpTransportMonitoringConfig; +import org.thingsboard.monitoring.transport.TransportHealthChecker; + +import java.time.Duration; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class HttpTransportHealthChecker extends TransportHealthChecker { + + private RestTemplate restTemplate; + + protected HttpTransportHealthChecker(HttpTransportMonitoringConfig config, MonitoringTargetConfig target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (restTemplate == null) { + restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofMillis(config.getRequestTimeoutMs())) + .setReadTimeout(Duration.ofMillis(config.getRequestTimeoutMs())) + .build(); + log.debug("Initialized HTTP client"); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + String accessToken = target.getDevice().getCredentials().getCredentialsId(); + restTemplate.postForObject(target.getBaseUrl() + "/api/v1/" + accessToken + "/telemetry", payload, String.class); + } + + @Override + protected void destroyClient() throws Exception {} + + @Override + protected TransportType getTransportType() { + return TransportType.HTTP; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java new file mode 100644 index 0000000000..850cfe136b --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java @@ -0,0 +1,87 @@ +/** + * 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.monitoring.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.service.MqttTransportMonitoringConfig; +import org.thingsboard.monitoring.transport.TransportHealthChecker; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class MqttTransportHealthChecker extends TransportHealthChecker { + + private MqttClient mqttClient; + + private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; + + protected MqttTransportHealthChecker(MqttTransportMonitoringConfig config, MonitoringTargetConfig target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (mqttClient == null || !mqttClient.isConnected()) { + String clientId = MqttAsyncClient.generateClientId(); + String accessToken = target.getDevice().getCredentials().getCredentialsId(); + mqttClient = new MqttClient(target.getBaseUrl(), clientId, new MemoryPersistence()); + mqttClient.setTimeToWait(config.getRequestTimeoutMs()); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + options.setConnectionTimeout(config.getRequestTimeoutMs() / 1000); + IMqttToken result = mqttClient.connectWithResult(options); + if (result.getException() != null) { + throw result.getException(); + } + log.debug("Initialized MQTT client for URI {}", mqttClient.getServerURI()); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + MqttMessage message = new MqttMessage(); + message.setPayload(payload.getBytes()); + message.setQos(config.getQos()); + mqttClient.publish(DEVICE_TELEMETRY_TOPIC, message); + } + + @Override + protected void destroyClient() throws Exception { + if (mqttClient != null) { + mqttClient.disconnect(); + mqttClient = null; + log.info("Disconnected MQTT client"); + } + } + + @Override + protected TransportType getTransportType() { + return TransportType.MQTT; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java b/monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java new file mode 100644 index 0000000000..49e387999a --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java @@ -0,0 +1,41 @@ +/** + * 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.monitoring.util; + +import org.apache.commons.lang3.time.StopWatch; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TbStopWatch { + + private final StopWatch internal = new StopWatch(); + + public void start() { + internal.reset(); + internal.start(); + } + + public long getTime() { + internal.stop(); + long nanoTime = internal.getNanoTime(); + internal.reset(); + return nanoTime; + } + +} diff --git a/monitoring/src/main/resources/logback.xml b/monitoring/src/main/resources/logback.xml new file mode 100644 index 0000000000..a7ca84126f --- /dev/null +++ b/monitoring/src/main/resources/logback.xml @@ -0,0 +1,38 @@ + + + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml new file mode 100644 index 0000000000..403b29acc2 --- /dev/null +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -0,0 +1,107 @@ +# +# 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. +# + +monitoring: + # Monitored server domain + domain: "${DOMAIN:localhost}" + rest: + # Base REST API url, https://DOMAIN by default + base_url: '${REST_BASE_URL:https://${monitoring.domain}}' + # Authentication username + username: '${REST_AUTH_USERNAME:tenant@thingsboard.org}' + # Authentication password + password: '${REST_AUTH_PASSWORD:tenant}' + # REST request timeout in milliseconds + request_timeout_ms: '${REST_REQUEST_TIMEOUT_MS:5000}' + ws: + # WebSocket url, wss://DOMAIN by default + base_url: '${WS_BASE_URL:wss://${monitoring.domain}}' + # WebSocket request timeout + request_timeout_ms: '${WS_REQUEST_TIMEOUT_MS:3000}' + + # Maximum time between request to transport and WebSocket update + check_timeout_ms: '${CHECK_TIMEOUT_MS:5000}' + + # Failures threshold for notifying + failures_threshold: '${FAILURES_THRESHOLD:2}' + # Whether to notify about next failures after first notification (will notify after each FAILURES_THRESHOLD failures) + send_repeated_failure_notification: '${SEND_REPEATED_FAILURE_NOTIFICATION:true}' + + transports: + # Transports check frequency in milliseconds + monitoring_rate_ms: '${TRANSPORTS_MONITORING_RATE_MS:10000}' + + mqtt: + # Enable MQTT checks + enabled: '${MQTT_TRANSPORT_MONITORING_ENABLED:true}' + # MQTT request timeout in milliseconds + request_timeout_ms: '${MQTT_REQUEST_TIMEOUT_MS:4000}' + # MQTT QoS + qos: '${MQTT_QOS_LEVEL:1}' + targets: + # MQTT base url, tcp://DOMAIN:1883 by default + - base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}' + device: + # MQTT device to push telemetry for. If not set - device will be found or created automatically + id: '${MQTT_TRANSPORT_TARGET_DEVICE_ID:}' + # To add more targets, use following environment variables: + # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[1].device.id, + # monitoring.transports.mqtt.targets[2].base_url, monitoring.transports.mqtt.targets[2].device.id, etc. + + coap: + # Enable CoAP checks + enabled: '${COAP_TRANSPORT_MONITORING_ENABLED:true}' + # CoAP request timeout in milliseconds + request_timeout_ms: '${COAP_REQUEST_TIMEOUT_MS:4000}' + targets: + # CoAP base url, coap://DOMAIN by default + - base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}' + # CoAP device to push telemetry for. If not set - device will be found or created automatically + device: + id: '${COAP_TRANSPORT_TARGET_DEVICE_ID:}' + # To add more targets, use following environment variables: + # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[1].device.id, + # monitoring.transports.coap.targets[2].base_url, monitoring.transports.coap.targets[2].device.id, etc. + + http: + # Enable HTTP checks + enabled: '${HTTP_TRANSPORT_MONITORING_ENABLED:true}' + # HTTP request timeout in milliseconds + request_timeout_ms: '${HTTP_REQUEST_TIMEOUT_MS:4000}' + targets: + # HTTP base url, https://DOMAIN by default + - base_url: '${HTTP_TRANSPORT_BASE_URL:https://${monitoring.domain}}' + device: + # HTTP device to push telemetry for. If not set - device will be found or created automatically + id: '${HTTP_TRANSPORT_TARGET_DEVICE_ID:}' + # To add more targets, use following environment variables: + # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[1].device.id, + # monitoring.transports.http.targets[2].base_url, monitoring.transports.http.targets[2].device.id, etc. + + notification_channels: + slack: + # Enable notifying via Slack + enabled: '${SLACK_NOTIFICATION_CHANNEL_ENABLED:false}' + # Slack webhook url + webhook_url: '${SLACK_WEBHOOK_URL:}' + + latency: + # Enable latencies reporting + enabled: "${LATENCY_REPORTING_ENABLED:true}" + # Latency threshold for notifying + threshold_ms: '${LATENCY_THRESHOLD:2000}' + # ID of the asset to save latencies to. If not set and latencies reporting is enabled - asset will be found or created automatically + reporting_asset_id: '${LATENCY_REPORTING_ASSET_ID:}' diff --git a/msa/monitoring/docker/Dockerfile b/msa/monitoring/docker/Dockerfile new file mode 100644 index 0000000000..5da050eb91 --- /dev/null +++ b/msa/monitoring/docker/Dockerfile @@ -0,0 +1,30 @@ +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM thingsboard/openjdk11:bullseye-slim + +COPY start-tb-monitoring.sh ${pkg.name}.deb /tmp/ + +RUN chmod a+x /tmp/*.sh \ + && mv /tmp/start-tb-monitoring.sh /usr/bin && \ + (yes | dpkg -i /tmp/${pkg.name}.deb) && \ + rm /tmp/${pkg.name}.deb && \ + (systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :) && \ + chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar + +USER ${pkg.user} + +CMD ["start-tb-monitoring.sh"] diff --git a/msa/monitoring/docker/start-tb-monitoring.sh b/msa/monitoring/docker/start-tb-monitoring.sh new file mode 100755 index 0000000000..d5bcc52d37 --- /dev/null +++ b/msa/monitoring/docker/start-tb-monitoring.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# 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. +# + +CONF_FOLDER=${pkg.installFolder}/conf +jarfile=${pkg.installFolder}/bin/${pkg.name}.jar +configfile=${pkg.name}.conf + +source "${CONF_FOLDER}/${configfile}" + +echo "Starting '${project.name}' ..." + +cd ${pkg.installFolder}/bin + +exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.monitoring.ThingsboardMonitoringApplication \ + -Dspring.jpa.hibernate.ddl-auto=none \ + -Dlogging.config=$CONF_FOLDER/logback.xml \ + org.springframework.boot.loader.PropertiesLauncher diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml new file mode 100644 index 0000000000..09b492f759 --- /dev/null +++ b/msa/monitoring/pom.xml @@ -0,0 +1,192 @@ + + + + 4.0.0 + + org.thingsboard + 3.5.0-SNAPSHOT + msa + + + org.thingsboard.msa + monitoring + pom + + ThingsBoard Monitoring Microservice + + + UTF-8 + ${basedir}/../.. + tb-monitoring + tb-monitoring + /var/log/${pkg.name} + /usr/share/${pkg.name} + pre-integration-test + + + + + org.thingsboard + monitoring + ${project.version} + deb + deb + provided + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-tb-monitoring-deb + package + + copy + + + + + org.thingsboard + monitoring + deb + deb + ${pkg.name}.deb + ${project.build.directory} + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-docker-config + process-resources + + copy-resources + + + ${project.build.directory} + + + docker + true + + + + + + + + com.spotify + dockerfile-maven-plugin + + + build-docker-image + pre-integration-test + + build + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + true + false + ${project.build.directory} + + + + tag-docker-image + pre-integration-test + + tag + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + ${project.version} + + + + + + + + + push-docker-image + + + push-docker-image + + + + + + com.spotify + dockerfile-maven-plugin + + + push-latest-docker-image + pre-integration-test + + push + + + latest + ${docker.repo}/${docker.name} + + + + push-version-docker-image + pre-integration-test + + push + + + ${project.version} + ${docker.repo}/${docker.name} + + + + + + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + + \ No newline at end of file diff --git a/msa/pom.xml b/msa/pom.xml index ad40e574ec..6b76f974f1 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -47,6 +47,7 @@ tb-node transport js-executor + monitoring diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index ed1377e1b9..2f73ab31f7 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -50,6 +50,13 @@ queue: print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip @@ -167,6 +174,14 @@ vc: io_pool_size: "${TB_VC_GIT_POOL_SIZE:3}" repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" diff --git a/pom.xml b/pom.xml index fb1aafd6b6..f78a4826bc 100755 --- a/pom.xml +++ b/pom.xml @@ -39,13 +39,13 @@ 1.3.2 2.3.2 2.3.2 - 2.7.0 - 2.7.0 - 5.3.20 - 5.5.12 - 5.7.1 - 2.7.0 - 3.7.1 + 2.7.10 + 2.7.10 + 5.3.26 + 5.5.17 + 5.7.7 + 2.7.10 + 3.8.0 0.7.0 1.7.32 2.17.1 @@ -75,14 +75,14 @@ 2.3.30 1.6.2 4.2.0 - 3.5.5 + 3.8.1 3.21.9 1.42.1 1.0.6 1.18.18 1.2.4 1.2.5 - 4.1.75.Final + 4.1.91.Final 2.0.51.Final 1.7.0 4.8.0 @@ -107,13 +107,13 @@ 4.1.1 2.57 2.7.7 - 1.27 + 2.0 1.11.747 1.105.0 2.1.0 3.2.0 1.5.0 - 1.4.3 + 1.5.4 1.9.4 3.2.2 1.9.0 @@ -123,7 +123,7 @@ 6.0.20.Final 3.0.0 2.0.1.Final - 1.6.8 + 1.7.2 2.8.5 4.1.0 @@ -164,6 +164,7 @@ application msa rest-client + monitoring @@ -194,6 +195,7 @@ org.apache.maven.plugins maven-resources-plugin + 3.2.0 copy-conf @@ -623,7 +625,7 @@ org.apache.maven.plugins maven-resources-plugin - 2.7 + 3.2.0 org.apache.maven.plugins @@ -1084,6 +1086,11 @@ jaxb-runtime ${jaxb-runtime.version} + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + org.springframework.boot spring-boot-starter-security diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index 029531494e..65b7800fe3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -21,7 +21,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; @@ -127,7 +126,6 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { } } - @NotNull private FutureCallback getFutureCallback(TbContext ctx, TbMsg msg, EntityView entityView) { return new FutureCallback() { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 7be7dae2de..318b38aa39 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -77,9 +77,9 @@ public abstract class AbstractTbMsgPushNode entityBody = new HashMap<>(); Map metadata = msg.getMetaData().getData(); + EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType, metadata); + Map entityBody = new HashMap<>(); JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); switch (actionType) { case ATTRIBUTES_UPDATED: @@ -148,7 +148,7 @@ public abstract class AbstractTbMsgPushNode metadata) { EdgeEventActionType actionType; if (SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) || DataConstants.TIMESERIES_UPDATED.equals(msgType)) { @@ -159,6 +159,16 @@ public abstract class AbstractTbMsgPushNode miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT, + DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); + for (String event : miscEvents) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); + testEvent(event, metaData, EdgeEventActionType.ATTRIBUTES_UPDATED, "kv"); + } + } + + @Test + public void testMiscEventsProcessedAsTimeseriesUpdated() { + List miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT, + DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); + for (String event : miscEvents) { + testEvent(event, new TbMsgMetaData(), EdgeEventActionType.TIMESERIES_UPDATED, "data"); + } + } + + private void testEvent(String event, TbMsgMetaData metaData, EdgeEventActionType expectedType, String dataKey) { + Mockito.when(ctx.getTenantId()).thenReturn(tenantId); + Mockito.when(ctx.getEdgeService()).thenReturn(edgeService); + Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService); + Mockito.when(ctx.getDbCallbackExecutor()).thenReturn(dbCallbackExecutor); + Mockito.when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create()); + + TbMsg msg = TbMsg.newMsg(event, new EdgeId(UUID.randomUUID()), metaData, + TbMsgDataType.JSON, "{\"lastConnectTs\":1}", null, null); + + node.onMsg(ctx, msg); + + ArgumentMatcher eventArgumentMatcher = edgeEvent -> + edgeEvent.getAction().equals(expectedType) + && edgeEvent.getBody().get(dataKey).get("lastConnectTs").asInt() == 1; + verify(edgeEventService).saveAsync(Mockito.argThat(eventArgumentMatcher)); + + Mockito.reset(ctx, edgeEventService); + } } diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index bcf1e8ddd7..1bf71fee13 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -151,6 +151,13 @@ queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip @@ -270,6 +277,14 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 56a8419e76..caeb1bfc2a 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -136,6 +136,13 @@ queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip @@ -255,6 +262,14 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 70d7a895fd..f930836d16 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -217,6 +217,13 @@ queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip @@ -337,6 +344,14 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 99907e797e..89074fd649 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -166,6 +166,13 @@ queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip @@ -285,6 +292,14 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 16bfb6397a..169e464e57 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -112,6 +112,13 @@ queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" acks: "${TB_KAFKA_ACKS:all}" retries: "${TB_KAFKA_RETRIES:1}" compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip @@ -235,6 +242,14 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 311de3d7a0..d1be94fdb6 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -297,16 +297,6 @@ export class AuthService { } else if (authState.authUser.isPublic) { result = this.router.parseUrl(`dashboard/${authState.lastPublicDashboardId}`); } - } else if (authState.authUser.authority === Authority.SYS_ADMIN) { - this.adminService.checkUpdates().subscribe((updateMessage) => { - if (updateMessage && updateMessage.updateAvailable) { - this.store.dispatch(new ActionNotificationShow( - {message: updateMessage.message, - type: 'info', - verticalPosition: 'bottom', - horizontalPosition: 'right'})); - } - }); } } } else { diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index 6874d21d74..cd83c3c8c9 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -21,6 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { AdminSettings, AutoCommitSettings, + FeaturesInfo, JwtSettings, MailServerSettings, RepositorySettings, @@ -131,4 +132,8 @@ export class AdminService { public checkUpdates(config?: RequestConfig): Observable { return this.http.get(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); } + + public getFeaturesInfo(config?: RequestConfig): Observable { + return this.http.get('/api/admin/featuresInfo', defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/core/http/user-settings.service.ts b/ui-ngx/src/app/core/http/user-settings.service.ts index 8aed9d1345..55ce524750 100644 --- a/ui-ngx/src/app/core/http/user-settings.service.ts +++ b/ui-ngx/src/app/core/http/user-settings.service.ts @@ -16,10 +16,16 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models'; +import { + DocumentationLink, DocumentationLinks, + GettingStarted, + initialUserSettings, + UserSettings, + UserSettingsType +} from '@shared/models/user-settings.models'; import { map } from 'rxjs/operators'; import { HttpClient } from '@angular/common/http'; -import { defaultHttpOptionsFromConfig } from '@core/http/http-utils'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; @Injectable({ providedIn: 'root' @@ -51,4 +57,24 @@ export class UserSettingsService { defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true})); } + public getDocumentationLinks(config?: RequestConfig): Observable { + return this.http.get(`/api/user/settings/${UserSettingsType.DOC_LINKS}`, + defaultHttpOptionsFromConfig(config)); + } + + public updateDocumentationLinks(documentationLinks: DocumentationLinks, config?: RequestConfig): Observable { + return this.http.put(`/api/user/settings/${UserSettingsType.DOC_LINKS}`, documentationLinks, + defaultHttpOptionsFromConfig(config)); + } + + public getGettingStarted(config?: RequestConfig): Observable { + return this.http.get(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`, + defaultHttpOptionsFromConfig(config)); + } + + public updateGettingStarted(gettingStarted: GettingStarted, config?: RequestConfig): Observable { + return this.http.put(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`, gettingStarted, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 394e55ff4c..a56b8f1708 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -238,6 +238,7 @@ export class DashboardUtilsService { backgroundColor: '#eeeeee', columns: 24, margin: 10, + outerMargin: true, backgroundSizeMode: '100%' }; } @@ -282,6 +283,7 @@ export class DashboardUtilsService { layout.gridSettings.margin = layout.gridSettings.margins[0]; delete layout.gridSettings.margins; } + layout.gridSettings.outerMargin = isDefined(layout.gridSettings.outerMargin) ? layout.gridSettings.outerMargin : true; layout.gridSettings.margin = isDefined(layout.gridSettings.margin) ? layout.gridSettings.margin : 10; } @@ -604,6 +606,47 @@ export class DashboardUtilsService { delete entityAlias.entityType; delete entityAlias.entityFilter; } + entityAlias = this.validateAndUpdateEntityAliasSingleTypeFilters(entityAlias); + return entityAlias; + } + + private validateAndUpdateEntityAliasSingleTypeFilters(entityAlias: EntityAlias): EntityAlias { + if (entityAlias.filter.type === AliasFilterType.deviceType) { + if (entityAlias.filter.deviceType) { + if (!entityAlias.filter.deviceTypes) { + entityAlias.filter.deviceTypes = []; + } + entityAlias.filter.deviceTypes.push(entityAlias.filter.deviceType); + delete entityAlias.filter.deviceType; + } + } + if (entityAlias.filter.type === AliasFilterType.assetType) { + if (entityAlias.filter.assetType) { + if (!entityAlias.filter.assetTypes) { + entityAlias.filter.assetTypes = []; + } + entityAlias.filter.assetTypes.push(entityAlias.filter.assetType); + delete entityAlias.filter.assetType; + } + } + if (entityAlias.filter.type === AliasFilterType.entityViewType) { + if (entityAlias.filter.entityViewType) { + if (!entityAlias.filter.entityViewTypes) { + entityAlias.filter.entityViewTypes = []; + } + entityAlias.filter.entityViewTypes.push(entityAlias.filter.entityViewType); + delete entityAlias.filter.entityViewType; + } + } + if (entityAlias.filter.type === AliasFilterType.edgeType) { + if (entityAlias.filter.edgeType) { + if (!entityAlias.filter.edgeTypes) { + entityAlias.filter.edgeTypes = []; + } + entityAlias.filter.edgeTypes.push(entityAlias.filter.edgeType); + delete entityAlias.filter.edgeType; + } + } return entityAlias; } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html index d7e3366a35..8618592ae0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
- + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index 85bdf96ea3..0db5bd9584 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ 'alarm.alarm-details' | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 9c1732791c..2090e5f42c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -132,6 +132,9 @@ {{ 'dashboard.max-margin-message' | translate }} + + {{ 'dashboard.apply-outer-margin' | translate }} + {{ 'dashboard.autofill-height' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index f1407d6aaf..4844211355 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -156,6 +156,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent
{ + this.addEntity(null); + }, 0); + } + } } ngAfterViewInit() { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts index 7ed0a8b3c9..9f4927402e 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -84,47 +84,63 @@ export class EntityFilterViewComponent implements ControlValueAccessor { this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description'); break; case AliasFilterType.assetType: - const assetType = this.filter.assetType; + const assetTypesQuoted = []; + this.filter.assetTypes.forEach((filterAssetType) => { + assetTypesQuoted.push(`'${filterAssetType}'`); + }); + const assetTypes = assetTypesQuoted.join(', '); prefix = this.filter.assetNameFilter; if (prefix && prefix.length) { this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description', - {assetType, prefix}); + {assetTypes, prefix}); } else { this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description', - {assetType}); + {assetTypes}); } break; case AliasFilterType.deviceType: - const deviceType = this.filter.deviceType; + const deviceTypesQuoted = []; + this.filter.deviceTypes.forEach((filterDeviceType) => { + deviceTypesQuoted.push(`'${filterDeviceType}'`); + }); + const deviceTypes = deviceTypesQuoted.join(', '); prefix = this.filter.deviceNameFilter; if (prefix && prefix.length) { this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description', - {deviceType, prefix}); + {deviceTypes, prefix}); } else { this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description', - {deviceType}); + {deviceTypes}); } break; case AliasFilterType.edgeType: - const edgeType = this.filter.edgeType; + const edgeTypesQuoted = []; + this.filter.edgeTypes.forEach((filterEdgeType) => { + edgeTypesQuoted.push(`'${filterEdgeType}'`); + }); + const edgeTypes = edgeTypesQuoted.join(', '); prefix = this.filter.edgeNameFilter; if (prefix && prefix.length) { this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-type-and-name-description', - {edgeType, prefix}); + {edgeTypes, prefix}); } else { this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-type-description', - {edgeType}); + {edgeTypes}); } break; case AliasFilterType.entityViewType: - const entityView = this.filter.entityViewType; + const entityViewTypesQuoted = []; + this.filter.entityViewTypes.forEach((entityViewType) => { + entityViewTypesQuoted.push(`'${entityViewType}'`); + }); + const entityViewTypes = entityViewTypesQuoted.join(', '); prefix = this.filter.entityViewNameFilter; if (prefix && prefix.length) { this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description', - {entityView, prefix}); + {entityViewTypes, prefix}); } else { this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description', - {entityView}); + {entityViewTypes}); } break; case AliasFilterType.relationsQuery: diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index c28dabf959..9cde6f4431 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -83,10 +83,10 @@
- - + + asset.name-starts-with - - + + device.name-starts-with - - + + entity-view.name-starts-with - - + + edge.name-starts-with diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts index 92fe48ee44..32a0377d42 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts @@ -136,25 +136,25 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit { break; case AliasFilterType.assetType: this.filterFormGroup = this.fb.group({ - assetType: [filter ? filter.assetType : null, [Validators.required]], + assetTypes: [filter ? filter.assetTypes : null, [Validators.required]], assetNameFilter: [filter ? filter.assetNameFilter : '', []], }); break; case AliasFilterType.deviceType: this.filterFormGroup = this.fb.group({ - deviceType: [filter ? filter.deviceType : null, [Validators.required]], + deviceTypes: [filter ? filter.deviceTypes : null, [Validators.required]], deviceNameFilter: [filter ? filter.deviceNameFilter : '', []], }); break; case AliasFilterType.edgeType: this.filterFormGroup = this.fb.group({ - edgeType: [filter ? filter.edgeType : null, [Validators.required]], + edgeTypes: [filter ? filter.edgeTypes : null, [Validators.required]], edgeNameFilter: [filter ? filter.edgeNameFilter : '', []], }); break; case AliasFilterType.entityViewType: this.filterFormGroup = this.fb.group({ - entityViewType: [filter ? filter.entityViewType : null, [Validators.required]], + entityViewTypes: [filter ? filter.entityViewTypes : null, [Validators.required]], entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []], }); break; 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 2d24bc479f..69f72c8aaf 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 @@ -69,7 +69,7 @@ style="min-width: 48px;" type="button" (click)="duplicateFilter($index)" - matTooltip="{{ 'filter.duplicate-filter' | translate }}" + matTooltip="{{ 'filter.duplicate-filter-action' | translate }}" matTooltipPosition="above"> content_copy diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html index 096cecd91a..0a0a5e3cdb 100644 --- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html @@ -25,13 +25,13 @@
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 81a1206800..efa6ef7e13 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -410,6 +410,9 @@ {{ 'tenant-profile.too-small-value-one' | translate}} + + {{ 'tenant-profile.queue-size-is-limited-by-system-configuration' | translate}} +
diff --git a/ui-ngx/src/app/modules/home/components/tokens.ts b/ui-ngx/src/app/modules/home/components/tokens.ts index 5ddd9fe661..abba6a0636 100644 --- a/ui-ngx/src/app/modules/home/components/tokens.ts +++ b/ui-ngx/src/app/modules/home/components/tokens.ts @@ -23,6 +23,9 @@ export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = export const HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = new InjectionToken>('HOME_COMPONENTS_MODULE_TOKEN'); +export const WIDGET_COMPONENTS_MODULE_TOKEN: InjectionToken> = + new InjectionToken>('WIDGET_COMPONENTS_MODULE_TOKEN'); + export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken> = new InjectionToken>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN'); diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts index 73b67017ff..356b31a312 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts @@ -16,7 +16,7 @@ import { Inject, Injectable, Type } from '@angular/core'; import { Observable } from 'rxjs'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { AuthService } from '@core/auth/auth.service'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; @@ -44,7 +44,8 @@ export class CustomDialogService { ) { } - customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any): Observable { + customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, + config?: MatDialogConfig): Observable { return this.dynamicComponentFactoryService.createDynamicComponentFactory( class CustomDialogComponentInstance extends CustomDialogComponent {}, template, @@ -55,13 +56,17 @@ export class CustomDialogService { customComponentFactory: factory, data }; + let dialogConfig: MatDialogConfig = { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: dialogData + }; + if (config) { + dialogConfig = {...dialogConfig, ...config}; + } return this.dialog.open( CustomDialogContainerComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: dialogData - }).afterClosed().pipe( + dialogConfig).afterClosed().pipe( tap(() => { this.dynamicComponentFactoryService.destroyDynamicComponentFactory(factory); }) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index f91caf9baa..9a23e86bf8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -91,14 +91,18 @@ {{ getUserInitials(alarm) }} + matTooltipPosition="above"> {{ getUserDisplayName(alarm) }} - + account_circle - alarm.unassigned + + alarm.unassigned + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.scss new file mode 100644 index 0000000000..da03a5e40b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.scss @@ -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. + */ +:host { + .tb-add-doc-link-dialog { + width: 480px; + .mat-toolbar-single-row { + padding: 0 24px; + } + h2 { + color: rgba(0, 0, 0, 0.76); + } + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts new file mode 100644 index 0000000000..8a7413b7dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DocumentationLink } from '@shared/models/user-settings.models'; + +@Component({ + selector: 'tb-add-doc-link-dialog', + templateUrl: './add-doc-link-dialog.component.html', + styleUrls: ['./add-doc-link-dialog.component.scss'] +}) +export class AddDocLinkDialogComponent extends + DialogComponent implements OnInit { + + addDocLinkFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.addDocLinkFormGroup = this.fb.group({ + docLink: [{ icon: 'notifications' }, [Validators.required]] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(docLink: DocumentationLink): void { + this.dialogRef.close(docLink); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html new file mode 100644 index 0000000000..01c02757a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html @@ -0,0 +1,77 @@ + + + + + {{ 'widgets.cluster-info.service-id' | translate }} + + + {{ serviceData.serviceId }} + + + + + {{ 'widgets.cluster-info.service-type' | translate }} + + + {{ serviceData.serviceType }} + + + + + {{ 'widgets.system-info.cpu' | translate }} + + + {{ serviceData.cpuUsage }}% + + + + + {{ 'widgets.system-info.ram' | translate }} + + + {{ serviceData.memoryUsage }}% + + + + + {{ 'widgets.system-info.disk' | translate }} + + + {{ serviceData.discUsage }}% + + + + +
+widgets.cluster-info.no-data diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss new file mode 100644 index 0000000000..0d76956dfc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + span.no-data-found { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } + + .tb-resource-usage { + cursor: default; + &.tb-status-warn, &.tb-status-critical { + position: relative; + overflow: visible; + &:before { + position: absolute; + top: 15px; + left: -24px; + font-family: 'Material Icons Round'; + font-size: 20px; + line-height: 1; + } + } + &.tb-status-warn { + color: #FAA405; + &:before { + content: "warning"; + color: #FAA405; + } + } + &.tb-status-critical { + color: #D12730; + &:before { + content: "warning"; + color: #D12730; + } + } + } +} + +:host ::ng-deep { + .mdc-data-table__header-cell { + font-weight: 500; + font-size: 11px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + } + .mdc-data-table__cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts new file mode 100644 index 0000000000..7940d2c21a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts @@ -0,0 +1,237 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, NgZone, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount, share } from 'rxjs/operators'; +import { MatSort } from '@angular/material/sort'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { EntityDataUpdate, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { EntityKeyType } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; + +export interface SystemInfoData { + serviceId: string; + serviceType: string; + cpuUsage: number; + cpuCount: number; + memoryUsage: number; + totalMemory: number; + discUsage: number; + totalDiscSpace: number; +} + +@Component({ + selector: 'tb-cluster-info-table', + templateUrl: './cluster-info-table.component.html', + styleUrls: ['./cluster-info-table.component.scss'] +}) +export class ClusterInfoTableComponent extends PageComponent implements OnInit, AfterViewInit { + + @ViewChild(MatSort) sort: MatSort; + + displayedColumns = ['serviceId', 'serviceType', 'cpuUsage', 'memoryUsage', 'discUsage']; + + dataSource: SystemInfoDataSource; + + pageLink: PageLink; + + constructor(protected store: Store, + private telemetryWsService: TelemetryWebsocketService, + private translate: TranslateService, + private zone: NgZone) { + super(store); + } + + ngOnInit() { + this.dataSource = new SystemInfoDataSource(this.telemetryWsService, this.zone); + const sortOrder: SortOrder = { + property: 'serviceId', + direction: Direction.ASC + }; + this.pageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); + this.dataSource.loadData(this.pageLink); + } + + ngAfterViewInit() { + this.sort.sortChange.subscribe(() => this.updateData()); + } + + updateData() { + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadData(this.pageLink); + } + + statusClass(value: number): string { + let status = ''; + if (value > 85) { + status = 'tb-status-critical'; + } else if (value > 75) { + status = 'tb-status-warn'; + } + return status; + } + + infoTooltip(serviceData: SystemInfoData, type: 'cpu' | 'ram' | 'disc'): string { + let tooltip = ''; + let value: number; + switch (type) { + case 'cpu': + value = serviceData.cpuUsage; + tooltip += value + ' / 100 %'; + break; + case 'ram': + value = serviceData.memoryUsage; + const memoryUsage = serviceData.memoryUsage; + const totalMemory = serviceData.totalMemory / 1073741824; + const usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0); + tooltip += usedMemory + ' / ' + totalMemory.toFixed(0) + ' Gb'; + break; + case 'disc': + value = serviceData.discUsage; + const discUsage = serviceData.discUsage; + const totalDiscSpace = serviceData.totalDiscSpace / 1073741824; + const usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0); + tooltip += usedDisc + ' / ' + totalDiscSpace.toFixed(0) + ' Gb'; + break; + } + if (value > 85) { + switch (type) { + case 'cpu': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.cpu-critical-text'); + break; + case 'ram': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.ram-critical-text'); + break; + case 'disc': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.disk-critical-text'); + break; + } + } else if (value > 75) { + switch (type) { + case 'cpu': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.cpu-warning-text'); + break; + case 'ram': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.ram-warning-text'); + break; + case 'disc': + tooltip += '\n\n' + this.translate.instant('widgets.system-info.disk-warning-text'); + break; + } + } + return tooltip; + } +} + +export class SystemInfoDataSource implements DataSource { + + private systemDataSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + private allSystemData: Observable>; + + private telemetrySubscriber: TelemetrySubscriber; + + constructor(private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.systemDataSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.systemDataSubject.complete(); + this.pageDataSubject.complete(); + if (this.telemetrySubscriber) { + this.telemetrySubscriber.unsubscribe(); + this.telemetrySubscriber = null; + } + } + + loadData(pageLink: PageLink): void { + this.getAllSystemData().pipe( + map((data) => pageLink.filterData(data)), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.onSystemData(pageData.data); + this.pageDataSubject.next(pageData); + } + ); + } + + getAllSystemData(): Observable> { + if (!this.allSystemData) { + this.telemetrySubscriber = TelemetrySubscriber.createEntityFilterLatestSubscription( + this.telemetryWsService, { type: AliasFilterType.apiUsageState }, this.zone, + [ { key: 'clusterSystemData', type: EntityKeyType.TIME_SERIES } ]); + this.telemetrySubscriber.subscribe(); + this.allSystemData = this.telemetrySubscriber.entityData$.pipe( + map((update) => this.toSystemInfoData(update)), + publishReplay(1), + refCount() + ); + } + return this.allSystemData; + } + + private toSystemInfoData(entityDataUpdate: EntityDataUpdate): Array { + const entityData = entityDataUpdate.data?.data ? entityDataUpdate.data?.data : entityDataUpdate.update; + if (entityData && entityData.length) { + const latest = entityData[0].latest; + if (latest && latest[EntityKeyType.TIME_SERIES]) { + const latestTs = latest[EntityKeyType.TIME_SERIES]; + const tsValue = latestTs.clusterSystemData; + if (tsValue && tsValue.value) { + return JSON.parse(tsValue.value); + } + } + } + return []; + } + + protected onSystemData(data: SystemInfoData[]) { + this.systemDataSubject.next(data); + } + + isEmpty(): Observable { + return this.systemDataSubject.pipe( + map((entities) => !entities.length), + share() + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements), + share() + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.html new file mode 100644 index 0000000000..9c028856be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.html @@ -0,0 +1,72 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss new file mode 100644 index 0000000000..427aa8bf3a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-content { + width: 100%; + height: 100%; + } + + .tb-title { + margin-top: 8px; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 16px; + } + } + + .tb-info-icon { + color: rgba(0, 0, 0, 0.12); + font-size: 16px; + width: 16px; + height: 16px; + line-height: 15px; + vertical-align: middle; + } + + .tb-features-list { + overflow: auto; + } + + .mat-grid-tile.tb-feature-tile { + overflow: visible; + } + + .tb-feature-button { + height: 50px; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + padding: 12px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + @media #{$mat-md-lg} { + height: 22px; + padding: 0 8px; + } + &:hover { + border: 1px solid rgba(0, 0, 0, 0.12); + box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08); + } + .tb-feature-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.87); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } + .tb-feature-status { + display: inline-block; + font-family: 'Material Icons Round'; + font-size: 20px; + line-height: 20px; + color: rgba(0, 0, 0, 0.12); + font-weight: 600; + position: relative; + background: #F4F4F4; + border-radius: 4px; + width: 24px; + height: 24px; + padding: 2px; + margin-right: 8px; + vertical-align: middle; + @media #{$mat-md-lg} { + font-size: 14px; + line-height: 14px; + width: 18px; + height: 18px; + margin-right: 4px; + } + } + &.configured { + .tb-feature-status { + color: #198038; + background: #F3F6FA; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts new file mode 100644 index 0000000000..be58092f1f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +import { FeaturesInfo } from '@shared/models/settings.models'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { of, Subscription } from 'rxjs'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { TranslateService } from '@ngx-translate/core'; +import { MediaBreakpoints } from '@shared/models/constants'; + +@Component({ + selector: 'tb-configured-features', + templateUrl: './configured-features.component.html', + styleUrls: ['./configured-features.component.scss'] +}) +export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy { + + authUser = getCurrentAuthUser(this.store); + featuresInfo: FeaturesInfo; + rowHeight = '50px'; + gutterSize = '12px'; + bottomColspan = 2; + lastColspan = 2; + + private observeBreakpointSubscription: Subscription; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private adminService: AdminService, + private translate: TranslateService, + private breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + const isLtMd = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']); + this.rowHeight = isMdLg ? '22px' : '50px'; + this.gutterSize = isMdLg ? '8px' : '12px'; + this.bottomColspan = isLtMd ? 3 : 2; + this.lastColspan = isLtMd ? 6 : 2; + this.observeBreakpointSubscription = this.breakpointObserver + .observe([MediaBreakpoints['md-lg'], MediaBreakpoints['lt-md']]) + .subscribe((state: BreakpointState) => { + if (state.breakpoints[MediaBreakpoints['md-lg']]) { + this.rowHeight = '22px'; + this.gutterSize = '8px'; + } else { + this.rowHeight = '50px'; + this.gutterSize = '12px'; + } + if (state.breakpoints[MediaBreakpoints['lt-md']]) { + this.bottomColspan = 3; + this.lastColspan = 6; + } else { + this.bottomColspan = 2; + this.lastColspan = 2; + } + this.cd.markForCheck(); + } + ); + (this.authUser.authority === Authority.SYS_ADMIN ? + this.adminService.getFeaturesInfo() : of(null)).subscribe( + (featuresInfo) => { + this.featuresInfo = featuresInfo; + this.cd.markForCheck(); + } + ); + } + + ngOnDestroy() { + if (this.observeBreakpointSubscription) { + this.observeBreakpointSubscription.unsubscribe(); + } + super.ngOnDestroy(); + } + + featureTooltip(configured: boolean): string { + if (configured) { + return this.translate.instant('widgets.configured-features.feature-configured'); + } else { + return this.translate.instant('widgets.configured-features.feature-not-configured'); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html new file mode 100644 index 0000000000..c64958a2ec --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html @@ -0,0 +1,70 @@ + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss new file mode 100644 index 0000000000..1d9de4fc38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-edit-doc-link { + padding: 16px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + &.edit-mode { + border: 1px solid rgba(48, 86, 128, 0.32); + } + } + .tb-doc-link { + height: 55px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 8px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + .tb-doc-container { + display: flex; + flex-direction: row; + align-items: center; + .tb-doc-icon-container { + height: 40px; + padding: 8px; + background: #F3F6FA; + border-radius: 6px; + margin-right: 8px; + } + .tb-doc-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.87); + } + } + } + .tb-edit-buttons { + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts new file mode 100644 index 0000000000..488330cf0d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts @@ -0,0 +1,190 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output, SkipSelf } from '@angular/core'; +import { + ControlValueAccessor, FormGroupDirective, + NG_VALUE_ACCESSOR, NgForm, + UntypedFormBuilder, UntypedFormControl, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DocumentationLink } from '@shared/models/user-settings.models'; +import { ErrorStateMatcher } from '@angular/material/core'; + +@Component({ + selector: 'tb-doc-link', + templateUrl: './doc-link.component.html', + styleUrls: ['./doc-link.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DocLinkComponent), + multi: true + }, + {provide: ErrorStateMatcher, useExisting: DocLinkComponent} + ] +}) +export class DocLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { + + @Input() + disabled: boolean; + + @Input() + addOnly = false; + + @Input() + disableEdit = false; + + @Output() + docLinkAdded = new EventEmitter(); + + @Output() + docLinkAddCanceled = new EventEmitter(); + + @Output() + docLinkUpdated = new EventEmitter(); + + @Output() + docLinkDeleted = new EventEmitter(); + + @Output() + editModeChanged = new EventEmitter(); + + editMode = false; + addMode = false; + + docLink: DocumentationLink; + + private propagateChange = null; + + public editDocLinkFormGroup: UntypedFormGroup; + + private submitted = false; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + super(store); + } + + ngOnInit(): void { + this.addMode = this.addOnly; + this.editDocLinkFormGroup = this.fb.group({ + icon: [null, [Validators.required]], + name: [null, [Validators.required]], + link: [null, [Validators.required]] + }); + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.editDocLinkFormGroup.disable({emitEvent: false}); + } else { + this.editDocLinkFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DocumentationLink): void { + this.docLink = value; + this.editDocLinkFormGroup.patchValue( + value, {emitEvent: false} + ); + if (!this.editDocLinkFormGroup.valid) { + this.addMode = true; + this.editModeChanged.emit(true); + } + } + + switchToEditMode() { + if (!this.disableEdit && !this.editMode) { + this.submitted = false; + this.editDocLinkFormGroup.patchValue( + this.docLink, {emitEvent: false} + ); + this.editMode = true; + this.editModeChanged.emit(true); + } + } + + apply() { + this.submitted = true; + this.updateModel(); + if (this.editDocLinkFormGroup.valid) { + this.editMode = false; + this.editModeChanged.emit(false); + this.docLinkUpdated.next(this.editDocLinkFormGroup.value); + } + } + + cancelEdit() { + this.submitted = false; + this.editMode = false; + this.editModeChanged.emit(false); + } + + add() { + this.submitted = true; + this.updateModel(); + if (this.editDocLinkFormGroup.valid) { + if (!this.addOnly) { + this.addMode = false; + this.editModeChanged.emit(false); + } + this.docLinkAdded.next(this.editDocLinkFormGroup.value); + } + } + + cancelAdd() { + this.editModeChanged.emit(false); + this.docLinkAddCanceled.emit(); + } + + delete() { + this.docLinkDeleted.emit(); + } + + isEditing() { + return this.editMode || this.addMode; + } + + private updateModel() { + if (this.editDocLinkFormGroup.valid) { + this.docLink = this.editDocLinkFormGroup.value; + this.propagateChange(this.editDocLinkFormGroup.value); + } else { + this.propagateChange(null); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html new file mode 100644 index 0000000000..96c5642feb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html @@ -0,0 +1,50 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss new file mode 100644 index 0000000000..ddc5a40eec --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss @@ -0,0 +1,159 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-content { + width: 100%; + height: 100%; + } + + .tb-card-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; + } + + .tb-title-link { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + position: relative; + border-bottom: none; + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 16px; + } + &:hover, &:focus { + border-bottom: none; + } + &::after { + content: 'arrow_forward'; + display: inline-block; + transform: rotate(315deg); + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 18px; + color: rgba(0, 0, 0, 0.12); + vertical-align: bottom; + margin-left: 6px; + } + &:hover::after { + color: inherit; + } + } + + .tb-title-icon { + color: rgba(0, 0, 0, 0.38); + width: 32px; + height: 32px; + padding: 4px; + } + + .tb-docs-list { + overflow: auto; + } + + .mat-grid-tile.tb-docs-tile { + overflow: visible; + } + + .tb-doc-button { + height: 55px; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + padding: 8px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + @media #{$mat-md-lg} { + height: 18px; + padding: 0 8px; + } + &:hover { + border: 1px solid rgba(0, 0, 0, 0.12); + box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08); + } + .tb-doc-container { + display: flex; + flex-direction: row; + align-items: center; + .tb-doc-icon-container { + height: 40px; + padding: 8px; + background: #F3F6FA; + border-radius: 6px; + margin-right: 8px; + @media #{$mat-md-lg} { + display: none; + } + } + .tb-doc-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.87); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } + } + } + + .tb-add-doc-button { + height: 55px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #FFFFFF; + padding: 8px; + border: 2px dashed rgba(0, 0, 0, 0.08); + border-radius: 10px; + @media #{$mat-md-lg} { + height: 18px; + padding: 0 8px; + .tb-add-icon { + height: 12px; + width: 12px; + font-size: 12px; + } + } + .tb-add-icon { + color: rgba(0, 0, 0, 0.12); + } + &:hover { + .tb-add-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts new file mode 100644 index 0000000000..1c7c5885b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts @@ -0,0 +1,206 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { map, Subscription } from 'rxjs'; +import { DocumentationLink, DocumentationLinks } from '@shared/models/user-settings.models'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { + ImportDialogCsvComponent, + ImportDialogCsvData +} from '@home/components/import-export/import-dialog-csv.component'; +import { MatDialog } from '@angular/material/dialog'; +import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component'; +import { + EditDocLinksDialogComponent, + EditDocLinksDialogData +} from '@home/components/widget/lib/home-page/edit-doc-links-dialog.component'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; + +const defaultDocLinksMap = new Map( + [ + [Authority.SYS_ADMIN, { + links: [ + { + icon: 'rocket', + name: 'Getting started', + link: 'https://thingsboard.io/docs/getting-started-guides/helloworld/' + }, + { + icon: 'title', + name: 'Tenant profiles', + link: 'https://thingsboard.io/docs/user-guide/tenant-profiles/' + }, + { + icon: 'insert_chart', + name: 'API', + link: 'https://thingsboard.io/docs/api/' + }, + { + icon: 'now_widgets', + name: 'Widgets Library', + link: 'https://thingsboard.io/docs/user-guide/ui/widget-library/' + } + ] + }], + [Authority.TENANT_ADMIN, { + links: [ + { + icon: 'rocket', + name: 'Getting started', + link: 'https://thingsboard.io/docs/getting-started-guides/helloworld/' + }, + { + icon: 'settings_ethernet', + name: 'Rule engine', + link: 'https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/' + }, + { + icon: 'insert_chart', + name: 'API', + link: 'https://thingsboard.io/docs/api/' + }, + { + icon: 'devices', + name: 'Device profiles', + link: 'https://thingsboard.io/docs/user-guide/device-profiles/' + } + ] + }], + [Authority.CUSTOMER_USER, { + links: [] + }] + ] +); + +interface DocLinksWidgetSettings { + columns: number; +} + +@Component({ + selector: 'tb-doc-links-widget', + templateUrl: './doc-links-widget.component.html', + styleUrls: ['./doc-links-widget.component.scss'] +}) +export class DocLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + settings: DocLinksWidgetSettings; + columns: number; + rowHeight = '55px'; + gutterSize = '12px'; + + documentationLinks: DocumentationLinks; + authUser = getCurrentAuthUser(this.store); + + private observeBreakpointSubscription: Subscription; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private userSettingsService: UserSettingsService, + private dialog: MatDialog, + private breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + this.settings = this.ctx.settings; + this.columns = this.settings.columns || 3; + const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + this.rowHeight = isMdLg ? '18px' : '55px'; + this.gutterSize = isMdLg ? '8px' : '12px'; + this.observeBreakpointSubscription = this.breakpointObserver + .observe(MediaBreakpoints['md-lg']) + .subscribe((state: BreakpointState) => { + if (state.matches) { + this.rowHeight = '18px'; + this.gutterSize = '8px'; + } else { + this.rowHeight = '55px'; + this.gutterSize = '12px'; + } + this.cd.markForCheck(); + } + ); + this.loadDocLinks(); + } + + ngOnDestroy() { + if (this.observeBreakpointSubscription) { + this.observeBreakpointSubscription.unsubscribe(); + } + super.ngOnDestroy(); + } + + private loadDocLinks() { + this.userSettingsService.getDocumentationLinks().pipe( + map((documentationLinks) => { + if (!documentationLinks || !documentationLinks.links) { + return defaultDocLinksMap.get(this.authUser.authority); + } else { + return documentationLinks; + } + }) + ).subscribe( + (documentationLinks) => { + this.documentationLinks = documentationLinks; + this.cd.markForCheck(); + } + ); + } + + edit() { + this.dialog.open(EditDocLinksDialogComponent, { + disableClose: true, + autoFocus: false, + data: { + docLinks: this.documentationLinks + }, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (result) => { + if (result) { + this.loadDocLinks(); + } + }); + } + + addLink() { + this.dialog.open(AddDocLinkDialogComponent, { + disableClose: true, + autoFocus: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (docLink) => { + if (docLink) { + this.documentationLinks.links.push(docLink); + this.cd.markForCheck(); + this.userSettingsService.updateDocumentationLinks(this.documentationLinks).subscribe(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.html new file mode 100644 index 0000000000..13cbf35539 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.html @@ -0,0 +1,63 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.scss new file mode 100644 index 0000000000..495c6f4037 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-edit-doc-links-dialog { + width: 480px; + .mat-mdc-dialog-content { + max-height: 50vh; + } + .mat-toolbar-single-row { + padding: 0 24px; + } + h2 { + color: rgba(0, 0, 0, 0.76); + } + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } + .mat-mdc-dialog-content { + padding: 24px 24px 8px 24px; + } + .mdc-dialog__actions { + padding: 0 24px 24px; + } + } + .tb-drag-handle { + height: 24px; + margin-left: 12px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + .tb-add-doc-button { + height: 55px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #FFFFFF; + padding: 8px; + border: 2px dashed rgba(0, 0, 0, 0.08); + border-radius: 10px; + .tb-add-icon { + color: rgba(0, 0, 0, 0.12); + } + &:hover { + .tb-add-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.ts new file mode 100644 index 0000000000..eca41713c4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.ts @@ -0,0 +1,114 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DocumentationLink, DocumentationLinks } from '@shared/models/user-settings.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { UserSettingsService } from '@core/http/user-settings.service'; + +export interface EditDocLinksDialogData { + docLinks: DocumentationLinks; +} + +@Component({ + selector: 'tb-edit-doc-links-dialog', + templateUrl: './edit-doc-links-dialog.component.html', + styleUrls: ['./edit-doc-links-dialog.component.scss'] +}) +export class EditDocLinksDialogComponent extends + DialogComponent implements OnInit { + + updated = false; + addMode = false; + editMode = false; + + docLinks = this.data.docLinks; + addingDocLink: Partial; + + editDocLinksFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EditDocLinksDialogData, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder, + private userSettingsService: UserSettingsService) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + const docLinksControls: Array = []; + for (const docLink of this.docLinks.links) { + docLinksControls.push(this.fb.control(docLink, [Validators.required])); + } + this.editDocLinksFormGroup = this.fb.group({ + links: this.fb.array(docLinksControls) + }); + } + + docLinksFormArray(): UntypedFormArray { + return this.editDocLinksFormGroup.get('links') as UntypedFormArray; + } + + trackByDocLink(index: number, docLinkControl: AbstractControl): any { + return docLinkControl; + } + + docLinkDrop(event: CdkDragDrop) { + const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray; + const docLink = docLinksArray.at(event.previousIndex); + docLinksArray.removeAt(event.previousIndex); + docLinksArray.insert(event.currentIndex, docLink); + this.update(); + } + + addLink() { + this.addingDocLink = { icon: 'notifications' }; + this.addMode = true; + } + + linkAdded(docLink: DocumentationLink) { + this.addMode = false; + const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray; + const docLinkControl = this.fb.control(docLink, [Validators.required]); + docLinksArray.push(docLinkControl); + this.update(); + } + + deleteLink(index: number) { + (this.editDocLinksFormGroup.get('links') as UntypedFormArray).removeAt(index); + this.update(); + } + + update() { + if (this.editDocLinksFormGroup.valid) { + const docLinks: DocumentationLinks = this.editDocLinksFormGroup.value; + this.userSettingsService.updateDocumentationLinks(docLinks).subscribe(() => { + this.updated = true; + }); + } + } + + close(): void { + this.dialogRef.close(this.updated); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.html new file mode 100644 index 0000000000..ac57d77c3a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.html @@ -0,0 +1,34 @@ + +
+
+ + +
+
+
+ done +
+
+
+
widgets.getting-started.done-welcome-title
+
widgets.getting-started.done-welcome-text
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.scss new file mode 100644 index 0000000000..e440fa89a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.scss @@ -0,0 +1,62 @@ +/** + * 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. + */ +.tb-getting-started-completed-dialog { + .mat-mdc-dialog-container .mdc-dialog__surface { + border-radius: 8px; + background-image: url(/assets/home/greetings_bg.svg); + background-repeat: no-repeat; + } + + .tb-getting-started-completed-dialog-container { + width: 500px; + height: 290px; + } + + .tb-done-container { + width: 70px; + height: 70px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.05); + } + + .tb-done-icon { + width: 62px; + height: 62px; + line-height: 56px; + font-size: 36px; + text-align: center; + border-radius: 50%; + border: 2px solid #305680; + background: #FFFFFF; + } + + .tb-done-welcome-title { + font-weight: 600; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.87); + padding-bottom: 8px; + } + + .tb-done-welcome-text { + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.54); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts new file mode 100644 index 0000000000..087b70f3d8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ViewEncapsulation } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormGroup } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-getting-started-completed-dialog', + templateUrl: './getting-started-completed-dialog.component.html', + styleUrls: ['./getting-started-completed-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class GettingStartedCompletedDialogComponent extends + DialogComponent { + + addDocLinkFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + dialogRef.addPanelClass('tb-getting-started-completed-dialog'); + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html new file mode 100644 index 0000000000..b2425a0231 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -0,0 +1,100 @@ + +
widgets.getting-started.get-started
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss new file mode 100644 index 0000000000..193fb5e793 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + display: flex; + flex-direction: column; + height: 100%; + + .tb-title { + font-weight: 600; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.76); + padding-bottom: 24px; + @media #{$mat-md-lg} { + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + padding-bottom: 20px; + } + } + .tb-get-started-container { + flex: 1; + overflow: auto; + } +} + +:host ::ng-deep { + + .tb-get-started .mat-vertical-stepper-header { + border-radius: 12px; + padding: 12px; + height: auto; + } + + .tb-get-started .mat-vertical-stepper-header[aria-selected="true"] { + background: rgba(0, 0, 0, 0.03); + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + .tb-get-started .mat-vertical-content-container { + margin-left: 0; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + background: rgba(0, 0, 0, 0.03); + } + + .tb-get-started .mat-stepper-vertical-line::before { + content: none; + } + + .tb-get-started .mat-step-label { + width: 100%; + white-space: inherit; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.76); + } + + .tb-get-started .mat-step-label.mat-step-label-selected { + font-weight: 500; + font-size: 16px; + line-height: 24px; + color: rgba(0, 0, 0, 0.87); + } + + .tb-get-started .mat-vertical-content p { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + } + + @media #{$mat-md-lg} { + .tb-get-started .mat-step-label { + font-size: 12px; + line-height: 16px; + } + .tb-get-started .mat-step-label.mat-step-label-selected { + font-size: 13px; + line-height: 16px; + } + + .tb-get-started .mdc-button { + font-size: 12px; + line-height: 16px; + } + + .tb-get-started .mat-vertical-content p { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.25px; + } + + .tb-get-started .mat-vertical-content { + padding: 0 16px 16px 16px; + } + + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts new file mode 100644 index 0000000000..9c07e62978 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts @@ -0,0 +1,134 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UserSettingsService } from '@core/http/user-settings.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + GettingStartedCompletedDialogComponent +} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; +import { GettingStarted } from '@shared/models/user-settings.models'; +import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { isUndefined } from '@core/utils'; +import { MatStepper } from '@angular/material/stepper'; +import { first } from 'rxjs/operators'; +import { Authority } from '@shared/models/authority.enum'; + +@Component({ + selector: 'tb-getting-started-widget', + templateUrl: './getting-started-widget.component.html', + styleUrls: ['./getting-started-widget.component.scss'] +}) +export class GettingStartedWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('matStepper') + matStepper: MatStepper; + + @Input() + ctx: WidgetContext; + + authority = Authority; + + authUser = getCurrentAuthUser(this.store); + gettingStarted: GettingStarted = { + lastSelectedIndex: 0, + maxSelectedIndex: 0 + }; + allCompleted = false; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private userSettingsService: UserSettingsService, + private dialog: MatDialog) { + super(store); + } + + ngOnInit() { + this.userSettingsService.getGettingStarted().subscribe( + (gettingStarted) => { + if (gettingStarted) { + this.gettingStarted = gettingStarted; + if (isUndefined(this.gettingStarted.lastSelectedIndex)) { + this.gettingStarted.lastSelectedIndex = 0; + } + if (isUndefined(this.gettingStarted.maxSelectedIndex)) { + this.gettingStarted.maxSelectedIndex = 0; + } + if (this.gettingStarted.lastSelectedIndex > 0 && this.gettingStarted.lastSelectedIndex < this.matStepper.steps.length) { + const animationDuration = this.matStepper.animationDuration; + this.matStepper.animationDuration = '0'; + this.matStepper.selectedIndex = this.gettingStarted.lastSelectedIndex; + this.matStepper.animationDone.pipe(first()).subscribe(() => { + setTimeout(() => {this.matStepper.animationDuration = animationDuration;}, 0); + }); + } + if (this.matStepper.steps.length) { + this.updateCompletedSteps(); + this.cd.markForCheck(); + } else { + this.matStepper.steps.changes.subscribe( + () => { + this.updateCompletedSteps(); + this.cd.markForCheck(); + } + ); + } + } + } + ); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + isSelected(step: CdkStep) { + return this.matStepper?.selected === step; + } + + gettingStartedCompleted() { + this.dialog.open(GettingStartedCompletedDialogComponent, { + disableClose: true, + autoFocus: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe(); + } + + updateSelectedIndex(event: StepperSelectionEvent) { + if (this.gettingStarted.lastSelectedIndex !== event.selectedIndex) { + this.gettingStarted.lastSelectedIndex = event.selectedIndex; + if (event.selectedIndex > this.gettingStarted.maxSelectedIndex) { + this.gettingStarted.maxSelectedIndex = event.selectedIndex; + this.updateCompletedSteps(); + } + this.userSettingsService + .updateGettingStarted(this.gettingStarted, {ignoreLoading: true}).subscribe(); + } + } + + updateCompletedSteps() { + if (this.gettingStarted.maxSelectedIndex >= this.matStepper.steps.length-1) { + this.allCompleted = true; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts new file mode 100644 index 0000000000..3be30dad30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { ClusterInfoTableComponent } from '@home/components/widget/lib/home-page/cluster-info-table.component'; +import { ConfiguredFeaturesComponent } from '@home/components/widget/lib/home-page/configured-features.component'; +import { VersionInfoComponent } from '@home/components/widget/lib/home-page/version-info.component'; +import { DocLinksWidgetComponent } from '@home/components/widget/lib/home-page/doc-links-widget.component'; +import { DocLinkComponent } from '@home/components/widget/lib/home-page/doc-link.component'; +import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component'; +import { EditDocLinksDialogComponent } from '@home/components/widget/lib/home-page/edit-doc-links-dialog.component'; +import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-page/getting-started-widget.component'; +import { + GettingStartedCompletedDialogComponent +} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; + +@NgModule({ + declarations: + [ + ClusterInfoTableComponent, + ConfiguredFeaturesComponent, + VersionInfoComponent, + DocLinksWidgetComponent, + DocLinkComponent, + AddDocLinkDialogComponent, + EditDocLinksDialogComponent, + GettingStartedWidgetComponent, + GettingStartedCompletedDialogComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + ClusterInfoTableComponent, + ConfiguredFeaturesComponent, + VersionInfoComponent, + DocLinksWidgetComponent, + DocLinkComponent, + AddDocLinkDialogComponent, + EditDocLinksDialogComponent, + GettingStartedWidgetComponent, + GettingStartedCompletedDialogComponent + ] +}) +export class HomePageWidgetsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.html new file mode 100644 index 0000000000..fb5dde16d2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.html @@ -0,0 +1,63 @@ + + + +
+
check
+
widgets.version-info.version-is-up-to-date
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss new file mode 100644 index 0000000000..133a0d498e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host { + .tb-card-content { + width: 100%; + height: 100%; + } + + .tb-card-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + @media #{$mat-md-lg} { + margin-bottom: 4px; + } + } + + .tb-title { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 16px; + } + } + + .tb-contact-us { + @media #{$mat-md-lg} { + height: 24px; + font-size: 12px; + } + } + + .tb-version-info-container { + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); + border-radius: 10px; + padding: 8px 16px 4px 16px; + overflow: hidden; + white-space: nowrap; + @media #{$mat-lt-md} { + padding: 16px; + } + @media #{$mat-md-lg} { + padding: 4px; + } + &.up-to-date { + box-shadow: none; + border: none; + background: #F3F6FA; + white-space: initial; + text-align: center; + } + } + + .tb-version-link { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + position: relative; + border-bottom: none; + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + &:hover, &:focus { + border-bottom: none; + } + &::after { + content: 'arrow_forward'; + display: inline-block; + transform: rotate(315deg); + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 18px; + color: rgba(0, 0, 0, 0.12); + vertical-align: bottom; + margin-left: 6px; + } + &:hover::after { + color: inherit; + } + } + + .tb-version { + font-weight: 500; + font-size: 18px; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.87); + @media #{$mat-md-lg} { + font-size: 12px; + line-height: 14px; + } + } + + .tb-upgrade { + @media #{$mat-md-lg} { + height: 24px; + font-size: 12px; + padding: 0; + overflow: hidden; + } + } + + .check-icon { + color: #198038; + font-weight: 600; + @media #{$mat-md-lg} { + font-size: 16px; + } + } + + .up-to-date-text { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts new file mode 100644 index 0000000000..27acef15f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +import { UpdateMessage } from '@shared/models/settings.models'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { of } from 'rxjs'; + +@Component({ + selector: 'tb-version-info', + templateUrl: './version-info.component.html', + styleUrls: ['./version-info.component.scss'] +}) +export class VersionInfoComponent extends PageComponent implements OnInit { + + authUser = getCurrentAuthUser(this.store); + updateMessage: UpdateMessage; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private adminService: AdminService) { + super(store); + } + + ngOnInit() { + (this.authUser.authority === Authority.SYS_ADMIN ? + this.adminService.checkUpdates() : of(null)).subscribe( + (updateMessage) => { + this.updateMessage = updateMessage; + this.cd.markForCheck(); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index 1c3df367c4..16fb6090e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -, private utils: UtilsService, @Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type, + @Inject(WIDGET_COMPONENTS_MODULE_TOKEN) public widgetComponentsModule: Type, private cd: ChangeDetectorRef) { super(store); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.html new file mode 100644 index 0000000000..08d2d8d8e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.html @@ -0,0 +1,23 @@ + +
+ + widgets.documentation.columns + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts new file mode 100644 index 0000000000..7e0205a1e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-doc-links-widget-settings', + templateUrl: './doc-links-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class DocLinksWidgetSettingsComponent extends WidgetSettingsComponent { + + docLinksWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.docLinksWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + columns: 3 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.docLinksWidgetSettingsForm = this.fb.group({ + columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index ef7668f306..73ff1433a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -259,6 +259,9 @@ import { import { TripAnimationPointSettingsComponent } from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component'; +import { + DocLinksWidgetSettingsComponent +} from '@home/components/widget/lib/settings/home-page/doc-links-widget-settings.component'; @NgModule({ declarations: [ @@ -357,7 +360,8 @@ import { TripAnimationPointSettingsComponent, MapWidgetSettingsComponent, RouteMapWidgetSettingsComponent, - TripAnimationWidgetSettingsComponent + TripAnimationWidgetSettingsComponent, + DocLinksWidgetSettingsComponent ], imports: [ CommonModule, @@ -460,7 +464,8 @@ import { TripAnimationPointSettingsComponent, MapWidgetSettingsComponent, RouteMapWidgetSettingsComponent, - TripAnimationWidgetSettingsComponent + TripAnimationWidgetSettingsComponent, + DocLinksWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -527,5 +532,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type { return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetId === widgetId); } - private updateRowsAndSort() { + updateRowsAndSort() { let maxRows = this.dashboard.gridsterOpts.maxRows; this.activeDashboardWidgets.forEach((dashboardWidget) => { const bottom = dashboardWidget.y + dashboardWidget.rows; @@ -300,6 +300,13 @@ export class DashboardWidgets implements Iterable { this.dashboardWidgets.sort((widget1, widget2) => { const row1 = widget1.widgetOrder; const row2 = widget2.widgetOrder; + if (isDefined(row1) && isUndefined(row2)) { + return -1; + } else if (isUndefined(row1) && isDefined(row2)) { + return 1; + } else if (isUndefined(row1) && isUndefined(row2)) { + return 0; + } let res = row1 - row2; if (res === 0) { res = widget1.x - widget2.x; @@ -627,10 +634,12 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { order = this.widgetLayout.mobileOrder; } else if (isDefined(this.widget.config.mobileOrder) && this.widget.config.mobileOrder >= 0) { order = this.widget.config.mobileOrder; - } else if (this.widgetLayout) { - order = this.widgetLayout.row; - } else { - order = this.widget.row; + } else if (!this.dashboard.isMobileSize) { + if (this.widgetLayout) { + order = this.widgetLayout.row; + } else { + order = this.widget.row; + } } return order; } diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index bbc8d76951..033747f6d3 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -33,6 +33,8 @@ import { } from '@home/models/entity/entities-table-config.models'; import { ActivatedRoute } from '@angular/router'; +export type EntitiesTableAction = 'add'; + export interface IEntitiesTableComponent { entitiesTableConfig: EntityTableConfig>; translations: EntityTypeTranslation; diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index 19da00e0f7..be408da234 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -22,15 +22,31 @@ import { Authority } from '@shared/models/authority.enum'; import { Observable } from 'rxjs'; import { HomeDashboard } from '@shared/models/dashboard.models'; import { DashboardService } from '@core/http/dashboard.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { map } from 'rxjs/operators'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import sysAdminHomePageDashboardJson from '!raw-loader!./sys_admin_home_page.raw'; @Injectable() export class HomeDashboardResolver implements Resolve { - constructor(private dashboardService: DashboardService) { + constructor(private dashboardService: DashboardService, + private store: Store) { } resolve(): Observable { - return this.dashboardService.getHomeDashboard(); + return this.dashboardService.getHomeDashboard().pipe( + map((dashboard) => { + if (!dashboard) { + if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { + dashboard = JSON.parse(sysAdminHomePageDashboardJson); + dashboard.hideDashboardToolbar = true; + } + } + return dashboard; + }) + ); } } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw new file mode 100644 index 0000000000..b1dfe94f86 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw @@ -0,0 +1,2724 @@ +{ + "title": "System Administrator Home Page", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'widgets.transport-messages.title' | translate }}\n info\n
\n
\n \n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n" + }, + "title": "Transport messages", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" + }, + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "timeseries_bars_flot", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:widgets.transport-messages.title}", + "color": "#305680", + "settings": {}, + "_hash": 0.2880464219129071, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": [] + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": true, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1680443065451, + "endTimeMs": 1680529465451 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "stack": true, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": 0, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "return value % 1000 === 0 ? ((value / 1000) + 'k') : '';" + }, + "defaultBarWidth": 1800000, + "barAlignment": "left", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "Transport messages", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "widgetStyle": { + "padding": "0" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "actions": {}, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", + "pageSize": 1024, + "noDataDisplayMessage": "", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } + }, + "row": 0, + "col": 0, + "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" + }, + "4b5e47ed-c197-a937-d727-041ba8decec2": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "clusterMode", + "type": "timeseries", + "label": "clusterMode", + "color": "#2196f3", + "settings": {}, + "_hash": 0.7272123990942316, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": false, + "postFuncBody": "" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#ffffff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "let html = '
';\nlet clusterMode = data[0]['clusterMode'];\nif (clusterMode === 'true') {\n html += '';\n} else {\n html += '';\n}\nhtml += '
';\nreturn html;", + "applyDefaultMarkdownStyle": false + }, + "title": "System info", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": { + "border": "none", + "boxShadow": "none", + "padding": "0" + }, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "4b5e47ed-c197-a937-d727-041ba8decec2" + }, + "eace9148-b02a-48fe-1a95-acd9928aa8c5": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "basic_timeseries", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "cpuUsage", + "type": "timeseries", + "label": "{i18n:widgets.system-info.cpu}", + "color": "#305680", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": false, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.9347575372081658, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "{i18n:widgets.system-info.ram}", + "color": "#ac3bc9", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": false, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.31887216598848855, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "{i18n:widgets.system-info.disk}", + "color": "#40d0ae", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": false, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.26499182606431004, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": null + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "timewindowMs": 3600000, + "quickInterval": "CURRENT_DAY", + "interval": 10000 + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": 0, + "max": 100, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 0, + "smoothLines": true, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "System Info Chart", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": false, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "\n.tb-legend-keys td .tb-legend-line {\n height: 5px !important;\n vertical-align: middle;\n}\n", + "pageSize": 1024, + "units": "%", + "noDataDisplayMessage": "", + "showLegend": true, + "legendConfig": { + "direction": "row", + "position": "top", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": false, + "showLatest": false + }, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "eace9148-b02a-48fe-1a95-acd9928aa8c5" + }, + "8acbf5df-f9fc-114d-216f-86f081aa4779": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
\n
widgets.functions.title
\n
\n {{ 'widgets.functions.switch-to-pe' | translate }}\n
\n
\n
\n
widgets.functions.alarms
\n
widgets.functions.dashboards
\n
widgets.functions.entities-and-relations
\n
widgets.functions.profiles
\n
widgets.functions.advanced-features
\n
\n
\n
widgets.functions.notification-center
\n
widgets.functions.api-usage
\n
widgets.functions.customers
\n
widgets.functions.customers-hierarchy
\n
widgets.functions.roles-and-permissions
\n
\n
\n
widgets.functions.groups
\n
widgets.functions.integrations
\n
widgets.functions.solution-templates
\n
widgets.functions.scheduler
\n
widgets.functions.white-labeling
\n
\n
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-header {\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n}\n\n.tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n margin-top: 4px;\n overflow: auto;\n}\n\n.tb-card-column {\n min-width: 0px;\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.feature {\n position: relative;\n cursor: default;\n font-weight: 400;\n font-size: 14px;\n line-height: 28px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.76);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.feature:before {\n content: \"check\";\n font-family: 'Material Icons Round';\n display: inline-block;\n font-size: 20px;\n line-height: 20px;\n color: #198038;\n font-weight: 600;\n position: relative;\n background: #F3F6FA;\n border-radius: 4px;\n width: 20px;\n height: 20px;\n padding: 2px;\n vertical-align: sub;\n margin-right: 8px;\n}\n\n.feature.pe:before {\n content: \"close\";\n background: #F4F4F4;\n color: rgba(0, 0, 0, 0.12);\n}\n\n.feature.pe:after {\n position: relative;\n display: inline-block;\n margin-left: 4px;\n vertical-align: baseline;\n top: -0.5em;\n content: \"PE\";\n color: #305680;\n font-weight: 700;\n font-size: 12px;\n line-height: 12px;\n letter-spacing: 0.0015em;\n}\n\n.feature+.feature {\n margin-top: 4px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-switch-to-pe {\n height: 24px;\n font-size: 12px;\n padding: 0 4px;\n }\n \n .feature+.feature {\n margin-top: 2px;\n }\n \n .feature {\n font-size: 11px;\n line-height: 18px;\n }\n\n .feature:before {\n font-size: 14px;\n line-height: 14px;\n width: 14px;\n height: 14px;\n margin-right: 4px;\n }\n .feature.pe:after {\n margin-left: 2px;\n top: -0.5em;\n content: \"PE\";\n font-size: 8px;\n line-height: 8px;\n }\n \n}\n\n" + }, + "title": "Functions", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "8acbf5df-f9fc-114d-216f-86f081aa4779" + }, + "d2784f6c-0518-fd95-1d28-b21f70bdcb10": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n \n
", + "applyDefaultMarkdownStyle": false + }, + "title": "Configured features", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10" + }, + "66dcf1a2-9d83-6873-c693-d6a5d989b3f8": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n \n
", + "applyDefaultMarkdownStyle": false + }, + "title": "Version", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8" + }, + "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { + "isSystemType": true, + "bundleAlias": "home_page_widgets", + "typeAlias": "documentation_links", + "type": "static", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 3 + }, + "title": "Documentation", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" + }, + "a23185ad-dc46-806c-0e50-5b21fb080ace": { + "isSystemType": true, + "bundleAlias": "home_page_widgets", + "typeAlias": "getting_started", + "type": "static", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 3 + }, + "title": "Getting started", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "a23185ad-dc46-806c-0e50-5b21fb080ace" + }, + "b35fa1b0-7f95-0b23-fade-d066653a40e1": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "cpuUsage", + "type": "timeseries", + "label": "cpuUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117 + }, + { + "name": "cpuCount", + "type": "timeseries", + "label": "cpuCount", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5332753234169949 + }, + { + "name": "cpuUsage", + "type": "timeseries", + "label": "status", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "cpuUsage", + "type": "timeseries", + "label": "statusTooltip", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8542003006820891, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = '{{ \\'widgets.system-info.cpu-critical-text\\' | translate }}';\n} else if (value > 75) {\n text = '{{ \\'widgets.system-info.cpu-warning-text\\' | translate }}';\n}\nreturn text;\n" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
widgets.system-info.cpu
\n
\n
\n
\n ${cpuUsage:0}%\n | ${cpuCount:0} cores\n
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-status {\n width: 18px;\n height: 18px;\n }\n .tb-status:after {\n font-size: 14px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n .tb-total {\n font-size: 11px;\n line-height: 16px;\n }\n}" + }, + "title": "CPU", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "b35fa1b0-7f95-0b23-fade-d066653a40e1" + }, + "5eabde4c-095f-defc-dd09-6c0afb9b9f7b": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "memoryUsage", + "type": "timeseries", + "label": "memoryUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "totalMemory", + "type": "timeseries", + "label": "totalMemory", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5332753234169949, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value / 1073741824;" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "status", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "statusTooltip", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8542003006820891, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = '{{ \\'widgets.system-info.ram-critical-text\\' | translate }}';\n} else if (value > 75) {\n text = '{{ \\'widgets.system-info.ram-warning-text\\' | translate }}';\n}\nreturn text;\n" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "const memoryUsage = data[0]['memoryUsage'];\nconst totalMemory = data[0]['totalMemory'];\nconst usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0);\n\n\nreturn '
' +\n '
' +\n '
widgets.system-info.ram
' +\n '
' +\n '
' +\n '
' +\n '${memoryUsage:0}%' +\n '| ${totalMemory:0} Gb' +\n '
' +\n '
';", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-status {\n width: 18px;\n height: 18px;\n }\n .tb-status:after {\n font-size: 14px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n .tb-total {\n font-size: 11px;\n line-height: 16px;\n }\n}" + }, + "title": "RAM", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b" + }, + "451e2588-80b8-cd08-2199-1263ed4dc12c": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "discUsage", + "type": "timeseries", + "label": "discUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "totalDiscSpace", + "type": "timeseries", + "label": "totalDiscSpace", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5332753234169949, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value / 1073741824;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "status", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "statusTooltip", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8542003006820891, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = '{{ \\'widgets.system-info.disk-critical-text\\' | translate }}';\n} else if (value > 75) {\n text = '{{ \\'widgets.system-info.disk-warning-text\\' | translate }}';\n}\nreturn text;" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "const discUsage = data[0]['discUsage'];\nconst totalDiscSpace = data[0]['totalDiscSpace'];\nconst usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0);\n\n\nreturn '
' +\n '
' +\n '
widgets.system-info.disk
' +\n '
' +\n '
' +\n '
' +\n '${discUsage:0}%' +\n '| ${totalDiscSpace:0} Gb' +\n '
' +\n '
';", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-status {\n width: 18px;\n height: 18px;\n }\n .tb-status:after {\n font-size: 14px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n .tb-total {\n font-size: 11px;\n line-height: 16px;\n }\n}" + }, + "title": "Disk", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "451e2588-80b8-cd08-2199-1263ed4dc12c" + }, + "01b94c17-84eb-4e49-6a92-c3f108ecff8e": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "cpuUsage", + "type": "timeseries", + "label": "cpuUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117 + }, + { + "name": "cpuCount", + "type": "timeseries", + "label": "cpuCount", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5332753234169949 + }, + { + "name": "cpuUsage", + "type": "timeseries", + "label": "cpuStatus", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "cpuUsage", + "type": "timeseries", + "label": "cpuStatusTooltip", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8542003006820891, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = '{{ \\'widgets.system-info.cpu-critical-text\\' | translate }}';\n} else if (value > 75) {\n text = '{{ \\'widgets.system-info.cpu-warning-text\\' | translate }}';\n}\nreturn text;\n" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "memoryUsage", + "color": "#607d8b", + "settings": {}, + "_hash": 0.9761283269385537 + }, + { + "name": "totalMemory", + "type": "timeseries", + "label": "totalMemory", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.6363130990513808, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value / 1073741824;" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "memoryStatus", + "color": "#8bc34a", + "settings": {}, + "_hash": 0.15388812616419556, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "memoryStatusTooltip", + "color": "#3f51b5", + "settings": {}, + "_hash": 0.8086245517967001, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = '{{ \\'widgets.system-info.ram-critical-text\\' | translate }}';\n} else if (value > 75) {\n text = '{{ \\'widgets.system-info.ram-warning-text\\' | translate }}';\n}\nreturn text;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "discUsage", + "color": "#e91e63", + "settings": {}, + "_hash": 0.4278686534236851 + }, + { + "name": "totalDiscSpace", + "type": "timeseries", + "label": "totalDiscSpace", + "color": "#ffeb3b", + "settings": {}, + "_hash": 0.543030429255416, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value / 1073741824;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "discStatus", + "color": "#03a9f4", + "settings": {}, + "_hash": 0.29138586081227835, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "discStatusTooltip", + "color": "#ff9800", + "settings": {}, + "_hash": 0.08995267596665912, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = '{{ \\'widgets.system-info.disk-critical-text\\' | translate }}';\n} else if (value > 75) {\n text = '{{ \\'widgets.system-info.disk-warning-text\\' | translate }}';\n}\nreturn text;" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "const memoryUsage = data[0]['memoryUsage'];\nconst totalMemory = data[0]['totalMemory'];\nconst usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0);\n\nconst discUsage = data[0]['discUsage'];\nconst totalDiscSpace = data[0]['totalDiscSpace'];\nconst usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0);\n\n\nreturn '
' +\n '
' +\n '
' +\n '
widgets.system-info.cpu
' +\n '
' +\n '
' +\n '
' +\n '${cpuUsage:0}%' +\n '| ${cpuCount:0} cores' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
widgets.system-info.ram
' +\n '
' +\n '
' +\n '
' +\n '${memoryUsage:0}%' +\n '| ${totalMemory:0} Gb' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
widgets.system-info.disk
' +\n '
' +\n '
' +\n '
' +\n '${discUsage:0}%' +\n '| ${totalDiscSpace:0} Gb' +\n '
' +\n '
' +\n '
';", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n justify-content: flex-start;\n align-items: stretch;\n}\n\n.tb-card-section {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-section.left {\n padding-right: 12px;\n border-right: solid 1px rgba(0, 0, 0, 0.12);\n}\n\n.tb-card-section.center {\n padding-left: 12px;\n padding-right: 12px;\n}\n\n.tb-card-section.right {\n padding-left: 12px;\n border-left: solid 1px rgba(0, 0, 0, 0.12);\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 400;\n font-size: 12px;\n line-height: 16px;\n color: rgba(0, 0, 0, 0.54);\n}\n" + }, + "title": "System info mobile card", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e" + }, + "f0479b00-ed47-0a30-0c36-bde7847aae00": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0,0,0,0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n \n \n \n \n
", + "applyDefaultMarkdownStyle": false + }, + "title": "System info cards", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": { + "border": "none", + "boxShadow": "none", + "padding": "0" + }, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "f0479b00-ed47-0a30-0c36-bde7847aae00" + }, + "57ed0ad9-8fcf-4454-1fa7-d39e22f84570": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "ae870700-071f-b3bc-406c-16ba554c5a55", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantsCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n \n
\n \n add\n \n
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-small-button-container {\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 28px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}\n" + }, + "title": "Tenants", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570" + }, + "0464b6bc-7863-ba75-4b84-64b8acae2af9": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantProfilesCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "\n", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-small-button-container {\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 28px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}\n" + }, + "title": "Tenant Profiles", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "0464b6bc-7863-ba75-4b84-64b8acae2af9" + }, + "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "devicesCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'device.devices' | translate }}
\n
\n
${devicesCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}" + }, + "title": "Devices", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3" + }, + "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "619cdf00-a042-3b55-124e-194c1b28c236", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "assetsCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'asset.assets' | translate }}
\n
\n
${assetsCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}" + }, + "title": "Assets", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4" + }, + "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "usersCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'user.users' | translate }}
\n
\n
${usersCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}\n" + }, + "title": "Users", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468" + }, + "a3d83738-a4f2-ea92-17d7-1a3c52aa1770": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "customersCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'customer.customers' | translate }}
\n
\n
${customersCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}" + }, + "title": "Customers", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770" + }, + "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "ae870700-071f-b3bc-406c-16ba554c5a55", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantsCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + }, + { + "type": "entityCount", + "name": null, + "entityAliasId": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantProfilesCount", + "color": "#4caf50", + "settings": {}, + "_hash": 0.04291827117688696, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + }, + { + "type": "entityCount", + "name": null, + "entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "devicesCount", + "color": "#f44336", + "settings": {}, + "_hash": 0.45105711028955886, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + }, + { + "type": "entityCount", + "name": null, + "entityAliasId": "619cdf00-a042-3b55-124e-194c1b28c236", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "assetsCount", + "color": "#ffc107", + "settings": {}, + "_hash": 0.17028494648519876, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + }, + { + "type": "entityCount", + "name": null, + "entityAliasId": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "usersCount", + "color": "#607d8b", + "settings": {}, + "_hash": 0.0362458342595664, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + }, + { + "type": "entityCount", + "name": null, + "entityAliasId": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "customersCount", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.7596585662918276, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n \n
\n
\n
${devicesCount:0}
\n
{{ 'device.devices' | translate }}
\n
\n
\n
${assetsCount:0}
\n
{{ 'asset.assets' | translate }}
\n
\n
\n
${usersCount:0}
\n
{{ 'user.users' | translate }}
\n
\n
\n
${customersCount:0}
\n
{{ 'customer.customers' | translate }}
\n
\n
\n
\n", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n justify-content: flex-start;\n align-items: stretch;\n}\n\n.tb-card-section {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-evenly;\n align-items: stretch;\n}\n\n.tb-card-section.left {\n border-right: solid 1px rgba(0, 0, 0, 0.12);\n}\n\n.tb-card-section.right .tb-entity-section {\n padding-left: 40px;\n}\n\n.tb-card-section.right .tb-count {\n min-width: 24px;\n text-align: right;\n}\n\n.tb-button-section {\n display: flex;\n padding-right: 40px;\n flex-direction: row;\n justify-content: flex-end;\n align-items: center;\n}\n\n.tb-entity-section {\n display: flex;\n flex-direction: row;\n justify-content: flex-start;\n align-items: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n margin-right: 12px;\n}\n\n.tb-entity-title {\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n" + }, + "title": "Entities info mobile card", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d" + }, + "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0,0,0,0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n \n \n \n \n
", + "applyDefaultMarkdownStyle": false + }, + "title": "Entities info cards", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": { + "border": "none", + "boxShadow": "none", + "padding": "0" + }, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8" + } + }, + "states": { + "default": { + "name": "System Administrator Home Page", + "root": true, + "layouts": { + "main": { + "widgets": { + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "sizeX": 41, + "sizeY": 16, + "row": 26, + "col": 44, + "mobileOrder": 7, + "mobileHeight": 8 + }, + "4b5e47ed-c197-a937-d727-041ba8decec2": { + "sizeX": 41, + "sizeY": 26, + "row": 0, + "col": 44, + "mobileOrder": 6, + "mobileHeight": 11 + }, + "8acbf5df-f9fc-114d-216f-86f081aa4779": { + "sizeX": 52, + "sizeY": 16, + "row": 42, + "col": 33, + "mobileHide": true + }, + "d2784f6c-0518-fd95-1d28-b21f70bdcb10": { + "sizeX": 44, + "sizeY": 13, + "row": 29, + "col": 0, + "mobileOrder": 8, + "mobileHeight": 8 + }, + "66dcf1a2-9d83-6873-c693-d6a5d989b3f8": { + "sizeX": 33, + "sizeY": 16, + "row": 42, + "col": 0, + "mobileOrder": 9, + "mobileHeight": 7 + }, + "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { + "sizeX": 44, + "sizeY": 14, + "row": 15, + "col": 0, + "mobileHide": true + }, + "a23185ad-dc46-806c-0e50-5b21fb080ace": { + "sizeX": 35, + "sizeY": 58, + "row": 0, + "col": 85, + "mobileHide": true + }, + "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": { + "sizeX": 44, + "sizeY": 15, + "mobileOrder": 0, + "mobileHeight": 7, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 120, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 20, + "outerMargin": true + } + } + } + }, + "transport_messages": { + "name": "Transport messages", + "root": false, + "layouts": { + "main": { + "widgets": { + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#ffffff", + "columns": 24, + "margin": 0, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "outerMargin": true + } + } + } + }, + "monolith_system_info": { + "name": "Monolith System Info", + "root": false, + "layouts": { + "main": { + "widgets": { + "eace9148-b02a-48fe-1a95-acd9928aa8c5": { + "sizeX": 48, + "sizeY": 20, + "row": 8, + "col": 0, + "mobileOrder": 1, + "mobileHeight": 10 + }, + "f0479b00-ed47-0a30-0c36-bde7847aae00": { + "sizeX": 48, + "sizeY": 8, + "mobileOrder": 0, + "mobileHeight": 4, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "rgba(0,0,0,0)", + "columns": 48, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "outerMargin": false + } + } + } + }, + "system_info_desktop": { + "name": "System Info Desktop", + "root": false, + "layouts": { + "main": { + "widgets": { + "b35fa1b0-7f95-0b23-fade-d066653a40e1": { + "sizeX": 8, + "sizeY": 3, + "row": 0, + "col": 0 + }, + "5eabde4c-095f-defc-dd09-6c0afb9b9f7b": { + "sizeX": 8, + "sizeY": 3, + "row": 0, + "col": 8 + }, + "451e2588-80b8-cd08-2199-1263ed4dc12c": { + "sizeX": 8, + "sizeY": 3, + "row": 0, + "col": 16 + } + }, + "gridSettings": { + "backgroundColor": "rgba(0,0,0,0)", + "columns": 24, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "outerMargin": false + } + } + } + }, + "system_info_mobile": { + "name": "System Info Mobile", + "root": false, + "layouts": { + "main": { + "widgets": { + "01b94c17-84eb-4e49-6a92-c3f108ecff8e": { + "sizeX": 24, + "sizeY": 3, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "rgba(0,0,0,0)", + "columns": 24, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "outerMargin": false + } + } + } + }, + "entities_info_desktop": { + "name": "Entities Info Desktop", + "root": false, + "layouts": { + "main": { + "widgets": { + "57ed0ad9-8fcf-4454-1fa7-d39e22f84570": { + "sizeX": 60, + "sizeY": 29, + "row": 0, + "col": 0 + }, + "0464b6bc-7863-ba75-4b84-64b8acae2af9": { + "sizeX": 60, + "sizeY": 29, + "row": 0, + "col": 60 + }, + "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3": { + "sizeX": 30, + "sizeY": 26, + "row": 29, + "col": 0 + }, + "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4": { + "sizeX": 30, + "sizeY": 26, + "row": 29, + "col": 30 + }, + "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468": { + "sizeX": 30, + "sizeY": 26, + "row": 29, + "col": 60 + }, + "a3d83738-a4f2-ea92-17d7-1a3c52aa1770": { + "sizeX": 30, + "sizeY": 26, + "row": 29, + "col": 90 + } + }, + "gridSettings": { + "backgroundColor": "rgba(0,0,0,0)", + "columns": 120, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "entities_info_mobile": { + "name": "Entities Info Mobile", + "root": false, + "layouts": { + "main": { + "widgets": { + "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "rgba(0,0,0,0)", + "columns": 24, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70 + } + } + } + } + }, + "entityAliases": { + "ae870700-071f-b3bc-406c-16ba554c5a55": { + "id": "ae870700-071f-b3bc-406c-16ba554c5a55", + "alias": "Tenants", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "ca4d90e4-f9ae-6fca-6b09-85815a48d52b": { + "id": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "alias": "TenantProfiles", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT_PROFILE" + } + }, + "a1ddb8fa-90ff-5598-e7f2-e254194d055d": { + "id": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "alias": "Devices", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "DEVICE" + } + }, + "619cdf00-a042-3b55-124e-194c1b28c236": { + "id": "619cdf00-a042-3b55-124e-194c1b28c236", + "alias": "Assets", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "ASSET" + } + }, + "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2": { + "id": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "alias": "Users", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "USER" + } + }, + "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31": { + "id": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "alias": "Customers", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "CUSTOMER" + } + }, + "d9229b29-3f46-de8d-7fe8-eb0c43c75079": { + "id": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "alias": "Api Usage State", + "filter": { + "type": "apiUsageState", + "resolveMultiple": true + } + } + }, + "filters": {}, + "timewindow": { + "displayValue": "", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168326072, + "endTimeMs": 1680254726072 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "hideToolbar": true, + "showFilters": true, + "showUpdateDashboardImage": true, + "dashboardCss": ".tb-widget-container > .tb-widget {\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0px 5px 16px rgba(0, 0, 0, 0.04);\n border-radius: 12px;\n}\n\n.tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 16px !important;\n}\n\n.tb-card-title {\n display: grid;\n}\n\n.tb-home-widget-title {\n font-style: normal;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tb-home-widget-link {\n position: relative;\n border-bottom: none;\n}\n\n.tb-home-widget-link:hover {\n border-bottom: none;\n}\n\n.tb-home-widget-link:focus {\n border-bottom: none;\n}\n\n.tb-home-widget-link::after {\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px; \n}\n\n.tb-home-widget-link:hover::after {\n color: inherit;\n}\n\n.tb-home-widget-info-icon {\n color: rgba(0, 0, 0, 0.12);\n font-size: 16px;\n width: 16px;\n height: 16px;\n line-height: 15px;\n vertical-align: middle;\n}\n\n.tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n padding: 0;\n}\n\n.tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n cursor: pointer;\n user-select: none;\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .tb-widget-container > .tb-widget {\n border-radius: 4px;\n }\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 2px !important;\n }\n .tb-hide-md {\n display: none;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 8px !important;\n }\n .tb-hide-lg {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-hide-md-lg {\n display: none;\n }\n\n .tb-home-widget-title {\n font-size: 12px;\n line-height: 16px;\n }\n \n .tb-widget-container > .tb-widget .tb-widget-title {\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 12px;\n line-height: 16px;\n min-height: 24px;\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 {\n width: 24px;\n height: 24px;\n line-height: 24px;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 .mat-icon {\n width: 18px;\n height: 18px;\n font-size: 18px;\n }\n \n .tb-widget-container > .tb-widget tb-legend {\n padding-bottom: 0 !important;\n }\n \n .tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n font-size: 11px;\n line-height: 16px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (max-width: 959px), screen and (min-width: 1820px) {\n .tb-hide-not-md-lg {\n display: none;\n }\n}\n" + } + }, + "externalId": null, + "name": "System Administrator Home Page" +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index 0f2ca8a228..0bf2baebc2 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -130,6 +130,8 @@ border-radius: 4px; background: #fff; padding: 12px 16px; + display: inline-grid; + width: 100%; .subject { padding-bottom: 12px; @@ -142,6 +144,7 @@ font-weight: 400; font-size: 14px; line-height: 20px; + overflow-x: auto; } } } diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 4ed77b66d2..5af7ccce6b 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -23,7 +23,7 @@ import { EventEmitter, Inject, Injector, - Input, + Input, NgZone, OnChanges, Output, Renderer2, @@ -90,6 +90,7 @@ export class TbMarkdownComponent implements OnChanges { constructor(private help: HelpService, private cd: ChangeDetectorRef, + private zone: NgZone, public markdownService: MarkdownService, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type, private dynamicComponentFactoryService: DynamicComponentFactoryService, @@ -97,7 +98,7 @@ export class TbMarkdownComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (isDefinedAndNotNull(this.data)) { - this.render(this.data); + this.zone.run(() => this.render(this.data)); } } diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.scss b/ui-ngx/src/app/shared/components/notification/notification.component.scss index f73b3cc137..c2d50ed106 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.scss +++ b/ui-ngx/src/app/shared/components/notification/notification.component.scss @@ -20,6 +20,7 @@ display: block; border: 1px solid; border-radius: 4px; + margin: 4px; } .icon { @@ -61,6 +62,7 @@ .alarm-severity { padding: 5px; border-radius: 8px; + margin-right: 7px; .severity { font-weight: 500; diff --git a/ui-ngx/src/app/shared/layout/layout.directives.ts b/ui-ngx/src/app/shared/layout/layout.directives.ts new file mode 100644 index 0000000000..3a450cbae6 --- /dev/null +++ b/ui-ngx/src/app/shared/layout/layout.directives.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive } from '@angular/core'; +import { BREAKPOINT, LayoutAlignDirective, LayoutDirective, LayoutGapDirective, ShowHideDirective } from '@angular/flex-layout'; + +const TB_BREAKPOINTS = [ + { + alias: 'md-lg', + mediaQuery: 'screen and (min-width: 960px) and (max-width: 1819px)', + priority: 750 + }, + { + alias: 'gt-md-lg', + mediaQuery: 'screen and (min-width: 1820px)', + priority: -600 + } +]; + +export const TbBreakPointsProvider = { + provide: BREAKPOINT, + useValue: TB_BREAKPOINTS, + multi: true +}; + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayout.md-lg]', inputs: ['fxLayout.md-lg']}) +export class MdLgLayoutDirective extends LayoutDirective { + protected inputs = ['fxLayout.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutAlign.md-lg]', inputs: ['fxLayoutAlign.md-lg']}) +export class MdLgLayoutAlignDirective extends LayoutAlignDirective { + protected inputs = ['fxLayoutAlign.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutGap.md-lg]', inputs: ['fxLayoutGap.md-lg']}) +export class MdLgLayoutGapDirective extends LayoutGapDirective { + protected inputs = ['fxLayoutGap.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxHide.md-lg]', inputs: ['fxHide.md-lg']}) +export class MdLgShowHideDirective extends ShowHideDirective { + protected inputs = ['fxHide.md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayout.gt-md-lg]', inputs: ['fxLayout.gt-md-lg']}) +export class GtMdLgLayoutDirective extends LayoutDirective { + protected inputs = ['fxLayout.gt-md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutAlign.gt-md-lg]', inputs: ['fxLayoutAlign.gt-md-lg']}) +export class GtMdLgLayoutAlignDirective extends LayoutAlignDirective { + protected inputs = ['fxLayoutAlign.gt-md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxLayoutGap.gt-md-lg]', inputs: ['fxLayoutGap.gt-md-lg']}) +export class GtMdLgLayoutGapDirective extends LayoutGapDirective { + protected inputs = ['fxLayoutGap.gt-md-lg']; +} + +// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property,@angular-eslint/directive-selector +@Directive({selector: '[fxHide.gt-md-lg]', inputs: ['fxHide.gt-md-lg']}) +export class GtMdLgShowHideDirective extends ShowHideDirective { + protected inputs = ['fxHide.gt-md-lg']; +} diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index fdcc3407a6..ee64df5288 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -86,22 +86,38 @@ export interface StateEntityFilter { } export interface AssetTypeFilter { + /** + * @deprecated + */ assetType?: string; + assetTypes?: string[]; assetNameFilter?: string; } export interface DeviceTypeFilter { + /** + * @deprecated + */ deviceType?: string; + deviceTypes?: string[]; deviceNameFilter?: string; } export interface EdgeTypeFilter { + /** + * @deprecated + */ edgeType?: string; + edgeTypes?: string[]; edgeNameFilter?: string; } export interface EntityViewFilter { + /** + * @deprecated + */ entityViewType?: string; + entityViewTypes?: string[]; entityViewNameFilter?: string; } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index a93df2e3fd..74eefbcd14 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -67,7 +67,8 @@ export const MediaBreakpoints = { 'gt-sm': 'screen and (min-width: 960px)', 'gt-md': 'screen and (min-width: 1280px)', 'gt-lg': 'screen and (min-width: 1920px)', - 'gt-xl': 'screen and (min-width: 5001px)' + 'gt-xl': 'screen and (min-width: 5001px)', + 'md-lg': 'screen and (min-width: 960px) and (max-width: 1819px)' }; export const helpBaseUrl = 'https://thingsboard.io'; diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 8548bc73d2..f3f2038fea 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -52,6 +52,7 @@ export interface GridSettings { backgroundColor?: string; columns?: number; margin?: number; + outerMargin?: boolean; backgroundSizeMode?: string; backgroundImageUrl?: string; autoFillHeight?: boolean; diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index e55ac494e0..586b246f1e 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -18,6 +18,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { getDescendantProp, isObject } from '@core/utils'; import { SortDirection } from '@angular/material/sort'; +import { EntitiesTableAction } from '@home/models/entity/entity-table-component.models'; export const MAX_SAFE_PAGE_SIZE = 2147483647; @@ -27,6 +28,7 @@ export interface PageQueryParam extends Partial{ textSearch?: string; pageSize?: number; page?: number; + action?: EntitiesTableAction; } export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction { diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index a673e0cacc..0cb02d6257 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -71,8 +71,12 @@ export interface JwtSettings { } export interface UpdateMessage { - message: string; updateAvailable: boolean; + currentVersion: string; + latestVersion: string; + upgradeInstructionsUrl: string; + currentVersionReleaseNotesUrl: string; + latestVersionReleaseNotesUrl: string; } export const phoneNumberPattern = /^\+[1-9]\d{1,14}$/; @@ -437,3 +441,11 @@ export interface AutoVersionCreateConfig extends VersionCreateConfig { } export type AutoCommitSettings = {[entityType: string]: AutoVersionCreateConfig}; + +export interface FeaturesInfo { + emailEnabled: boolean; + smsEnabled: boolean; + notificationEnabled: boolean; + oauthEnabled: boolean; + twoFaEnabled: boolean; +} diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 44af6afc30..17a7031099 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -25,7 +25,7 @@ import { AlarmData, AlarmDataQuery, EntityCountQuery, EntityData, - EntityDataQuery, + EntityDataQuery, EntityFilter, EntityKey, TsValue } from '@shared/models/query/query.models'; @@ -599,6 +599,27 @@ export class TelemetrySubscriber extends WsSubscriber { return subscriber; } + public static createEntityFilterLatestSubscription(telemetryService: TelemetryWebsocketService, + entityFilter: EntityFilter, zone: NgZone, + latestKeys: EntityKey[] = null): TelemetrySubscriber { + const entityDataQuery: EntityDataQuery = { + entityFilter, + pageLink: { + pageSize: 1, + page: 0 + }, + latestValues: latestKeys || [] + }; + const cmd = new EntityDataCmd(); + cmd.query = entityDataQuery; + cmd.latestCmd = { + keys: latestKeys || [] + }; + const subscriber = new TelemetrySubscriber(telemetryService, zone); + subscriber.subscriptionCommands.push(cmd); + return subscriber; + } + constructor(private telemetryService: TelemetryWebsocketService, protected zone?: NgZone) { super(telemetryService, zone); } diff --git a/ui-ngx/src/app/shared/models/user-settings.models.ts b/ui-ngx/src/app/shared/models/user-settings.models.ts index f64b12ea71..5e902a9973 100644 --- a/ui-ngx/src/app/shared/models/user-settings.models.ts +++ b/ui-ngx/src/app/shared/models/user-settings.models.ts @@ -21,3 +21,26 @@ export interface UserSettings { export const initialUserSettings: UserSettings = { openedMenuSections: [] }; + +export enum UserSettingsType { + GENERAL = 'GENERAL', + QUICK_LINKS = 'QUICK_LINKS', + DOC_LINKS = 'DOC_LINKS', + DASHBOARDS = 'DASHBOARDS', + GETTING_STARTED = 'GETTING_STARTED' +} + +export interface DocumentationLink { + icon: string; + name: string; + link: string; +} + +export interface DocumentationLinks { + links?: DocumentationLink[]; +} + +export interface GettingStarted { + maxSelectedIndex?: number; + lastSelectedIndex?: number; +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index c82ae61644..59003e7cf5 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -176,6 +176,17 @@ import { NotificationComponent } from '@shared/components/notification/notificat import { TemplateAutocompleteComponent } from '@shared/components/notification/template-autocomplete.component'; import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { + TbBreakPointsProvider, + MdLgLayoutDirective, + MdLgLayoutAlignDirective, + MdLgLayoutGapDirective, + MdLgShowHideDirective, + GtMdLgLayoutDirective, + GtMdLgLayoutAlignDirective, + GtMdLgLayoutGapDirective, + GtMdLgShowHideDirective +} from '@shared/layout/layout.directives'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -222,7 +233,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) useValue: { hideSingleSelectionIndicator: true } - } + }, + TbBreakPointsProvider ], declarations: [ FooterComponent, @@ -327,7 +339,15 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) NotificationComponent, TemplateAutocompleteComponent, SlackConversationAutocompleteComponent, - DateAgoPipe + DateAgoPipe, + MdLgLayoutDirective, + MdLgLayoutAlignDirective, + MdLgLayoutGapDirective, + MdLgShowHideDirective, + GtMdLgLayoutDirective, + GtMdLgLayoutAlignDirective, + GtMdLgLayoutGapDirective, + GtMdLgShowHideDirective ], imports: [ CommonModule, @@ -541,7 +561,15 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) NotificationComponent, TemplateAutocompleteComponent, SlackConversationAutocompleteComponent, - DateAgoPipe + DateAgoPipe, + MdLgLayoutDirective, + MdLgLayoutAlignDirective, + MdLgLayoutGapDirective, + MdLgShowHideDirective, + GtMdLgLayoutDirective, + GtMdLgLayoutAlignDirective, + GtMdLgLayoutGapDirective, + GtMdLgShowHideDirective ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf b/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf new file mode 100644 index 0000000000..dacf094b51 Binary files /dev/null and b/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf differ diff --git a/ui-ngx/src/assets/fonts/material-icons.css b/ui-ngx/src/assets/fonts/material-icons.css index 3809ea3696..0e200c245e 100644 --- a/ui-ngx/src/assets/fonts/material-icons.css +++ b/ui-ngx/src/assets/fonts/material-icons.css @@ -22,8 +22,24 @@ url(MaterialIcons-Regular.ttf) format('truetype'); } +@font-face { + font-family: 'Material Icons Round'; + font-style: normal; + font-weight: 400; + src: local('Material Icons Round'), + local('MaterialIconsRound-Regular'), + url(MaterialIconsRound-Regular.otf) format('truetype'); +} + .material-icons { font-family: 'Material Icons'; +} + +.material-icons-round { + font-family: 'Material Icons Round'; +} + +.material-icons, .material-icons-round { font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ diff --git a/ui-ngx/src/assets/home/greetings_bg.svg b/ui-ngx/src/assets/home/greetings_bg.svg new file mode 100644 index 0000000000..6bc782e0e4 --- /dev/null +++ b/ui-ngx/src/assets/home/greetings_bg.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json index 58d2cd5387..dc66a0e9f5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json @@ -497,17 +497,17 @@ "filter-type-state-entity-owner": "Propietari de l'entitat des de l'estat del tauler", "filter-type-state-entity-owner-description": "Propietari de l'entitat presa dels paràmetres d'estat del tauler de control", "filter-type-asset-type": "Tipus d'actiu", - "filter-type-asset-type-description": "Actius del tipus '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Actius del tipus '{{assetType}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-asset-type-description": "Actius del tipus '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Actius del tipus '{{assetTypes}}' i el nom del qual comenci per '{{prefix}}'", "filter-type-device-type": "Tipus de dispositiu", - "filter-type-device-type-description": "Dispositius de tipus '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispositius de tipus '{{deviceType}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-device-type-description": "Dispositius de tipus '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Dispositius de tipus '{{deviceTypes}}' i el nom del qual comenci per '{{prefix}}'", "filter-type-entity-view-type": "Tipus de vista d'entitat", - "filter-type-entity-view-type-description": "Vistes d'entitat del tipus '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Vistes d'entitat del tipus '{{entityView}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-entity-view-type-description": "Vistes d'entitat del tipus '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Vistes d'entitat del tipus '{{entityViewTypes}}' i el nom del qual comenci per '{{prefix}}'", "filter-type-edge-type": "Tipus de vora", - "filter-type-edge-type-description": "Vores del tipus '{{edgeType}}'", - "filter-type-edge-type-and-name-description": "Vores del tipus '{{edgeType}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-edge-type-description": "Vores del tipus '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Vores del tipus '{{edgeTypes}}' i el nom del qual comenci per '{{prefix}}'", "filter-type-relations-query": "Consulta de relacions", "filter-type-relations-query-description": "{{entities}} que tenen {{relationType}} relació {{direction}} {{rootEntity}}", "filter-type-edge-search-query": "Consultar cerca de vora", diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 1a845ff17d..41f19bd6f7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -324,17 +324,17 @@ "filter-type-state-entity": "Entita ze stavu dashboardu", "filter-type-state-entity-description": "Entita převzata z parametrů stavu dashboardu", "filter-type-asset-type": "Typ aktiva", - "filter-type-asset-type-description": "Aktiva typu '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Aktiva typu '{{assetType}}' s názvem začínajícím '{{prefix}}'", + "filter-type-asset-type-description": "Aktiva typu '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Aktiva typu '{{assetTypes}}' s názvem začínajícím '{{prefix}}'", "filter-type-device-type": "Typ zařízení", - "filter-type-device-type-description": "Zařízení typu '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Zařízení typu '{{deviceType}}' s názvem začínajícím '{{prefix}}'", + "filter-type-device-type-description": "Zařízení typu '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Zařízení typu '{{deviceTypes}}' s názvem začínajícím '{{prefix}}'", "filter-type-entity-view-type": "Typ entitního pohledu", - "filter-type-entity-view-type-description": "Entitní pohledy typu '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entitní pohledy typu '{{entityView}}' s názvem začínajícím '{{prefix}}'", + "filter-type-entity-view-type-description": "Entitní pohledy typu '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Entitní pohledy typu '{{entityViewTypes}}' s názvem začínajícím '{{prefix}}'", "filter-type-edge-type": "Typ edge", - "filter-type-edge-type-description": "Edge typu '{{edgeType}}'", - "filter-type-edge-type-and-name-description": "Edge typu '{{edgeType}}' s názvem začínajícím '{{prefix}}'", + "filter-type-edge-type-description": "Edge typu '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Edge typu '{{edgeTypes}}' s názvem začínajícím '{{prefix}}'", "filter-type-relations-query": "Dotaz na vztahy", "filter-type-relations-query-description": "{{entities}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Dotaz na vyhledání aktiva", diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index e42342b78a..3f37abf513 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -212,16 +212,16 @@ "filter-type-state-entity": "Entität aus dem Dashboard Status", "filter-type-state-entity-description": "Entität aus den Dashboard Status Parametern", "filter-type-asset-type": "Objekttyp", - "filter-type-asset-type-description": "Objekte vom Typ '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetType}}' und Name beginnend mit '{{prefix}}'", + "filter-type-asset-type-description": "Objekte vom Typ '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetTypes}}' und Name beginnend mit '{{prefix}}'", "filter-type-device-type": "Gerätetyp", - "filter-type-device-type-description": "Geräte vom Typ '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceType}}' und Name beginnend mit '{{prefix}}'", + "filter-type-device-type-description": "Geräte vom Typ '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceTypes}}' und Name beginnend mit '{{prefix}}'", "filter-type-entity-view-type": "Entitätsansichtstyp", - "filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityView}}' und Name beginnend mit '{{prefix}}'", + "filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityViewTypes}}' und Name beginnend mit '{{prefix}}'", "filter-type-edge-type": "Randtyp", - "filter-type-edge-type-description": "Rand vom Typ '{{edgeType}}'", + "filter-type-edge-type-description": "Rand vom Typ '{{edgeTypes}}'", "filter-type-relations-query": "Beziehungsabfrage", "filter-type-relations-query-description": "{{entities}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Objektabfrage", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 53944625ae..2bc94ebf42 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -179,14 +179,14 @@ "filter-type-state-entity": "Οντότητα από την κατάσταση του dashboard", "filter-type-state-entity-description": "Οντότητα που λαμβάνεται από τις παραμέτρους κατάστασης του dashboard", "filter-type-asset-type": "Τύπος Asset", - "filter-type-asset-type-description": "Assets του τύπου '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetType}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-asset-type-description": "Assets του τύπου '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetTypes}}' με όνομα που αρχίζει από '{{prefix}}'", "filter-type-device-type": "Τύπος Συσκευής", - "filter-type-device-type-description": "Συσκευές του τύπου '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceType}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-device-type-description": "Συσκευές του τύπου '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceTypes}}' με όνομα που αρχίζει από '{{prefix}}'", "filter-type-entity-view-type": "Τύπος προβολής Οντοτήτων", - "filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityView}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityViewTypes}}' με όνομα που αρχίζει από '{{prefix}}'", "filter-type-relations-query": "Ερώτημα Σχέσεων", "filter-type-relations-query-description": "{{entities}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Ερώτημα αναζήτησης Asset", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index b3be9d7bf7..93fb935d8f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -534,17 +534,17 @@ "filter-type-state-entity": "Entity from dashboard state", "filter-type-state-entity-description": "Entity taken from dashboard state parameters", "filter-type-asset-type": "Asset type", - "filter-type-asset-type-description": "Assets of type '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'", + "filter-type-asset-type-description": "Assets of type '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Assets of type '{{assetTypes}}' and with name starting with '{{prefix}}'", "filter-type-device-type": "Device type", - "filter-type-device-type-description": "Devices of type '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", + "filter-type-device-type-description": "Devices of type '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Devices of types '{{deviceTypes}}' and with name starting with '{{prefix}}'", "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewType}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewType}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewTypes}}' and with name starting with '{{prefix}}'", "filter-type-edge-type": "Edge type", - "filter-type-edge-type-description": "Edges of type '{{edgeType}}'", - "filter-type-edge-type-and-name-description": "Edges of type '{{edgeType}}' and with name starting with '{{prefix}}'", + "filter-type-edge-type-description": "Edges of type '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Edges of type '{{edgeTypes}}' and with name starting with '{{prefix}}'", "filter-type-relations-query": "Relations query", "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Asset search query", @@ -995,6 +995,7 @@ "vertical-margin-required": "Vertical margin value is required.", "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", + "apply-outer-margin": "Apply margin to the sides of the layout", "autofill-height": "Auto fill layout height", "mobile-layout": "Mobile layout settings", "mobile-row-height": "Mobile row height, px", @@ -2400,7 +2401,7 @@ "ignore-case": "ignore case", "value": "Value", "remove-filter": "Remove filter", - "duplicate-filter": "Duplicate filter", + "duplicate-filter-action": "Duplicate filter", "preview": "Filter preview", "no-filters": "No filters configured", "add-filter": "Add filter", @@ -3483,6 +3484,7 @@ "tenant-profile": "Tenant profile", "tenant-profiles": "Tenant profiles", "add": "Add tenant profile", + "add-profile": "Add profile", "edit": "Edit tenant profile", "tenant-profile-details": "Tenant profile details", "no-tenant-profiles-text": "No tenant profiles found", @@ -3600,6 +3602,7 @@ "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", "too-small-value-zero": "The value must be bigger than 0", "too-small-value-one": "The value must be bigger than 1", + "queue-size-is-limited-by-system-configuration": "The size of the queue is also limited by the system configuration.", "cassandra-tenant-limits-configuration": "Cassandra query for tenant", "ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number", "ws-limit-max-sessions-per-customer": "Sessions per customer maximum number", @@ -5083,6 +5086,115 @@ "font-weight-lighter": "Lighter", "color": "Color", "shadow-color": "Shadow color" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "Disk", + "cpu-warning-text": "Running high on CPU usage. To avoid system failure, optimize system performance.", + "cpu-critical-text": "Critically high CPU usage. To avoid system failure, optimize system performance.", + "ram-warning-text": "Running low on reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", + "ram-critical-text": "Critically low reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", + "disk-warning-text": "Running low on disk space. To avoid data loss, free up or expand the disk space.", + "disk-critical-text": "Critically low disk space. To avoid data loss, free up or expand the disk space." + }, + "cluster-info": { + "service-id": "Service id", + "service-type": "Service type", + "no-data": "No data" + }, + "transport-messages": { + "title": "Transport messages", + "info": "All the messages that came from devices" + }, + "documentation": { + "title": "Documentation", + "add-link": "Add link", + "add-link-title": "Add documentation link", + "name": "Name", + "name-required": "Name is required.", + "link": "Link", + "link-required": "Link is required.", + "columns": "Columns" + }, + "configured-features": { + "title": "Configured features", + "info": "Status of features that require configuration", + "email-feature": "Email", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2FA", + "feature-configured": "Feature is configured.\nClick to setup", + "feature-not-configured": "Feature is not configured.\nClick to setup" + }, + "version-info": { + "title": "Version", + "contact-us": "Contact us", + "current-version": "Current version", + "current": "Current", + "available-version": "Available version", + "available": "Available", + "upgrade": "Upgrade", + "version-is-up-to-date": "Version is up to date" + }, + "functions": { + "title": "Functions", + "pe-feature-tooltip": "Only on ThingsBoard\nProfessional Edition", + "switch-to-pe": "Switch to PE", + "alarms": "Alarms", + "dashboards": "Dashboards", + "entities-and-relations": "Entities & Relations", + "profiles": "Profiles", + "advanced-features": "Advanced features", + "notification-center": "Notification center", + "api-usage": "API usage", + "customers": "Customers", + "customers-hierarchy": "Customers hierarchy", + "roles-and-permissions": "Roles & Permissions", + "groups": "Groups", + "integrations": "Integrations", + "solution-templates": "Solution templates", + "scheduler": "Scheduler", + "white-labeling": "White-labeling" + }, + "getting-started": { + "get-started": "Get started", + "finish": "Finish", + "done-welcome-title": "Welcome on board", + "done-welcome-text": "You did great with it!", + "sys-admin": { + "step1": { + "title": "Create Tenant and Tenant Administrator", + "content": "

A tenant is an individual or organization that owns or produces devices and assets. Tenants can create Tenant administrator users.

Follow the documentation on how to do it:

", + "how-to-create-tenant": "How to create Tenant" + }, + "step2": { + "title": "Configure feature: Mail server", + "content": "

Users will receive activation and password reset emails from the System Administrator via an SMTP server.

Follow the documentation on how to do it:

", + "how-to-configure-mail-server": "How to configure Mail server" + }, + "step3": { + "title": "Configure feature: SMS provider", + "content": "

To distribute SMS messages about alarms, the System Administrator can configure SMS providers.

Follow the documentation on how to do it:

", + "how-to-configure-sms-provider": "How to configure SMS provider" + }, + "step4": { + "title": "Configure feature: 2FA", + "content": "

Adding two-factor authentication to your account increases security and prevents malicious access.

Follow the documentation on how to do it:

", + "how-to-configure-2fa": "How to configure 2FA" + }, + "step5": { + "title": "Configure feature: OAuth 2", + "content": "

You can provide your customers with Single Sign-On functionality and automatically create tenants, customers, or sub-customers by using external user management platforms that support OAuth 2.0.

Follow the documentation on how to do it:

", + "how-to-configure-oauth2": "How to configure OAuth 2" + }, + "step6": { + "title": "Configure feature: Notifications", + "content": "

Some text

Follow the documentation on how to do it:

", + "how-to-configure-notifications": "How to configure Notifications" + } + } } }, "icon": { diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index fbd450337b..8783ab3cb0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -465,17 +465,17 @@ "filter-type-state-entity": "Entidad desde estado de panel", "filter-type-state-entity-description": "Entidad tomada de los parámetros de panel", "filter-type-asset-type": "Tipo de activo", - "filter-type-asset-type-description": "Activos del tipo '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Activos del tipo '{{assetType}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-asset-type-description": "Activos del tipo '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Activos del tipo '{{assetTypes}}' y cuyo nombre comience por '{{prefix}}'", "filter-type-device-type": "Tipo de dispositivo", - "filter-type-device-type-description": "Dispositivos de tipo '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispositivos de tipo '{{deviceType}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-device-type-description": "Dispositivos de tipo '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Dispositivos de tipo '{{deviceTypes}}' y cuyo nombre comience por '{{prefix}}'", "filter-type-entity-view-type": "Tipo de vista de entidad", - "filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityView}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityViewTypes}}' y cuyo nombre comience por '{{prefix}}'", "filter-type-edge-type": "Tipo de borde", - "filter-type-edge-type-description": "Bordes del tipo '{{edgeType}}'", - "filter-type-edge-type-and-name-description": "Vistas de entidad del tipo '{{entityViewType}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-edge-type-description": "Bordes del tipo '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Vistas de entidad del tipo '{{edgeTypes}}' y cuyo nombre comience por '{{prefix}}'", "filter-type-relations-query": "Consulta de relaciones", "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Búsqueda de activos", diff --git a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json index 1f0285e53b..705fc6824c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json @@ -154,14 +154,14 @@ "filter-type-state-entity": "موجودي از وضعيت داشبورد", "filter-type-state-entity-description": "پارامترهاي موجودي گرفته شده از وضعيت داشبورد", "filter-type-asset-type": "نوع دارايي", - "filter-type-asset-type-description": "'{{assetType}}' دارايي هاي نوع", - "filter-type-asset-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{assetType}}' دارايي هاي نوع", + "filter-type-asset-type-description": "'{{assetTypes}}' دارايي هاي نوع", + "filter-type-asset-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{assetTypes}}' دارايي هاي نوع", "filter-type-device-type": "نوع دستگاه", - "filter-type-device-type-description": "'{{deviceType}}' دستگاه هاي نوع", - "filter-type-device-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{deviceType}}' دستگاه هاي نوع", + "filter-type-device-type-description": "'{{deviceTypes}}' دستگاه هاي نوع", + "filter-type-device-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{deviceTypes}}' دستگاه هاي نوع", "filter-type-entity-view-type": "نوع نمايش موجودي", - "filter-type-entity-view-type-description": "'{{entityView}}' نمايش هاي موجودي نوع ", - "filter-type-entity-view-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{entityView}}' نمايش هاي موجودي نوع", + "filter-type-entity-view-type-description": "'{{entityViewTypes}}' نمايش هاي موجودي نوع ", + "filter-type-entity-view-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{entityViewTypes}}' نمايش هاي موجودي نوع", "filter-type-relations-query": "پرس و جو درمورد ارتباطات", "filter-type-relations-query-description": ". دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط {{entities}}", "filter-type-asset-search-query": "پرس و جو درمورد جستجوي دارايي", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index b833f5a555..edaaabdd83 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -324,13 +324,13 @@ "filter-type-asset-search-query": "Requête de recherche d'actifs", "filter-type-asset-search-query-description": "Actifs de types {{assetTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-type": "type d'actif", - "filter-type-asset-type-and-name-description": "Actifs de type '{{assetType}}' et dont le nom commence par '{{prefix}}'", - "filter-type-asset-type-description": "Actifs de type '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Actifs de type '{{assetTypes}}' et dont le nom commence par '{{prefix}}'", + "filter-type-asset-type-description": "Actifs de type '{{assetTypes}}'", "filter-type-device-search-query": "Requête de recherche de dispositif", "filter-type-device-search-query-description": "Dispositifs de types {{deviceTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-device-type": "Type de dispositif", - "filter-type-device-type-and-name-description": "Dispositifs de type '{{deviceType}}' et dont le nom commence par '{{prefix}}'", - "filter-type-device-type-description": "Dispositifs de type '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Dispositifs de type '{{deviceTypes}}' et dont le nom commence par '{{prefix}}'", + "filter-type-device-type-description": "Dispositifs de type '{{deviceTypes}}'", "filter-type-entity-list": "Liste d'entités", "filter-type-entity-name": "Nom d'entité", "filter-type-entity-view-search-query": "Requête de recherche vue d'entité", @@ -338,10 +338,10 @@ "filter-type-edge-search-query": "Requête de recherche de Edge", "filter-type-edge-search-query-description": "Edges avec types {{edgeTypes}} qui ont {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-entity-view-type": "Type de vue d'entité", - "filter-type-entity-view-type-and-name-description": "Vues d'entité de type '{{entityView}}' et dont le nom commence par '{{prefix}}'", - "filter-type-entity-view-type-description": "Vues d'entité de type '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Vues d'entité de type '{{entityViewTypes}}' et dont le nom commence par '{{prefix}}'", + "filter-type-entity-view-type-description": "Vues d'entité de type '{{entityViewTypes}}'", "filter-type-edge-type": "Types de Edge", - "filter-type-edge-type-description": "Edges de type '{{edgeType}}'", + "filter-type-edge-type-description": "Edges de type '{{edgeTypes}}'", "filter-type-relations-query": "Interrogation des relations", "filter-type-relations-query-description": "{{entities}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-required": "Le type de filtre est requis.", @@ -359,7 +359,7 @@ "state-entity-parameter-name": "Nom du paramétre d'entité d'état", "unlimited-level": "Niveau illimité", "filter-type-entity-type": "Type d'entité", - "filter-type-edge-type-and-name-description": "Edges de type '{{edgeType}}' et dont le nom commence par '{{prefix}}'", + "filter-type-edge-type-and-name-description": "Edges de type '{{edgeTypes}}' et dont le nom commence par '{{prefix}}'", "filter-type-apiUsageState": "État d'utilisation de l'API", "last-level-relation": "Récupérer uniquement la relation de dernier niveau" }, diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json index 159bb96998..6d0eae5ee5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-it_IT.json +++ b/ui-ngx/src/assets/locale/locale.constant-it_IT.json @@ -177,13 +177,13 @@ "filter-type-state-entity-description": "Entità prelevata dai parametri di stato della dashboard", "filter-type-asset-type": "Tipo di Asset", "filter-type-asset-type-description": "Asset di tipo '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Asset di tipo '{{assetType}}' e con un nome che inizia per '{{prefix}}'", + "filter-type-asset-type-and-name-description": "Asset di tipo '{{assetTypes}}' e con un nome che inizia per '{{prefix}}'", "filter-type-device-type": "Tipo di dispositivo", - "filter-type-device-type-description": "Dispositivi di tipo '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispositivi di tipo '{{deviceType}}' e con un nome che inizia per '{{prefix}}'", + "filter-type-device-type-description": "Dispositivi di tipo '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Dispositivi di tipo '{{deviceTypes}}' e con un nome che inizia per '{{prefix}}'", "filter-type-entity-view-type": "Tipo vista entità", - "filter-type-entity-view-type-description": "Viste entità di tipo '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Viste entità di tipo '{{entityView}}' e con un nome che inizia per '{{prefix}}'", + "filter-type-entity-view-type-description": "Viste entità di tipo '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Viste entità di tipo '{{entityViewTypes}}' e con un nome che inizia per '{{prefix}}'", "filter-type-relations-query": "Query relazioni", "filter-type-relations-query-description": "{{entities}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Query ricerca asset", diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json index 5d0e259f09..f63a6681a2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json +++ b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json @@ -149,11 +149,11 @@ "filter-type-state-entity": "ダッシュボード状態からのエンティティ", "filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ", "filter-type-asset-type": "資産の種類", - "filter-type-asset-type-description": "'{{assetType}}'", - "filter-type-asset-type-and-name-description": "'{{assetType}}''{{prefix}}'", + "filter-type-asset-type-description": "'{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "'{{assetTypes}}''{{prefix}}'", "filter-type-device-type": "デバイスタイプ", - "filter-type-device-type-description": "'{{deviceType}}'", - "filter-type-device-type-and-name-description": "'{{deviceType}}''{{prefix}}'", + "filter-type-device-type-description": "'{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "'{{deviceTypes}}''{{prefix}}'", "filter-type-relations-query": "関係クエリ", "filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}", "filter-type-asset-search-query": "資産検索クエリ", diff --git a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json index 5973d87268..a6c5a6576d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json @@ -186,14 +186,14 @@ "filter-type-state-entity": "ფილტრის ტიპის სუბიექტი", "filter-type-state-entity-description": "ფილტრის ტიპის სუბიექტის აღწერა", "filter-type-asset-type": "აქტივის ტიპი", - "filter-type-asset-type-description": "აქტივის ტიპის აღწერა '{{assetType}}'", - "filter-type-asset-type-and-name-description": "ტიპის ასეტები '{{assetType}}' და სახელები რომლები იწყება '{{prefix}}'", + "filter-type-asset-type-description": "აქტივის ტიპის აღწერა '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "ტიპის ასეტები '{{assetTypes}}' და სახელები რომლები იწყება '{{prefix}}'", "filter-type-device-type": "მოწყობილობის ტიპი", - "filter-type-device-type-description": "ტიპის მოწყობილობები '{{deviceType}}'", - "filter-type-device-type-and-name-description": "ტიპის მოწყობილობები '{{deviceType}}' რომლების სახელებიც იწყება '{{prefix}}'", + "filter-type-device-type-description": "ტიპის მოწყობილობები '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "ტიპის მოწყობილობები '{{deviceTypes}}' რომლების სახელებიც იწყება '{{prefix}}'", "filter-type-entity-view-type": "ობიექტის მოწყობილობის ტიპი", - "filter-type-entity-view-type-description": "ობიექტის ტიპის ვარიანტები '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "ობიექტის ტიპის განსაზღვრება '{{entityView}}' და დასახელება რომელიც იწყება '{{prefix}}'", + "filter-type-entity-view-type-description": "ობიექტის ტიპის ვარიანტები '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "ობიექტის ტიპის განსაზღვრება '{{entityViewTypes}}' და დასახელება რომელიც იწყება '{{prefix}}'", "filter-type-relations-query": "მოთხოვნა რელაციის ტიპის მიხედვით", "filter-type-relations-query-description": "{{entities}}, არსებული რელაციის ტიპი {{relationType}} {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "ძიება აქტივების მიხედვით", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index a9eb1d2ace..28c02a410d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -302,14 +302,14 @@ "filter-type-state-entity": "대시보드 상태로 부터의 개체", "filter-type-state-entity-description": "개체 taken from dashboard state parameters", "filter-type-asset-type": "자산 유형", - "filter-type-asset-type-description": "'{{assetType}}' 유형의 자산", - "filter-type-asset-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{assetType}}' 유형의 자산", + "filter-type-asset-type-description": "'{{assetTypes}}' 유형의 자산", + "filter-type-asset-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{assetTypes}}' 유형의 자산", "filter-type-device-type": "장치 유형", - "filter-type-device-type-description": "'{{deviceType}}' 유형의 장치", - "filter-type-device-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{assetType}}' 유형의 장치", + "filter-type-device-type-description": "'{{deviceTypes}}' 유형의 장치", + "filter-type-device-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{deviceTypes}}' 유형의 장치", "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityView}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewTypes}}' and with name starting with '{{prefix}}'", "filter-type-relations-query": "관계 질의", "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "자산 검색 질의", diff --git a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json index 97998587da..645fe007d3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json +++ b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json @@ -155,14 +155,14 @@ "filter-type-state-entity": "Vienība no paneļa stāvokļa", "filter-type-state-entity-description": "Vienība ņemta no paneļa stāvokļa parametriem", "filter-type-asset-type": "Aktīvu tips", - "filter-type-asset-type-description": "Aktīvu tipi '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Aktīvu tips '{{assetType}}' un ar vārdu sākot ar '{{prefix}}'", + "filter-type-asset-type-description": "Aktīvu tipi '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Aktīvu tips '{{assetTypes}}' un ar vārdu sākot ar '{{prefix}}'", "filter-type-device-type": "Iekārtas tips", - "filter-type-device-type-description": "Iekārtas tipi '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Iekārtas tipi '{{deviceType}}' un ar vārdu sākot ar '{{prefix}}'", + "filter-type-device-type-description": "Iekārtas tipi '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Iekārtas tipi '{{deviceTypes}}' un ar vārdu sākot ar '{{prefix}}'", "filter-type-entity-view-type": "Vienības skata tips", - "filter-type-entity-view-type-description": "Vienības skats tipam '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Vienības skats tipam '{{entityView}}' un ar vārdu sākot ar '{{prefix}}'", + "filter-type-entity-view-type-description": "Vienības skats tipam '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Vienības skats tipam '{{entityViewTypes}}' un ar vārdu sākot ar '{{prefix}}'", "filter-type-relations-query": "Attiecību vaicājums", "filter-type-relations-query-description": "{{entities}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Aktīvu meklēšanas vaicājums", diff --git a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json index 3498825210..2bba0338d2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json +++ b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json @@ -204,14 +204,14 @@ "filter-type-state-entity": "Entidade do estado do dashboard", "filter-type-state-entity-description": "Entidade obtida dos parâmetros de estado do dashboard", "filter-type-asset-type": "Tipo de ativo", - "filter-type-asset-type-description": "Ativos do tipo '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Ativos do tipo '{{assetType}}' cujo nome começa com '{{prefix}}'", + "filter-type-asset-type-description": "Ativos do tipo '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Ativos do tipo '{{assetTypes}}' cujo nome começa com '{{prefix}}'", "filter-type-device-type": "Tipo de dispositivo", - "filter-type-device-type-description": "Dispositivos do tipo '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispositivos do tipo '{{deviceType}}' cujo nome começa com '{{prefix}}'", + "filter-type-device-type-description": "Dispositivos do tipo '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Dispositivos do tipo '{{deviceTypes}}' cujo nome começa com '{{prefix}}'", "filter-type-entity-view-type": "Tipo de exibições de entidades", - "filter-type-entity-view-type-description": "Exibições de entidades do tipo '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Exibições de entidades do tipo '{{entityView}}' cujo nome começa com '{{prefix}}'", + "filter-type-entity-view-type-description": "Exibições de entidades do tipo '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Exibições de entidades do tipo '{{entityViewTypes}}' cujo nome começa com '{{prefix}}'", "filter-type-relations-query": "Consulta de relações", "filter-type-relations-query-description": "{{entities}} que tem relação de {{relationType}} {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Consulta de pesquisa de ativo", diff --git a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json index 121df2468c..1cccfa551b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json +++ b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json @@ -183,14 +183,14 @@ "filter-type-state-entity": "Entitate din starea panoului", "filter-type-state-entity-description": "Entitate luată din parametrii stării panoului", "filter-type-asset-type": "Tip Proprietate", - "filter-type-asset-type-description": "Proprietăţi de tip: '{{assetType}}'", - "filter-type-asset-type-and-name-description": "proprietăţi de tipul: '{{assetType}}' a căror denumire începe cu: '{{prefix}}'", + "filter-type-asset-type-description": "Proprietăţi de tip: '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "proprietăţi de tipul: '{{assetTypes}}' a căror denumire începe cu: '{{prefix}}'", "filter-type-device-type": "Tip Dispozitiv", - "filter-type-device-type-description": "Dispozitive tip: '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispozitive Tip: '{{deviceType}}' a căror denumire începe cu: '{{prefix}}'", + "filter-type-device-type-description": "Dispozitive tip: '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Dispozitive Tip: '{{deviceTypes}}' a căror denumire începe cu: '{{prefix}}'", "filter-type-entity-view-type": "Tip entitate definită", - "filter-type-entity-view-type-description": "Tip entități definite: '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entități definite de tip: '{{entityView}}' a căror denumire începe cu : '{{prefix}}'", + "filter-type-entity-view-type-description": "Tip entități definite: '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Entități definite de tip: '{{entityViewTypes}}' a căror denumire începe cu : '{{prefix}}'", "filter-type-relations-query": "Relaţii Interogare", "filter-type-relations-query-description": "{{entities}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Proprietate Asset search query", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index ed4afa01a1..91fa7355b5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -209,14 +209,14 @@ "filter-type-state-entity": "Объект из состояния дашборда", "filter-type-state-entity-description": "Объект, полученный из параметров состояния дашборда", "filter-type-asset-type": "Тип актива", - "filter-type-asset-type-description": "Активы типа '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'", + "filter-type-asset-type-description": "Активы типа '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Активы типа '{{assetTypes}}' и названием, начинающимся с '{{prefix}}'", "filter-type-device-type": "Тип устройства", - "filter-type-device-type-description": "Устройства типа '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Устройства типа '{{deviceType}}' и названием, начинающимся с '{{prefix}}'", + "filter-type-device-type-description": "Устройства типа '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Устройства типа '{{deviceTypes}}' и названием, начинающимся с '{{prefix}}'", "filter-type-entity-view-type": "Тип Представления Объекта", - "filter-type-entity-view-type-description": "Представления Объекта типа '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Представления Объекта типа '{{entityView}}' и названием, начинающимся с '{{prefix}}'", + "filter-type-entity-view-type-description": "Представления Объекта типа '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Представления Объекта типа '{{entityViewTypes}}' и названием, начинающимся с '{{prefix}}'", "filter-type-relations-query": "Запрос по типу отношений", "filter-type-relations-query-description": "{{entities}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Поисковый запрос по активам", diff --git a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json index 3c3dcaf7e5..1200732365 100644 --- a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json +++ b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json @@ -302,14 +302,14 @@ "filter-type-state-entity": "Subjekt iz stanja nadzorne plošče", "filter-type-state-entity-description": "Entiteta, vzeta iz parametrov stanja nadzorne plošče", "filter-type-asset-type": "Vrsta sredstva", - "filter-type-asset-type-description": "Sredstva vrste '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Sredstva vrste '{{assetType}}' in z imenom, ki se začne z '{{prefix}}'", + "filter-type-asset-type-description": "Sredstva vrste '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Sredstva vrste '{{assetTypes}}' in z imenom, ki se začne z '{{prefix}}'", "filter-type-device-type": "Vrsta naprave", - "filter-type-device-type-description": "Naprave tipa '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Naprave vrste '{{deviceType}}' in z imenom, ki se začne z '{{prefix}}'", + "filter-type-device-type-description": "Naprave tipa '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Naprave vrste '{{deviceTypes}}' in z imenom, ki se začne z '{{prefix}}'", "filter-type-entity-view-type": "Vrsta pogleda entitete", - "filter-type-entity-view-type-description": "Pogledi entitet vrste '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Pogledi entitet vrste '{{entityView}}' in z imenom, ki se začne z '{{prefix}}'", + "filter-type-entity-view-type-description": "Pogledi entitet vrste '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Pogledi entitet vrste '{{entityViewTypes}}' in z imenom, ki se začne z '{{prefix}}'", "filter-type-relations-query": "Poizvedba odnosov", "filter-type-relations-query-description": "{{entities}}, ki imajo {{relationType}} relacijo {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Iskalna poizvedba sredstva", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index d5a55ad1a7..1aa6900be7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -324,17 +324,17 @@ "filter-type-state-entity": "Gösterge panelinden öğe", "filter-type-state-entity-description": "Gösterge paneli durum parametrelerinden alınan öğeler", "filter-type-asset-type": "Varlık türü", - "filter-type-asset-type-description": "'{{assetType}}' türünde varlıklar", - "filter-type-asset-type-and-name-description": "Adı '{{prefix}}' ile başlayan '{{assetType}}' türünde varlıklar", + "filter-type-asset-type-description": "'{{assetTypes}}' türünde varlıklar", + "filter-type-asset-type-and-name-description": "Adı '{{prefix}}' ile başlayan '{{assetTypes}}' türünde varlıklar", "filter-type-device-type": "Cihaz türü", - "filter-type-device-type-description": "'{{deviceType}}' türünde cihazlar", - "filter-type-device-type-and-name-description": "Adı '{{prefix}}' ile başlayan'{{deviceType}}' türünde cihazlar", + "filter-type-device-type-description": "'{{deviceTypes}}' türünde cihazlar", + "filter-type-device-type-and-name-description": "Adı '{{prefix}}' ile başlayan'{{deviceTypes}}' türünde cihazlar", "filter-type-entity-view-type": "Öğe Görünümü türü", - "filter-type-entity-view-type-description": "'{{entityViewType}}' türünde Öğe Görünümleri", - "filter-type-entity-view-type-and-name-description": "'{{entityViewType}}' türünde ve adı '{{prefix}}' ile başlayan Öğe Görünümleri", + "filter-type-entity-view-type-description": "'{{entityViewTypes}}' türünde Öğe Görünümleri", + "filter-type-entity-view-type-and-name-description": "'{{entityViewTypes}}' türünde ve adı '{{prefix}}' ile başlayan Öğe Görünümleri", "filter-type-edge-type": "Uç tipi", - "filter-type-edge-type-description": "'{{edgeType}}' türünün uçları", - "filter-type-edge-type-and-name-description": "'{{edgeType}}' türü ve adı '{{prefix}}' ile başlayan kenarlar", + "filter-type-edge-type-description": "'{{edgeTypes}}' türünün uçları", + "filter-type-edge-type-and-name-description": "'{{edgeTypes}}' türü ve adı '{{prefix}}' ile başlayan kenarlar", "filter-type-relations-query": "İlişkiler sorgusu", "filter-type-relations-query-description": "{{relationType}} türünde ilişkili olan öğeler: {{entities}}. {{direction}}: {{rootEntity}}", "filter-type-asset-search-query": "Varlık arama sorgusu", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 99a09485ce..aa71c84048 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -228,14 +228,14 @@ "filter-type-state-entity": "Сутність з стану панелі пристроїв", "filter-type-state-entity-description": "Сутність, взята з параметрів стану панелі пристроїв", "filter-type-asset-type": "Тип активу", - "filter-type-asset-type-description": "Тип активів '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Тип активів '{{assetType}}' і ім'я, що починаються з '{{prefix}}'", + "filter-type-asset-type-description": "Тип активів '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Тип активів '{{assetTypes}}' і ім'я, що починаються з '{{prefix}}'", "filter-type-device-type": "Тип пристрою", - "filter-type-device-type-description": "Тип пристроїв '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Тип пристроїв '{{deviceType}}' і ім'я, що починаються з '{{prefix}}'", + "filter-type-device-type-description": "Тип пристроїв '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Тип пристроїв '{{deviceTypes}}' і ім'я, що починаються з '{{prefix}}'", "filter-type-entity-view-type": "Тип перегляду сутності", - "filter-type-entity-view-type-description": "Перегляд сутності з типом '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Перегляд сутності з типом'{{entityView}}' і іменем, що починаються з '{{prefix}}'", + "filter-type-entity-view-type-description": "Перегляд сутності з типом '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Перегляд сутності з типом'{{entityViewTypes}}' і іменем, що починаються з '{{prefix}}'", "filter-type-relations-query": "Запит відносин", "filter-type-relations-query-description": "{{entities}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Запит пошуку активу", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index edec8bf758..a37f96875a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -466,17 +466,17 @@ "filter-type-state-entity": "仪表板状态实体", "filter-type-state-entity-description": "仪表板实体令牌状态参数", "filter-type-asset-type": "资产类型", - "filter-type-asset-type-description": "类型为 '{{assetType}}' 的资产", - "filter-type-asset-type-and-name-description": "类型为 '{{assetType}}' 且以 '{{prefix}}' 开头的资产", + "filter-type-asset-type-description": "类型为 '{{assetTypes}}' 的资产", + "filter-type-asset-type-and-name-description": "类型为 '{{assetTypes}}' 且以 '{{prefix}}' 开头的资产", "filter-type-device-type": "设备类型", - "filter-type-device-type-description": "类型为 '{{deviceType}}' 的设备", - "filter-type-device-type-and-name-description": "类型为 '{{deviceType}}' 且以 '{{prefix}}' 开头的设备", + "filter-type-device-type-description": "类型为 '{{deviceTypes}}' 的设备", + "filter-type-device-type-and-name-description": "类型为 '{{deviceTypes}}' 且以 '{{prefix}}' 开头的设备", "filter-type-entity-view-type": "实体视图类型", - "filter-type-entity-view-type-description": "类型为 '{{entityView}}' 的实体视图", - "filter-type-entity-view-type-and-name-description": "类型为 {{entityView}}' 且以 '{{prefix}}' 开头的实体视图", + "filter-type-entity-view-type-description": "类型为 '{{entityViewTypes}}' 的实体视图", + "filter-type-entity-view-type-and-name-description": "类型为 {{entityViewTypes}}' 且以 '{{prefix}}' 开头的实体视图", "filter-type-edge-type": "边缘类型", - "filter-type-edge-type-description": "类型为 '{{edgeType}}' 的边缘", - "filter-type-edge-type-and-name-description": "类型为 '{{edgeType}}' 且以 '{{prefix}}' 开头的边缘", + "filter-type-edge-type-description": "类型为 '{{edgeTypes}}' 的边缘", + "filter-type-edge-type-and-name-description": "类型为 '{{edgeTypes}}' 且以 '{{prefix}}' 开头的边缘", "filter-type-relations-query": "关联查询", "filter-type-relations-query-description": "具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的 {{entities}} ", "filter-type-asset-search-query": "资产搜索查询", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index 4e876fd271..97ac15a4bc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -395,17 +395,17 @@ "filter-type-state-entity": "實體(儀表板狀態)", "filter-type-state-entity-description": "實體taken(儀表板狀態參數)", "filter-type-asset-type": "資產類型", - "filter-type-asset-type-description": "類型為 '{{assetType}}' 的資產", - "filter-type-asset-type-and-name-description": "類型為 '{{assetType}}' 且以 '{{prefix}}' 開頭的資產", + "filter-type-asset-type-description": "類型為 '{{assetTypes}}' 的資產", + "filter-type-asset-type-and-name-description": "類型為 '{{assetTypes}}' 且以 '{{prefix}}' 開頭的資產", "filter-type-device-type": "設備類型", - "filter-type-device-type-description": "類型為 '{{deviceType}}' 的設備", - "filter-type-device-type-and-name-description": "類型為 '{{deviceType}}' 且以 '{{prefix}}' 開頭的設備", + "filter-type-device-type-description": "類型為 '{{deviceTypes}}' 的設備", + "filter-type-device-type-and-name-description": "類型為 '{{deviceTypes}}' 且以 '{{prefix}}' 開頭的設備", "filter-type-entity-view-type": "實體視圖類型", - "filter-type-entity-view-type-description": "類型為 '{{entityView}}' 的實體視圖", - "filter-type-entity-view-type-and-name-description": "類型為 {{entityView}}' 且以 '{{prefix}}' 開頭的實體視圖", + "filter-type-entity-view-type-description": "類型為 '{{entityViewTypes}}' 的實體視圖", + "filter-type-entity-view-type-and-name-description": "類型為 {{entityViewTypes}}' 且以 '{{prefix}}' 開頭的實體視圖", "filter-type-edge-type": "邊緣類型", - "filter-type-edge-type-description": " '類型為{{edgeType}}'的邊緣", - "filter-type-edge-type-and-name-description": "類型為 '{{edgeType}}' 且名稱以 '{{prefix}}'開頭的邊緣", + "filter-type-edge-type-description": " '類型為{{edgeTypes}}'的邊緣", + "filter-type-edge-type-and-name-description": "類型為 '{{edgeTypes}}' 且名稱以 '{{prefix}}'開頭的邊緣", "filter-type-relations-query": "關聯查詢", "filter-type-relations-query-description": "具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的 {{entities}} ", "filter-type-asset-search-query": "資產搜尋查詢", diff --git a/ui-ngx/src/index.html b/ui-ngx/src/index.html index 415dae7fa9..5b0ca17cd6 100644 --- a/ui-ngx/src/index.html +++ b/ui-ngx/src/index.html @@ -25,6 +25,7 @@ +