Browse Source

Merge with UI changes

pull/3477/head
Andrii Shvaika 6 years ago
parent
commit
df823c8f90
  1. 21
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  2. 6
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  3. 2
      application/src/main/resources/logback.xml
  4. 6
      application/src/main/resources/thingsboard.yml
  5. 19
      common/dao-api/pom.xml
  6. 19
      common/data/pom.xml
  7. 8
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileData.java
  8. 19
      common/message/pom.xml
  9. 29
      common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java
  10. 52
      common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java
  11. 9
      common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java
  12. 9
      common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java
  13. 1
      common/queue/src/main/proto/queue.proto
  14. 2
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
  15. 35
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  16. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  17. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  18. 1
      dao/src/main/java/org/thingsboard/server/dao/sqlts/hsql/JpaHsqlTimeseriesDao.java
  19. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/hsql/HsqlLatestInsertTsRepository.java
  20. 19
      dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java
  21. 9
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  22. 1
      dao/src/test/resources/sql/hsql/drop-all-tables.sql
  23. 4621
      msa/js-executor/package-lock.json
  24. 28
      msa/js-executor/package.json
  25. 24
      msa/js-executor/pom.xml
  26. 2902
      msa/js-executor/yarn.lock
  27. 3554
      msa/web-ui/package-lock.json
  28. 26
      msa/web-ui/package.json
  29. 194
      msa/web-ui/pom.xml
  30. 2155
      msa/web-ui/yarn.lock
  31. 4
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
  32. 21
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java
  33. 23
      rest-client/pom.xml
  34. 24
      rule-engine/rule-engine-api/pom.xml
  35. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
  36. 66
      ui-ngx/src/app/core/http/device-profile.service.ts
  37. 13
      ui-ngx/src/app/core/services/menu.service.ts
  38. 84
      ui-ngx/src/app/modules/common/modules-map.ts
  39. 22
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  40. 40
      ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html
  41. 92
      ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts
  42. 77
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  43. 128
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
  44. 24
      ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html
  45. 97
      ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts
  46. 27
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html
  47. 103
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts
  48. 29
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html
  49. 79
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts
  50. 24
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html
  51. 93
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts
  52. 53
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html
  53. 101
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts
  54. 4
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html
  55. 8
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts
  56. 2
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  57. 4
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts
  59. 32
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  60. 2
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts
  61. 2
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts
  62. 136
      ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts
  63. 8
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts
  64. 52
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  65. 4
      ui-ngx/src/app/modules/home/models/services.map.ts
  66. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  67. 9
      ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts
  68. 56
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts
  69. 43
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  70. 38
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  71. 35
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts
  72. 125
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  73. 2
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  74. 3
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html
  75. 3
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  76. 6
      ui-ngx/src/app/shared/models/ace/service-completion.models.ts
  77. 29
      ui-ngx/src/app/shared/models/device.models.ts
  78. 10
      ui-ngx/src/app/shared/models/tenant.model.ts
  79. 34
      ui-ngx/src/assets/locale/locale.constant-en_US.json

21
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
@ -79,8 +80,6 @@ import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
/**
* @author Andrew Shvayka
@ -279,17 +278,17 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture;
ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture;
if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
clientAttributesFuture = findAllAttributesByScope(CLIENT_SCOPE);
sharedAttributesFuture = findAllAttributesByScope(SHARED_SCOPE);
clientAttributesFuture = findAllAttributesByScope(DataConstants.CLIENT_SCOPE);
sharedAttributesFuture = findAllAttributesByScope(DataConstants.SHARED_SCOPE);
} else if (!CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), DataConstants.CLIENT_SCOPE);
sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), DataConstants.SHARED_SCOPE);
} else if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
clientAttributesFuture = Futures.immediateFuture(Collections.emptyList());
sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), DataConstants.SHARED_SCOPE);
} else {
sharedAttributesFuture = Futures.immediateFuture(Collections.emptyList());
clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), DataConstants.CLIENT_SCOPE);
}
return Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture));
}
@ -316,7 +315,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder();
if (msg.isDeleted()) {
List<String> sharedKeys = msg.getDeletedKeys().stream()
.filter(key -> SHARED_SCOPE.equals(key.getScope()))
.filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
.map(AttributeKey::getAttributeKey)
.collect(Collectors.toList());
if (!sharedKeys.isEmpty()) {
@ -324,7 +323,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
hasNotificationData = true;
}
} else {
if (SHARED_SCOPE.equals(msg.getScope())) {
if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
if (attributes.size() > 0) {
List<TsKvProto> sharedUpdated = msg.getValues().stream().map(this::toTsKvProto)
@ -334,7 +333,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
hasNotificationData = true;
}
} else {
log.debug("[{}] No public server side attributes changed!", deviceId);
log.debug("[{}] No public shared side attributes changed!", deviceId);
}
}
}

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

@ -197,19 +197,21 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
@ResponseBody
public DeferredResult<ResponseEntity> getTimeseries(
@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
@PathVariable("entityType") String entityType,
@PathVariable("entityId") String entityIdStr,
@RequestParam(name = "keys") String keys,
@RequestParam(name = "startTs") Long startTs,
@RequestParam(name = "endTs") Long endTs,
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@RequestParam(name= "orderBy", defaultValue = "DESC") String orderBy,
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> {
// If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
List<ReadTsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg))
List<ReadTsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy))
.collect(Collectors.toList());
Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());

2
application/src/main/resources/logback.xml

@ -29,6 +29,8 @@
<!-- <logger name="org.thingsboard.server.service.queue" level="TRACE" />-->
<!-- <logger name="org.thingsboard.server.service.transport" level="TRACE" />-->
<!-- <logger name="org.thingsboard.server.queue.memory.InMemoryStorage" level="DEBUG" />-->
<!-- <logger name="org.thingsboard.server.service.subscription" level="TRACE"/>-->
<!-- <logger name="org.thingsboard.server.service.telemetry" level="TRACE"/>-->

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

@ -522,7 +522,7 @@ js:
# Specify thread pool size for JavaScript sandbox resource monitor
monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
# Maximum CPU time in milliseconds allowed for script execution
max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:10000}"
max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:8000}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}"
# JS Eval max request timeout. 0 - no timeout
@ -615,6 +615,10 @@ swagger:
queue:
type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
in_memory:
stats:
# For debug lvl
print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}"
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
acks: "${TB_KAFKA_ACKS:all}"

19
common/dao-api/pom.xml

@ -106,6 +106,25 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>

19
common/data/pom.xml

@ -75,6 +75,25 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>

8
common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileData.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.util.HashMap;
@ -25,15 +26,16 @@ import java.util.Map;
@Data
public class TenantProfileData {
private Map<String, String> properties = new HashMap<>();
@JsonIgnore
private Map<String, Object> properties = new HashMap<>();
@JsonAnyGetter
public Map<String, String> properties() {
public Map<String, Object> properties() {
return this.properties;
}
@JsonAnySetter
public void put(String name, String value) {
public void put(String name, Object value) {
this.properties.put(name, value);
}

19
common/message/pom.xml

@ -87,6 +87,25 @@
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>

29
common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java

@ -1,29 +0,0 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.msg.kv;
import java.io.Serializable;
import java.util.List;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
public interface AttributesKVMsg extends Serializable {
List<AttributeKvEntry> getClientAttributes();
List<AttributeKvEntry> getSharedAttributes();
List<AttributeKey> getDeletedAttributes();
}

52
common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java

@ -1,52 +0,0 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.msg.kv;
import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.Collections;
import java.util.List;
@Data
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class BasicAttributeKVMsg implements AttributesKVMsg {
private static final long serialVersionUID = 1L;
private final List<AttributeKvEntry> clientAttributes;
private final List<AttributeKvEntry> sharedAttributes;
private final List<AttributeKey> deletedAttributes;
public static BasicAttributeKVMsg fromClient(List<AttributeKvEntry> attributes) {
return new BasicAttributeKVMsg(attributes, Collections.emptyList(), Collections.emptyList());
}
public static BasicAttributeKVMsg fromShared(List<AttributeKvEntry> attributes) {
return new BasicAttributeKVMsg(Collections.emptyList(), attributes, Collections.emptyList());
}
public static BasicAttributeKVMsg from(List<AttributeKvEntry> client, List<AttributeKvEntry> shared) {
return new BasicAttributeKVMsg(client, shared, Collections.emptyList());
}
public static AttributesKVMsg fromDeleted(List<AttributeKey> shared) {
return new BasicAttributeKVMsg(Collections.emptyList(), Collections.emptyList(), shared);
}
}

9
common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java

@ -24,7 +24,6 @@ import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@Slf4j
public final class InMemoryStorage {
@ -35,6 +34,14 @@ public final class InMemoryStorage {
storage = new ConcurrentHashMap<>();
}
public void printStats() {
storage.forEach((topic, queue) -> {
if (queue.size() > 0) {
log.debug("[{}] Queue Size [{}]", topic, queue.size());
}
});
}
public static InMemoryStorage getInstance() {
if (instance == null) {
synchronized (InMemoryStorage.class) {

9
common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java

@ -17,6 +17,7 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.gen.js.JsInvokeProtos;
@ -28,6 +29,7 @@ import org.thingsboard.server.queue.common.TbProtoJsQueueMsg;
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.memory.InMemoryStorage;
import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer;
import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer;
import org.thingsboard.server.queue.settings.TbQueueCoreSettings;
@ -47,6 +49,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE
private final TbQueueRuleEngineSettings ruleEngineSettings;
private final TbQueueTransportApiSettings transportApiSettings;
private final TbQueueTransportNotificationSettings transportNotificationSettings;
private final InMemoryStorage storage;
public InMemoryMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings,
TbQueueRuleEngineSettings ruleEngineSettings,
@ -59,6 +62,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE
this.ruleEngineSettings = ruleEngineSettings;
this.transportApiSettings = transportApiSettings;
this.transportNotificationSettings = transportNotificationSettings;
this.storage = InMemoryStorage.getInstance();
}
@Override
@ -120,4 +124,9 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE
public TbQueueRequestTemplate<TbProtoJsQueueMsg<JsInvokeProtos.RemoteJsRequest>, TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> createRemoteJsRequestTemplate() {
return null;
}
@Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}")
private void printInMemoryStats() {
storage.printStats();
}
}

1
common/queue/src/main/proto/queue.proto

@ -131,7 +131,6 @@ message GetAttributeResponseMsg {
int32 requestId = 1;
repeated TsKvProto clientAttributeList = 2;
repeated TsKvProto sharedAttributeList = 3;
repeated string deletedAttributeKeys = 4;
string error = 5;
}

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

@ -125,7 +125,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
@Override
public Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException {
if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0 && msg.getDeletedAttributeKeysCount() == 0) {
if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) {
return new Response(CoAP.ResponseCode.NOT_FOUND);
} else {
Response response = new Response(CoAP.ResponseCode.CONTENT);

35
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -35,7 +35,6 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
@ -269,11 +268,6 @@ public class JsonConverter {
payload.getSharedAttributeListList().forEach(addToObjectFromProto(attrObject));
result.add("shared", attrObject);
}
if (payload.getDeletedAttributeKeysCount() > 0) {
JsonArray attrObject = new JsonArray();
payload.getDeletedAttributeKeysList().forEach(attrObject::add);
result.add("deleted", attrObject);
}
return result;
}
@ -290,31 +284,6 @@ public class JsonConverter {
return result;
}
public static JsonObject toJson(AttributesKVMsg payload, boolean asMap) {
JsonObject result = new JsonObject();
if (asMap) {
if (!payload.getClientAttributes().isEmpty()) {
JsonObject attrObject = new JsonObject();
payload.getClientAttributes().forEach(addToObject(attrObject));
result.add("client", attrObject);
}
if (!payload.getSharedAttributes().isEmpty()) {
JsonObject attrObject = new JsonObject();
payload.getSharedAttributes().forEach(addToObject(attrObject));
result.add("shared", attrObject);
}
} else {
payload.getClientAttributes().forEach(addToObject(result));
payload.getSharedAttributes().forEach(addToObject(result));
}
if (!payload.getDeletedAttributes().isEmpty()) {
JsonArray attrObject = new JsonArray();
payload.getDeletedAttributes().forEach(addToObject(attrObject));
result.add("deleted", attrObject);
}
return result;
}
public static JsonObject getJsonObjectForGateway(String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) {
JsonObject result = new JsonObject();
result.addProperty("id", responseMsg.getRequestId());
@ -370,10 +339,6 @@ public class JsonConverter {
}
}
private static Consumer<AttributeKey> addToObject(JsonArray result) {
return key -> result.add(key.getAttributeKey());
}
private static Consumer<TsKvProto> addToObjectFromProto(JsonObject result) {
return de -> {
switch (de.getKv().getType()) {

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

@ -25,6 +25,7 @@ import org.thingsboard.server.dao.model.sql.AlarmEntity;
import org.thingsboard.server.dao.model.sql.AlarmInfoEntity;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
@ -70,7 +71,7 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
@Param("affectedEntityType") String affectedEntityType,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmStatuses") List<AlarmStatus> alarmStatuses,
@Param("alarmStatuses") Set<AlarmStatus> alarmStatuses,
@Param("searchText") String searchText,
Pageable pageable);

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

@ -24,14 +24,12 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
@ -40,7 +38,6 @@ import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.sql.query.AlarmQueryRepository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -102,7 +99,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
Set<AlarmStatus> statusSet = null;
if (query.getSearchStatus() != null) {
statusSet = query.getSearchStatus().getStatuses();
} else if (query.getStatus() != null){
} else if (query.getStatus() != null) {
statusSet = Collections.singleton(query.getStatus());
}
return DaoUtil.toPageData(
@ -112,7 +109,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
affectedEntity.getEntityType().name(),
query.getPageLink().getStartTime(),
query.getPageLink().getEndTime(),
new ArrayList<>(statusSet),
statusSet,
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)

1
dao/src/main/java/org/thingsboard/server/dao/sqlts/hsql/JpaHsqlTimeseriesDao.java

@ -46,6 +46,7 @@ public class JpaHsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDa
entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
entity.setJsonValue(tsKvEntry.getJsonValue().orElse(null));
log.trace("Saving entity: {}", entity);
return tsQueue.add(entity);
}

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/hsql/HsqlLatestInsertTsRepository.java

@ -41,8 +41,8 @@ public class HsqlLatestInsertTsRepository extends AbstractInsertRepository imple
"ON (ts_kv_latest.entity_id=T.entity_id " +
"AND ts_kv_latest.key=T.key) " +
"WHEN MATCHED THEN UPDATE SET ts_kv_latest.ts = T.ts, ts_kv_latest.bool_v = T.bool_v, ts_kv_latest.str_v = T.str_v, ts_kv_latest.long_v = T.long_v, ts_kv_latest.dbl_v = T.dbl_v, ts_kv_latest.json_v = T.json_v " +
"WHEN NOT MATCHED THEN INSERT (entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " +
"VALUES (T.entity_id, T.key, T.ts, T.bool_v, T.str_v, T.long_v, T.dbl_v);";
"WHEN NOT MATCHED THEN INSERT (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) " +
"VALUES (T.entity_id, T.key, T.ts, T.bool_v, T.str_v, T.long_v, T.dbl_v, T.json_v);";
@Override
public void saveOrUpdate(List<TsKvLatestEntity> entities) {

19
dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java

@ -31,6 +31,7 @@ import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Slf4j
public abstract class AbstractCassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao {
@ -85,4 +86,22 @@ public abstract class AbstractCassandraBaseTimeseriesDao extends CassandraAbstra
return new BasicTsKvEntry(ts, toKvEntry(row, key));
}
protected TsKvEntry convertResultToTsKvEntry(String key, Row row) {
if (row != null) {
Optional<String> foundKeyOpt = getKey(row);
long ts = row.getLong(ModelConstants.TS_COLUMN);
return new BasicTsKvEntry(ts, toKvEntry(row, foundKeyOpt.orElse(key)));
} else {
return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
}
private Optional<String> getKey(Row row){
try{
return Optional.ofNullable(row.getString(ModelConstants.KEY_COLUMN));
} catch (IllegalArgumentException e){
return Optional.empty();
}
}
}

9
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -186,15 +186,6 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
rows -> this.convertResultToTsKvEntryList(rows), readResultsProcessingExecutor);
}
private TsKvEntry convertResultToTsKvEntry(String key, Row row) {
if (row != null) {
long ts = row.getLong(ModelConstants.TS_COLUMN);
return new BasicTsKvEntry(ts, toKvEntry(row, key));
} else {
return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
}
private PreparedStatement getLatestStmt() {
if (latestInsertStmt == null) {
latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +

1
dao/src/test/resources/sql/hsql/drop-all-tables.sql

@ -13,6 +13,7 @@ DROP TABLE IF EXISTS relation;
DROP TABLE IF EXISTS tb_user;
DROP TABLE IF EXISTS tenant;
DROP TABLE IF EXISTS ts_kv;
DROP TABLE IF EXISTS ts_kv_dictionary;
DROP TABLE IF EXISTS ts_kv_latest;
DROP TABLE IF EXISTS user_credentials;
DROP TABLE IF EXISTS widget_type;

4621
msa/js-executor/package-lock.json

File diff suppressed because it is too large

28
msa/js-executor/package.json

@ -6,28 +6,28 @@
"main": "server.js",
"bin": "server.js",
"scripts": {
"install": "pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js",
"install": "pkg -t node12-linux-x64,node12-win-x64 --out-path ./target . && node install.js",
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon server.js",
"start-prod": "NODE_ENV=production nodemon server.js"
},
"dependencies": {
"@azure/service-bus": "^1.1.7",
"@google-cloud/pubsub": "^1.7.3",
"amqplib": "^0.5.6",
"aws-sdk": "^2.677.0",
"@azure/service-bus": "^1.1.9",
"@google-cloud/pubsub": "^2.5.0",
"amqplib": "^0.6.0",
"aws-sdk": "^2.741.0",
"azure-sb": "^0.11.1",
"config": "^3.3.1",
"js-yaml": "^3.12.0",
"js-yaml": "^3.14.0",
"kafkajs": "^1.12.0",
"long": "^4.0.0",
"uuid-parse": "^1.0.0",
"uuid-random": "^1.3.0",
"winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.1"
"uuid-parse": "^1.1.0",
"uuid-random": "^1.3.2",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0"
},
"engines": {
"node": ">=8.0.0 <11.0.0"
"node": ">=12.0.0 <14.0.0"
},
"nyc": {
"exclude": [
@ -38,9 +38,9 @@
]
},
"devDependencies": {
"fs-extra": "^6.0.1",
"nodemon": "^1.17.5",
"pkg": "^4.4.8"
"fs-extra": "^9.0.1",
"nodemon": "^2.0.4",
"pkg": "^4.4.9"
},
"pkg": {
"assets": [

24
msa/js-executor/pom.xml

@ -59,26 +59,26 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.0</version>
<version>1.7.5</version>
<configuration>
<installDirectory>target</installDirectory>
<workingDirectory>${basedir}</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<id>install node and yarn</id>
<goals>
<goal>install-node-and-npm</goal>
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v10.16.0</nodeVersion>
<npmVersion>6.4.1</npmVersion>
<nodeVersion>v12.16.1</nodeVersion>
<yarnVersion>v1.22.4</yarnVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<id>yarn install</id>
<goals>
<goal>npm</goal>
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install</arguments>
@ -138,10 +138,10 @@
</build>
<profiles>
<profile>
<id>npm-start</id>
<id>yarn-start</id>
<activation>
<property>
<name>npm-start</name>
<name>yarn-start</name>
</property>
</activation>
<build>
@ -149,16 +149,16 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.0</version>
<version>1.7.5</version>
<configuration>
<installDirectory>target</installDirectory>
<workingDirectory>${basedir}</workingDirectory>
</configuration>
<executions>
<execution>
<id>npm start</id>
<id>yarn start</id>
<goals>
<goal>npm</goal>
<goal>yarn</goal>
</goals>
<configuration>

2902
msa/js-executor/yarn.lock

File diff suppressed because it is too large

3554
msa/web-ui/package-lock.json

File diff suppressed because it is too large

26
msa/web-ui/package.json

@ -6,24 +6,24 @@
"main": "server.js",
"bin": "server.js",
"scripts": {
"install": "pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js",
"install": "pkg -t node12-linux-x64,node12-win-x64 --out-path ./target . && node install.js",
"test": "echo \"Error: no test specified\" && exit 1",
"start": "WEB_FOLDER=./target/web nodemon server.js",
"start-prod": "NODE_ENV=production nodemon server.js"
},
"dependencies": {
"compression": "^1.7.3",
"config": "^3.2.2",
"connect-history-api-fallback": "^1.5.0",
"express": "^4.16.3",
"compression": "^1.7.4",
"config": "^3.3.1",
"connect-history-api-fallback": "^1.6.0",
"express": "^4.17.1",
"http": "0.0.0",
"http-proxy": "^1.17.0",
"js-yaml": "^3.12.0",
"winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.1"
"http-proxy": "^1.18.1",
"js-yaml": "^3.14.0",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0"
},
"engines": {
"node": ">=8.0.0 <11.0.0"
"node": ">=12.0.0 <14.0.0"
},
"nyc": {
"exclude": [
@ -34,9 +34,9 @@
]
},
"devDependencies": {
"fs-extra": "^6.0.1",
"nodemon": "^1.17.5",
"pkg": "^4.4.0"
"fs-extra": "^9.0.1",
"nodemon": "^2.0.4",
"pkg": "^4.4.9"
},
"pkg": {
"assets": [

194
msa/web-ui/pom.xml

@ -68,26 +68,26 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.0</version>
<version>1.7.5</version>
<configuration>
<installDirectory>target</installDirectory>
<workingDirectory>${basedir}</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<id>install node and yarn</id>
<goals>
<goal>install-node-and-npm</goal>
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v10.16.0</nodeVersion>
<npmVersion>6.4.1</npmVersion>
<nodeVersion>v12.16.1</nodeVersion>
<yarnVersion>v1.22.4</yarnVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<id>yarn install</id>
<goals>
<goal>npm</goal>
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install</arguments>
@ -149,176 +149,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-resources-plugin</artifactId>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <id>copy-linux-conf</id>-->
<!-- <phase>process-resources</phase>-->
<!-- <goals>-->
<!-- <goal>copy-resources</goal>-->
<!-- </goals>-->
<!-- <configuration>-->
<!-- <outputDirectory>${pkg.linux.dist}/conf</outputDirectory>-->
<!-- <resources>-->
<!-- <resource>-->
<!-- <directory>config</directory>-->
<!-- <filtering>true</filtering>-->
<!-- </resource>-->
<!-- </resources>-->
<!-- <filters>-->
<!-- <filter>src/main/filters/unix.properties</filter>-->
<!-- </filters>-->
<!-- </configuration>-->
<!-- </execution>-->
<!-- <execution>-->
<!-- <id>copy-linux-init</id>-->
<!-- <phase>process-resources</phase>-->
<!-- <goals>-->
<!-- <goal>copy-resources</goal>-->
<!-- </goals>-->
<!-- <configuration>-->
<!-- <outputDirectory>${pkg.linux.dist}/init</outputDirectory>-->
<!-- <resources>-->
<!-- <resource>-->
<!-- <directory>src/main/scripts/init</directory>-->
<!-- <filtering>true</filtering>-->
<!-- </resource>-->
<!-- </resources>-->
<!-- <filters>-->
<!-- <filter>src/main/filters/unix.properties</filter>-->
<!-- </filters>-->
<!-- </configuration>-->
<!-- </execution>-->
<!-- <execution>-->
<!-- <id>copy-win-conf</id>-->
<!-- <phase>process-resources</phase>-->
<!-- <goals>-->
<!-- <goal>copy-resources</goal>-->
<!-- </goals>-->
<!-- <configuration>-->
<!-- <outputDirectory>${pkg.win.dist}/conf</outputDirectory>-->
<!-- <resources>-->
<!-- <resource>-->
<!-- <directory>config</directory>-->
<!-- <excludes>-->
<!-- <exclude>tb-web-ui.conf</exclude>-->
<!-- </excludes>-->
<!-- <filtering>true</filtering>-->
<!-- </resource>-->
<!-- </resources>-->
<!-- <filters>-->
<!-- <filter>src/main/filters/windows.properties</filter>-->
<!-- </filters>-->
<!-- </configuration>-->
<!-- </execution>-->
<!-- <execution>-->
<!-- <id>copy-control</id>-->
<!-- <phase>process-resources</phase>-->
<!-- <goals>-->
<!-- <goal>copy-resources</goal>-->
<!-- </goals>-->
<!-- <configuration>-->
<!-- <outputDirectory>${project.build.directory}/control</outputDirectory>-->
<!-- <resources>-->
<!-- <resource>-->
<!-- <directory>src/main/scripts/control</directory>-->
<!-- <filtering>true</filtering>-->
<!-- </resource>-->
<!-- </resources>-->
<!-- <filters>-->
<!-- <filter>src/main/filters/unix.properties</filter>-->
<!-- </filters>-->
<!-- </configuration>-->
<!-- </execution>-->
<!-- <execution>-->
<!-- <id>copy-windows-control</id>-->
<!-- <phase>process-resources</phase>-->
<!-- <goals>-->
<!-- <goal>copy-resources</goal>-->
<!-- </goals>-->
<!-- <configuration>-->
<!-- <outputDirectory>${pkg.win.dist}</outputDirectory>-->
<!-- <resources>-->
<!-- <resource>-->
<!-- <directory>src/main/scripts/windows</directory>-->
<!-- <filtering>true</filtering>-->
<!-- </resource>-->
<!-- </resources>-->
<!-- <filters>-->
<!-- <filter>src/main/filters/windows.properties</filter>-->
<!-- </filters>-->
<!-- </configuration>-->
<!-- </execution>-->
<!-- <execution>-->
<!-- <id>copy-docker-config</id>-->
<!-- <phase>process-resources</phase>-->
<!-- <goals>-->
<!-- <goal>copy-resources</goal>-->
<!-- </goals>-->
<!-- <configuration>-->
<!-- <outputDirectory>${project.build.directory}</outputDirectory>-->
<!-- <resources>-->
<!-- <resource>-->
<!-- <directory>docker</directory>-->
<!-- <filtering>true</filtering>-->
<!-- </resource>-->
<!-- </resources>-->
<!-- </configuration>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<!-- <plugin>-->
<!-- <groupId>org.thingsboard</groupId>-->
<!-- <artifactId>gradle-maven-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <gradleProjectDirectory>${basedir}</gradleProjectDirectory>-->
<!-- <tasks>-->
<!-- <task>build</task>-->
<!-- <task>buildDeb</task>-->
<!-- <task>buildRpm</task>-->
<!-- <task>renameDeb</task>-->
<!-- <task>renameRpm</task>-->
<!-- </tasks>-->
<!-- <args>-->
<!-- <arg>-PprojectBuildDir=${project.build.directory}</arg>-->
<!-- <arg>-PprojectVersion=${project.version}</arg>-->
<!-- <arg>-PpkgName=${pkg.name}</arg>-->
<!-- <arg>-PpkgUser=${pkg.user}</arg>-->
<!-- <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>-->
<!-- <arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg>-->
<!-- </args>-->
<!-- </configuration>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <phase>package</phase>-->
<!-- <goals>-->
<!-- <goal>invoke</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-assembly-plugin</artifactId>-->
<!-- <version>3.0.0</version>-->
<!-- <configuration>-->
<!-- <finalName>${pkg.name}</finalName>-->
<!-- <descriptors>-->
<!-- <descriptor>../../packaging/js/assembly/windows.xml</descriptor>-->
<!-- </descriptors>-->
<!-- </configuration>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <id>assembly</id>-->
<!-- <phase>package</phase>-->
<!-- <goals>-->
<!-- <goal>single</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
@ -355,10 +185,10 @@
</build>
<profiles>
<profile>
<id>npm-start</id>
<id>yarn-start</id>
<activation>
<property>
<name>npm-start</name>
<name>yarn-start</name>
</property>
</activation>
<build>
@ -366,16 +196,16 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.0</version>
<version>1.7.5</version>
<configuration>
<installDirectory>target</installDirectory>
<workingDirectory>${basedir}</workingDirectory>
</configuration>
<executions>
<execution>
<id>npm start</id>
<id>yarn start</id>
<goals>
<goal>npm</goal>
<goal>yarn</goal>
</goals>
<configuration>

2155
msa/web-ui/yarn.lock

File diff suppressed because it is too large

4
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java

@ -198,10 +198,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId());
MqttMessage pubrecMessage = new MqttMessage(fixedHeader, variableHeader);
MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message, pubrecMessage);
MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message);
this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish);
message.payload().retain();
incomingQos2Publish.startPubrecRetransmitTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket);
channel.writeAndFlush(pubrecMessage);
}
@ -248,7 +247,6 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) {
MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish());
incomingQos2Publish.onPubrelReceived();
this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId());
}
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);

21
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java

@ -15,34 +15,17 @@
*/
package org.thingsboard.mqtt;
import io.netty.channel.EventLoop;
import io.netty.handler.codec.mqtt.*;
import java.util.function.Consumer;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
final class MqttIncomingQos2Publish {
private final MqttPublishMessage incomingPublish;
private final RetransmissionHandler<MqttMessage> retransmissionHandler = new RetransmissionHandler<>();
MqttIncomingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) {
MqttIncomingQos2Publish(MqttPublishMessage incomingPublish) {
this.incomingPublish = incomingPublish;
this.retransmissionHandler.setOriginalMessage(originalMessage);
}
MqttPublishMessage getIncomingPublish() {
return incomingPublish;
}
void startPubrecRetransmitTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
this.retransmissionHandler.setHandle((fixedHeader, originalMessage) ->
sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader())));
this.retransmissionHandler.start(eventLoop);
}
void onPubrelReceived() {
this.retransmissionHandler.stop();
}
}

23
rest-client/pom.xml

@ -45,4 +45,27 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

24
rule-engine/rule-engine-api/pom.xml

@ -89,4 +89,28 @@
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java

@ -27,7 +27,7 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.FILTER,
name = "originator type switch",
configClazz = EmptyNodeConfiguration.class,
relationTypes = {"Device", "Asset", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
relationTypes = {"Device", "Asset", "Alarm", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
nodeDescription = "Route incoming messages by Message Originator Type",
nodeDetails = "Routes messages to chain according to the originator type ('Device', 'Asset', etc.).",
uiResources = {"static/rulenode/rulenode-core-config.js"},
@ -73,6 +73,9 @@ public class TbOriginatorTypeSwitchNode implements TbNode {
case RULE_NODE:
relationType = "Rule node";
break;
case ALARM:
relationType = "Alarm";
break;
default:
throw new TbNodeException("Unsupported originator type: " + originatorType);
}

66
ui-ngx/src/app/core/http/device-profile.service.ts

@ -0,0 +1,66 @@
///
/// Copyright © 2016-2020 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { DeviceProfile, DeviceProfileInfo } from '@shared/models/device.models';
@Injectable({
providedIn: 'root'
})
export class DeviceProfileService {
constructor(
private http: HttpClient
) { }
public getDeviceProfiles(pageLink: PageLink, config?: RequestConfig): Observable<PageData<DeviceProfile>> {
return this.http.get<PageData<DeviceProfile>>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable<DeviceProfile> {
return this.http.get<DeviceProfile>(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
}
public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable<DeviceProfile> {
return this.http.post<DeviceProfile>('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config));
}
public deleteDeviceProfile(deviceProfileId: string, config?: RequestConfig) {
return this.http.delete(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
}
public setDefaultDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable<DeviceProfile> {
return this.http.post<DeviceProfile>(`/api/deviceProfile/${deviceProfileId}/default`, defaultHttpOptionsFromConfig(config));
}
public getDefaultDeviceProfileInfo(config?: RequestConfig): Observable<DeviceProfileInfo> {
return this.http.get<DeviceProfileInfo>('/api/deviceProfileInfo/default', defaultHttpOptionsFromConfig(config));
}
public getDeviceProfileInfo(deviceProfileId: string, config?: RequestConfig): Observable<DeviceProfileInfo> {
return this.http.get<DeviceProfileInfo>(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
}
public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable<PageData<DeviceProfileInfo>> {
return this.http.get<PageData<DeviceProfileInfo>>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
}

13
ui-ngx/src/app/core/services/menu.service.ts

@ -215,6 +215,13 @@ export class MenuService {
path: '/devices',
icon: 'devices_other'
},
{
name: 'device-profile.device-profiles',
type: 'link',
path: '/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
},
{
name: 'entity-view.entity-views',
type: 'link',
@ -283,6 +290,12 @@ export class MenuService {
name: 'device.devices',
icon: 'devices_other',
path: '/devices'
},
{
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/deviceProfiles'
}
]
},

84
ui-ngx/src/app/modules/common/modules-map.ts

@ -14,15 +14,53 @@
/// limitations under the License.
///
import * as AngularAnimations from '@angular/animations';
import * as AngularCore from '@angular/core';
import * as AngularCommon from '@angular/common';
import * as AngularForms from '@angular/forms';
import * as AngularFlexLayout from '@angular/flex-layout';
import * as AngularPlatformBrowser from '@angular/platform-browser';
import * as AngularRouter from '@angular/router';
import * as AngularCdkKeycodes from '@angular/cdk/keycodes';
import * as AngularCdkCoercion from '@angular/cdk/coercion';
import * as AngularMaterialChips from '@angular/material/chips';
import * as AngularCdkCollections from '@angular/cdk/collections';
import * as AngularCdkKeycodes from '@angular/cdk/keycodes';
import * as AngularCdkLayout from '@angular/cdk/layout';
import * as AngularCdkOverlay from '@angular/cdk/overlay';
import * as AngularCdkPortal from '@angular/cdk/portal';
import * as AngularMaterialAutocomplete from '@angular/material/autocomplete';
import * as AngularMaterialBadge from '@angular/material/badge';
import * as AngularMaterialBottomSheet from '@angular/material/bottom-sheet';
import * as AngularMaterialButton from '@angular/material/button';
import * as AngularMaterialButtonToggle from '@angular/material/button-toggle';
import * as AngularMaterialCard from '@angular/material/card';
import * as AngularMaterialCheckbox from '@angular/material/checkbox';
import * as AngularMaterialChips from '@angular/material/chips';
import * as AngularMaterialCore from '@angular/material/core';
import * as AngularMaterialDatepicker from '@angular/material/datepicker';
import * as AngularMaterialDialog from '@angular/material/dialog';
import * as AngularMaterialDivider from '@angular/material/divider';
import * as AngularMaterialExpansion from '@angular/material/expansion';
import * as AngularMaterialFormField from '@angular/material/form-field';
import * as AngularMaterialGridList from '@angular/material/grid-list';
import * as AngularMaterialIcon from '@angular/material/icon';
import * as AngularMaterialInput from '@angular/material/input';
import * as AngularMaterialList from '@angular/material/list';
import * as AngularMaterialMenu from '@angular/material/menu';
import * as AngularMaterialPaginator from '@angular/material/paginator';
import * as AngularMaterialProgressBar from '@angular/material/progress-bar';
import * as AngularMaterialProgressSpinner from '@angular/material/progress-spinner';
import * as AngularMaterialRadio from '@angular/material/radio';
import * as AngularMaterialSelect from '@angular/material/select';
import * as AngularMaterialSidenav from '@angular/material/sidenav';
import * as AngularMaterialSlideToggle from '@angular/material/slide-toggle';
import * as AngularMaterialSlider from '@angular/material/slider';
import * as AngularMaterialSnackBar from '@angular/material/snack-bar';
import * as AngularMaterialSort from '@angular/material/sort';
import * as AngularMaterialStepper from '@angular/material/stepper';
import * as AngularMaterialTable from '@angular/material/table';
import * as AngularMaterialTabs from '@angular/material/tabs';
import * as AngularMaterialToolbar from '@angular/material/toolbar';
import * as AngularMaterialTree from '@angular/material/tree';
import * as NgrxStore from '@ngrx/store';
import * as RxJs from 'rxjs';
import * as RxJsOperators from 'rxjs/operators';
@ -35,15 +73,53 @@ import * as _moment from 'moment';
declare const SystemJS;
export const modulesMap: {[key: string]: any} = {
'@angular/animations': SystemJS.newModule(AngularAnimations),
'@angular/core': SystemJS.newModule(AngularCore),
'@angular/common': SystemJS.newModule(AngularCommon),
'@angular/forms': SystemJS.newModule(AngularForms),
'@angular/flex-layout': SystemJS.newModule(AngularFlexLayout),
'@angular/platform-browser': SystemJS.newModule(AngularPlatformBrowser),
'@angular/router': SystemJS.newModule(AngularRouter),
'@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes),
'@angular/cdk/coercion': SystemJS.newModule(AngularCdkCoercion),
'@angular/material/chips': SystemJS.newModule(AngularMaterialChips),
'@angular/cdk/collections': SystemJS.newModule(AngularCdkCollections),
'@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes),
'@angular/cdk/layout': SystemJS.newModule(AngularCdkLayout),
'@angular/cdk/overlay': SystemJS.newModule(AngularCdkOverlay),
'@angular/cdk/portal': SystemJS.newModule(AngularCdkPortal),
'@angular/material/autocomplete': SystemJS.newModule(AngularMaterialAutocomplete),
'@angular/material/badge': SystemJS.newModule(AngularMaterialBadge),
'@angular/material/bottom-sheet': SystemJS.newModule(AngularMaterialBottomSheet),
'@angular/material/button': SystemJS.newModule(AngularMaterialButton),
'@angular/material/button-toggle': SystemJS.newModule(AngularMaterialButtonToggle),
'@angular/material/card': SystemJS.newModule(AngularMaterialCard),
'@angular/material/checkbox': SystemJS.newModule(AngularMaterialCheckbox),
'@angular/material/chips': SystemJS.newModule(AngularMaterialChips),
'@angular/material/core': SystemJS.newModule(AngularMaterialCore),
'@angular/material/datepicker': SystemJS.newModule(AngularMaterialDatepicker),
'@angular/material/dialog': SystemJS.newModule(AngularMaterialDialog),
'@angular/material/divider': SystemJS.newModule(AngularMaterialDivider),
'@angular/material/expansion': SystemJS.newModule(AngularMaterialExpansion),
'@angular/material/form-field': SystemJS.newModule(AngularMaterialFormField),
'@angular/material/grid-list': SystemJS.newModule(AngularMaterialGridList),
'@angular/material/icon': SystemJS.newModule(AngularMaterialIcon),
'@angular/material/input': SystemJS.newModule(AngularMaterialInput),
'@angular/material/list': SystemJS.newModule(AngularMaterialList),
'@angular/material/menu': SystemJS.newModule(AngularMaterialMenu),
'@angular/material/paginator': SystemJS.newModule(AngularMaterialPaginator),
'@angular/material/progress-bar': SystemJS.newModule(AngularMaterialProgressBar),
'@angular/material/progress-spinner': SystemJS.newModule(AngularMaterialProgressSpinner),
'@angular/material/radio': SystemJS.newModule(AngularMaterialRadio),
'@angular/material/select': SystemJS.newModule(AngularMaterialSelect),
'@angular/material/sidenav': SystemJS.newModule(AngularMaterialSidenav),
'@angular/material/slide-toggle': SystemJS.newModule(AngularMaterialSlideToggle),
'@angular/material/slider': SystemJS.newModule(AngularMaterialSlider),
'@angular/material/snack-bar': SystemJS.newModule(AngularMaterialSnackBar),
'@angular/material/sort': SystemJS.newModule(AngularMaterialSort),
'@angular/material/stepper': SystemJS.newModule(AngularMaterialStepper),
'@angular/material/table': SystemJS.newModule(AngularMaterialTable),
'@angular/material/tabs': SystemJS.newModule(AngularMaterialTabs),
'@angular/material/toolbar': SystemJS.newModule(AngularMaterialToolbar),
'@angular/material/tree': SystemJS.newModule(AngularMaterialTree),
'@ngrx/store': SystemJS.newModule(NgrxStore),
rxjs: SystemJS.newModule(RxJs),
'rxjs/operators': SystemJS.newModule(RxJsOperators),

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

@ -86,6 +86,12 @@ import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.
import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component';
import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component';
import { TenantProfileComponent } from './profile/tenant-profile.component';
import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component';
import { TenantProfileDataComponent } from './profile/tenant-profile-data.component';
import { DefaultDeviceProfileConfigurationComponent } from './profile/device/default-device-profile-configuration.component';
import { DeviceProfileConfigurationComponent } from './profile/device/device-profile-configuration.component';
import { DeviceProfileDataComponent } from './profile/device-profile-data.component';
import { DeviceProfileComponent } from './profile/device-profile.component';
@NgModule({
declarations:
@ -154,7 +160,13 @@ import { TenantProfileComponent } from './profile/tenant-profile.component';
FilterUserInfoDialogComponent,
FilterPredicateValueComponent,
TenantProfileAutocompleteComponent,
TenantProfileComponent
TenantProfileDataComponent,
TenantProfileComponent,
TenantProfileDialogComponent,
DefaultDeviceProfileConfigurationComponent,
DeviceProfileConfigurationComponent,
DeviceProfileDataComponent,
DeviceProfileComponent
],
imports: [
CommonModule,
@ -212,7 +224,13 @@ import { TenantProfileComponent } from './profile/tenant-profile.component';
FiltersEditComponent,
UserFilterDialogComponent,
TenantProfileAutocompleteComponent,
TenantProfileComponent
TenantProfileDataComponent,
TenantProfileComponent,
TenantProfileDialogComponent,
DefaultDeviceProfileConfigurationComponent,
DeviceProfileConfigurationComponent,
DeviceProfileDataComponent,
DeviceProfileComponent
],
providers: [
WidgetComponentService,

40
ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="deviceProfileDataFormGroup">
<mat-accordion multi="true">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>device-profile.profile-configuration</div>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-device-profile-configuration
formControlName="configuration"
required>
</tb-device-profile-configuration>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>device-profile.transport-configuration</div>
</mat-panel-title>
</mat-expansion-panel-header>
TODO
</mat-expansion-panel>
</mat-accordion>
</div>

92
ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts

@ -0,0 +1,92 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileData } from '@shared/models/device.models';
@Component({
selector: 'tb-device-profile-data',
templateUrl: './device-profile-data.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceProfileDataComponent),
multi: true
}]
})
export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit {
deviceProfileDataFormGroup: FormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.deviceProfileDataFormGroup = this.fb.group({
configuration: [null, Validators.required]
});
this.deviceProfileDataFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceProfileDataFormGroup.disable({emitEvent: false});
} else {
this.deviceProfileDataFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DeviceProfileData | null): void {
this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false});
}
private updateModel() {
let deviceProfileData: DeviceProfileData = null;
if (this.deviceProfileDataFormGroup.valid) {
deviceProfileData = this.deviceProfileDataFormGroup.getRawValue();
}
this.propagateChange(deviceProfileData);
}
}

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

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')"
[fxShow]="!isEdit && !entity?.default">
{{'device-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'device-profile.delete' | translate }}
</button>
<div fxLayout="row" fxLayout.xs="column">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onDeviceProfileIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>device-profile.copyId</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="entityForm.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="entityForm.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-data
formControlName="profileData"
required>
</tb-device-profile-data>
<tb-entity-autocomplete
[entityType]="entityType.RULE_CHAIN"
formControlName="defaultRuleChainId">
</tb-entity-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</div>

128
ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts

@ -0,0 +1,128 @@
///
/// Copyright © 2016-2020 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, Input, Optional } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@app/core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { EntityComponent } from '../entity/entity.component';
import {
createDeviceProfileConfiguration,
DeviceProfile,
DeviceProfileData,
DeviceProfileType,
deviceProfileTypeTranslationMap
} from '@shared/models/device.models';
import { EntityType } from '@shared/models/entity-type.models';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
@Component({
selector: 'tb-device-profile',
templateUrl: './device-profile.component.html',
styleUrls: []
})
export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
@Input()
standalone = false;
entityType = EntityType;
deviceProfileTypes = Object.keys(DeviceProfileType);
deviceProfileTypeTranslations = deviceProfileTypeTranslationMap;
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Optional() @Inject('entity') protected entityValue: DeviceProfile,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>,
protected fb: FormBuilder) {
super(store, fb, entityValue, entitiesTableConfigValue);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildForm(entity: DeviceProfile): FormGroup {
const form = this.fb.group(
{
name: [entity ? entity.name : '', [Validators.required]],
type: [entity ? entity.type : '', [Validators.required]],
profileData: [entity && !this.isAdd ? entity.profileData : {}, []],
defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []],
description: [entity ? entity.description : '', []],
}
);
form.get('type').valueChanges.subscribe(() => {
this.deviceProfileTypeChanged(form);
});
this.checkIsNewDeviceProfile(entity, form);
return form;
}
private checkIsNewDeviceProfile(entity: DeviceProfile, form: FormGroup) {
if (entity && !entity.id) {
form.get('type').patchValue(DeviceProfileType.DEFAULT, {emitEvent: true});
}
}
private deviceProfileTypeChanged(form: FormGroup) {
const deviceProfileType: DeviceProfileType = form.get('type').value;
let profileData: DeviceProfileData = form.getRawValue().profileData;
if (!profileData) {
profileData = {
configuration: null
};
}
profileData.configuration = createDeviceProfileConfiguration(deviceProfileType);
this.entityForm.patchValue({profileData});
}
updateForm(entity: DeviceProfile) {
this.entityForm.patchValue({name: entity.name});
this.entityForm.patchValue({type: entity.type});
this.entityForm.patchValue({profileData: entity.profileData});
this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null});
this.entityForm.patchValue({description: entity.description});
}
prepareFormValue(formValue: any): any {
if (formValue.defaultRuleChainId) {
formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId);
}
return formValue;
}
onDeviceProfileIdCopied(event) {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('device-profile.idCopiedMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
}

24
ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html

@ -0,0 +1,24 @@
<!--
Copyright © 2016-2020 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.
-->
<form [formGroup]="defaultDeviceProfileConfigurationFormGroup" style="padding-bottom: 16px;">
<tb-json-object-edit
[required]="required"
label="{{ 'device-profile.type-default' | translate }}"
formControlName="configuration">
</tb-json-object-edit>
</form>

97
ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts

@ -0,0 +1,97 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DefaultDeviceProfileConfiguration,
DeviceProfileConfiguration,
DeviceProfileType
} from '@shared/models/device.models';
@Component({
selector: 'tb-default-device-profile-configuration',
templateUrl: './default-device-profile-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultDeviceProfileConfigurationComponent),
multi: true
}]
})
export class DefaultDeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit {
defaultDeviceProfileConfigurationFormGroup: FormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.defaultDeviceProfileConfigurationFormGroup = this.fb.group({
configuration: [null, Validators.required]
});
this.defaultDeviceProfileConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.defaultDeviceProfileConfigurationFormGroup.disable({emitEvent: false});
} else {
this.defaultDeviceProfileConfigurationFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DefaultDeviceProfileConfiguration | null): void {
this.defaultDeviceProfileConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
}
private updateModel() {
let configuration: DeviceProfileConfiguration = null;
if (this.defaultDeviceProfileConfigurationFormGroup.valid) {
configuration = this.defaultDeviceProfileConfigurationFormGroup.getRawValue().configuration;
configuration.type = DeviceProfileType.DEFAULT;
}
this.propagateChange(configuration);
}
}

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

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="deviceProfileConfigurationFormGroup">
<div [ngSwitch]="type">
<ng-template [ngSwitchCase]="deviceProfileType.DEFAULT">
<tb-default-device-profile-configuration
[required]="required"
formControlName="configuration">
</tb-default-device-profile-configuration>
</ng-template>
</div>
</div>

103
ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts

@ -0,0 +1,103 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models';
import { deepClone } from '../../../../../core/utils';
@Component({
selector: 'tb-device-profile-configuration',
templateUrl: './device-profile-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceProfileConfigurationComponent),
multi: true
}]
})
export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit {
deviceProfileType = DeviceProfileType;
deviceProfileConfigurationFormGroup: FormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
type: DeviceProfileType;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.deviceProfileConfigurationFormGroup = this.fb.group({
configuration: [null, Validators.required]
});
this.deviceProfileConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceProfileConfigurationFormGroup.disable({emitEvent: false});
} else {
this.deviceProfileConfigurationFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DeviceProfileConfiguration | null): void {
this.type = value?.type;
const configuration = deepClone(value);
if (configuration) {
delete configuration.type;
}
this.deviceProfileConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
}
private updateModel() {
let configuration: DeviceProfileConfiguration = null;
if (this.deviceProfileConfigurationFormGroup.valid) {
configuration = this.deviceProfileConfigurationFormGroup.getRawValue().configuration;
configuration.type = this.type;
}
this.propagateChange(configuration);
}
}

29
ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html

@ -20,6 +20,8 @@
#tenantProfileInput
formControlName="tenantProfile"
[required]="required"
(keydown)="tenantProfileEnter($event)"
(keypress)="tenantProfileEnter($event)"
[matAutocomplete]="tenantProfileAutocomplete">
<button *ngIf="selectTenantProfileFormGroup.get('tenantProfile').value && !disabled"
type="button"
@ -27,6 +29,14 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectTenantProfileFormGroup.get('tenantProfile').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Edit"
matTooltip="{{ 'tenant-profile.edit' | translate }}"
matTooltipPosition="above"
(click)="editTenantProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#tenantProfileAutocomplete="matAutocomplete"
@ -34,10 +44,21 @@
<mat-option *ngFor="let tenantProfile of filteredTenantProfiles | async" [value]="tenantProfile">
<span [innerHTML]="tenantProfile.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredTenantProfiles | async)?.length" [value]="null">
<span>
{{ translate.get('tenant-profile.no-tenant-profiles-matching', {entity: searchText}) | async }}
</span>
<mat-option *ngIf="!(filteredTenantProfiles | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>tenant-profile.no-tenant-profiles-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('tenant-profile.no-tenant-profiles-matching',
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
<span>
<a translate (click)="createTenantProfile($event, searchText)">tenant-profile.create-new-tenant-profile</a>
</span>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectTenantProfileFormGroup.get('tenantProfile').hasError('required')">

79
ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
@ -27,7 +27,12 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TenantProfileId } from '@shared/models/id/tenant-profile-id';
import { EntityInfoData } from '@shared/models/entity.models';
import { TenantProfileService } from '@core/http/tenant-profile.service';
import { entityIdEquals } from '../../../../shared/models/id/entity-id';
import { entityIdEquals } from '@shared/models/id/entity-id';
import { TruncatePipe } from '@shared//pipe/truncate.pipe';
import { ENTER } from '@angular/cdk/keycodes';
import { TenantProfile } from '@shared/models/tenant.model';
import { MatDialog } from '@angular/material/dialog';
import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component';
@Component({
selector: 'tb-tenant-profile-autocomplete',
@ -60,6 +65,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
@Input()
disabled: boolean;
@Output()
tenantProfileUpdated = new EventEmitter<TenantProfileId>();
@ViewChild('tenantProfileInput', {static: true}) tenantProfileInput: ElementRef;
filteredTenantProfiles: Observable<Array<EntityInfoData>>;
@ -70,8 +78,10 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
constructor(private store: Store<AppState>,
public translate: TranslateService,
public truncate: TruncatePipe,
private tenantProfileService: TenantProfileService,
private fb: FormBuilder) {
private fb: FormBuilder,
private dialog: MatDialog) {
this.selectTenantProfileFormGroup = this.fb.group({
tenantProfile: [null]
});
@ -168,4 +178,67 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
}, 0);
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
tenantProfileEnter($event: KeyboardEvent) {
if ($event.keyCode === ENTER) {
$event.preventDefault();
if (!this.modelValue) {
this.createTenantProfile($event, this.searchText);
}
}
}
createTenantProfile($event: Event, profileName: string) {
$event.preventDefault();
const tenantProfile: TenantProfile = {
id: null,
name: profileName
};
this.openTenantProfileDialog(tenantProfile, true);
}
editTenantProfile($event: Event) {
$event.preventDefault();
this.tenantProfileService.getTenantProfile(this.modelValue.id).subscribe(
(tenantProfile) => {
this.openTenantProfileDialog(tenantProfile, false);
}
);
}
openTenantProfileDialog(tenantProfile: TenantProfile, isAdd: boolean) {
this.dialog.open<TenantProfileDialogComponent, TenantProfileDialogData,
TenantProfile>(TenantProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
tenantProfile
}
}).afterClosed().subscribe(
(savedTenantProfile) => {
if (!savedTenantProfile) {
setTimeout(() => {
this.tenantProfileInput.nativeElement.blur();
this.tenantProfileInput.nativeElement.focus();
}, 0);
} else {
this.tenantProfileService.getTenantProfileInfo(savedTenantProfile.id.id).subscribe(
(profile) => {
this.modelValue = new TenantProfileId(profile.id.id);
this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: true});
if (isAdd) {
this.propagateChange(this.modelValue);
} else {
this.tenantProfileUpdated.next(savedTenantProfile.id);
}
}
);
}
}
);
}
}

24
ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html

@ -0,0 +1,24 @@
<!--
Copyright © 2016-2020 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.
-->
<form [formGroup]="tenantProfileDataFormGroup" style="padding-bottom: 16px;">
<tb-json-object-edit
[required]="required"
label="{{ 'tenant-profile.data' | translate }}"
formControlName="tenantProfileData">
</tb-json-object-edit>
</form>

93
ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts

@ -0,0 +1,93 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TenantProfileData } from '@shared/models/tenant.model';
@Component({
selector: 'tb-tenant-profile-data',
templateUrl: './tenant-profile-data.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TenantProfileDataComponent),
multi: true
}]
})
export class TenantProfileDataComponent implements ControlValueAccessor, OnInit {
tenantProfileDataFormGroup: FormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.tenantProfileDataFormGroup = this.fb.group({
tenantProfileData: [null, Validators.required]
});
this.tenantProfileDataFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.tenantProfileDataFormGroup.disable({emitEvent: false});
} else {
this.tenantProfileDataFormGroup.enable({emitEvent: false});
}
}
writeValue(value: TenantProfileData | null): void {
this.tenantProfileDataFormGroup.get('tenantProfileData').patchValue(value, {emitEvent: false});
}
private updateModel() {
let tenantProfileData: TenantProfileData = null;
if (this.tenantProfileDataFormGroup.valid) {
tenantProfileData = this.tenantProfileDataFormGroup.getRawValue().profileData;
}
this.propagateChange(tenantProfileData);
}
}

53
ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2020 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.
-->
<form (ngSubmit)="save()" style="min-width: 600px;">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'tenant-profile.add' : 'tenant-profile.edit' ) | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-tenant-profile
#tenantProfileComponent
[standalone]="true"
[entity]="tenantProfile"
[isEdit]="true">
</tb-tenant-profile>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || tenantProfileComponent.entityForm?.invalid || !tenantProfileComponent.entityForm?.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

101
ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts

@ -0,0 +1,101 @@
///
/// Copyright © 2016-2020 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,
ComponentFactoryResolver,
Inject,
Injector,
SkipSelf,
ViewChild
} from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { TenantProfile } from '@shared/models/tenant.model';
import { TenantProfileComponent } from './tenant-profile.component';
import { TenantProfileService } from '@core/http/tenant-profile.service';
export interface TenantProfileDialogData {
tenantProfile: TenantProfile;
isAdd: boolean;
}
@Component({
selector: 'tb-tenant-profile-dialog',
templateUrl: './tenant-profile-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: TenantProfileDialogComponent}],
styleUrls: []
})
export class TenantProfileDialogComponent extends
DialogComponent<TenantProfileDialogComponent, TenantProfile> implements ErrorStateMatcher, AfterViewInit {
isAdd: boolean;
tenantProfile: TenantProfile;
submitted = false;
@ViewChild('tenantProfileComponent', {static: true}) tenantProfileComponent: TenantProfileComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: TenantProfileDialogData,
public dialogRef: MatDialogRef<TenantProfileDialogComponent, TenantProfile>,
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private tenantProfileService: TenantProfileService) {
super(store, router, dialogRef);
this.isAdd = this.data.isAdd;
this.tenantProfile = this.data.tenantProfile;
}
ngAfterViewInit(): void {
if (this.isAdd) {
setTimeout(() => {
this.tenantProfileComponent.entityForm.markAsDirty();
}, 0);
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.tenantProfileComponent.entityForm.valid) {
this.tenantProfile = {...this.tenantProfile, ...this.tenantProfileComponent.entityFormValue()};
this.tenantProfileService.saveTenantProfile(this.tenantProfile).subscribe(
(tenantProfile) => {
this.dialogRef.close(tenantProfile);
}
);
}
}
}

4
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html

@ -59,6 +59,10 @@
<div class="tb-hint">{{'tenant.isolated-tb-rule-engine-details' | translate}}</div>
</mat-checkbox>
</div>
<tb-tenant-profile-data
formControlName="profileData"
required>
</tb-tenant-profile-data>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>

8
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, Input } from '@angular/core';
import { Component, Inject, Input, Optional } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -36,8 +36,8 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: TenantProfile,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>,
@Optional() @Inject('entity') protected entityValue: TenantProfile,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>,
protected fb: FormBuilder) {
super(store, fb, entityValue, entitiesTableConfigValue);
}
@ -56,6 +56,7 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
name: [entity ? entity.name : '', [Validators.required]],
isolatedTbCore: [entity ? entity.isolatedTbCore : false, []],
isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []],
profileData: [entity && !this.isAdd ? entity.profileData : {}, []],
description: [entity ? entity.description : '', []],
}
);
@ -65,6 +66,7 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
this.entityForm.patchValue({name: entity.name});
this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore});
this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine});
this.entityForm.patchValue({profileData: entity.profileData});
this.entityForm.patchValue({description: entity.description});
}

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

@ -41,6 +41,7 @@ import { CustomDialogService } from '@home/components/widget/dialog/custom-dialo
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
@Directive()
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
@ -77,6 +78,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
this.ctx.translate = $injector.get(TranslateService);
this.ctx.http = $injector.get(HttpClient);
this.ctx.sanitizer = $injector.get(DomSanitizer);
this.ctx.router = $injector.get(Router);
this.ctx.$scope = this;
if (this.ctx.defaultSubscription) {

4
ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts

@ -163,7 +163,7 @@ export const analogueCompassSettingsSchema: JsonSettingsSchema = {
form: [
{
key: 'majorTicks',
items:[
items: [
'majorTicks[]'
]
},
@ -267,7 +267,7 @@ export const analogueCompassSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',

2
ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts

@ -43,7 +43,7 @@ export class TbAnalogueCompass extends TbBaseGauge<AnalogueCompassSettings, Radi
protected createGaugeOptions(gaugeElement: HTMLElement, settings: AnalogueCompassSettings): RadialGaugeOptions {
const majorTicks = (settings.majorTicks && settings.majorTicks.length > 0) ? deepClone(settings.majorTicks) :
['N','NE','E','SE','S','SW','W','NW'];
['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
majorTicks.push(majorTicks[0]);
return {

32
ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts

@ -492,7 +492,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -581,7 +581,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -670,7 +670,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -759,7 +759,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -842,7 +842,7 @@ export abstract class TbBaseGauge<S, O extends GenericOptions> {
private gauge: BaseGauge;
protected constructor(protected ctx: WidgetContext, canvasId: string) {
const gaugeElement = $('#'+canvasId, ctx.$container)[0];
const gaugeElement = $('#' + canvasId, ctx.$container)[0];
const settings: S = ctx.settings;
const gaugeData: O = this.createGaugeOptions(gaugeElement, settings);
this.gauge = this.createGauge(gaugeData as O).draw();
@ -859,7 +859,7 @@ export abstract class TbBaseGauge<S, O extends GenericOptions> {
const tvPair = cellData.data[cellData.data.length -
1];
const value = tvPair[1];
if(value !== this.gauge.value) {
if (value !== this.gauge.value) {
this.gauge.value = value;
}
}
@ -876,10 +876,10 @@ export abstract class TbBaseGauge<S, O extends GenericOptions> {
}
}
export abstract class TbAnalogueGauge<S extends AnalogueGaugeSettings, O extends GenericOptions> extends TbBaseGauge<S,O> {
export abstract class TbAnalogueGauge<S extends AnalogueGaugeSettings, O extends GenericOptions> extends TbBaseGauge<S, O> {
protected constructor(ctx: WidgetContext, canvasId: string) {
super(ctx,canvasId);
super(ctx, canvasId);
}
protected createGaugeOptions(gaugeElement: HTMLElement, settings: S): O {
@ -891,26 +891,26 @@ export abstract class TbAnalogueGauge<S extends AnalogueGaugeSettings, O extends
const keyColor = settings.defaultColor || dataKey.color;
const majorTicksCount = settings.majorTicksCount || 10;
const total = maxValue-minValue;
let step = (total/majorTicksCount);
const total = maxValue - minValue;
let step = (total / majorTicksCount);
const valueInt = settings.valueInt || 3;
const valueDec = getValueDec(this.ctx, settings);
step = parseFloat(parseFloat(step+'').toFixed(valueDec));
step = parseFloat(parseFloat(step + '').toFixed(valueDec));
const majorTicks: number[] = [];
const highlights: Highlight[] = [];
let tick = minValue;
while(tick<=maxValue) {
while (tick <= maxValue) {
majorTicks.push(tick);
let nextTick = tick+step;
nextTick = parseFloat(parseFloat(nextTick+'').toFixed(valueDec));
if (tick<maxValue) {
let nextTick = tick + step;
nextTick = parseFloat(parseFloat(nextTick + '').toFixed(valueDec));
if (tick < maxValue) {
const highlightColor = tinycolor(keyColor);
const percent = (tick-minValue)/total;
const percent = (tick - minValue) / total;
highlightColor.setAlpha(percent);
const highlight: Highlight = {
from: tick,

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

@ -32,7 +32,7 @@ const tinycolor = tinycolor_;
const analogueLinearGaugeSettingsSchemaValue = getAnalogueLinearGaugeSettingsSchema();
export class TbAnalogueLinearGauge extends TbAnalogueGauge<AnalogueLinearGaugeSettings,LinearGaugeOptions>{
export class TbAnalogueLinearGauge extends TbAnalogueGauge<AnalogueLinearGaugeSettings, LinearGaugeOptions>{
static get settingsSchema(): JsonSettingsSchema {
return analogueLinearGaugeSettingsSchemaValue;

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

@ -28,7 +28,7 @@ import BaseGauge = CanvasGauges.BaseGauge;
const analogueRadialGaugeSettingsSchemaValue = getAnalogueRadialGaugeSettingsSchema();
export class TbAnalogueRadialGauge extends TbAnalogueGauge<AnalogueRadialGaugeSettings,RadialGaugeOptions>{
export class TbAnalogueRadialGauge extends TbAnalogueGauge<AnalogueRadialGaugeSettings, RadialGaugeOptions>{
static get settingsSchema(): JsonSettingsSchema {
return analogueRadialGaugeSettingsSchemaValue;

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

@ -18,7 +18,7 @@ import * as CanvasGauges from 'canvas-gauges';
import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models';
import * as tinycolor_ from 'tinycolor2';
import { ColorFormats } from 'tinycolor2';
import { isDefined, isString, isUndefined, padValue } from '@core/utils';
import { isDefined, isDefinedAndNotNull, isString, isUndefined, padValue } from '@core/utils';
import GenericOptions = CanvasGauges.GenericOptions;
import BaseGauge = CanvasGauges.BaseGauge;
@ -220,7 +220,7 @@ export class CanvasDigitalGauge extends BaseGauge {
public _value: number;
constructor(options: CanvasDigitalGaugeOptions) {
options = {...defaultDigitalGaugeOptions,...(options || {})};
options = {...defaultDigitalGaugeOptions, ...(options || {})};
super(CanvasDigitalGauge.configure(options));
this.initValueClone();
}
@ -236,10 +236,10 @@ export class CanvasDigitalGauge extends BaseGauge {
}
if (options.gaugeType === 'donut') {
if (!options.donutStartAngle) {
if (!isDefinedAndNotNull(options.donutStartAngle)) {
options.donutStartAngle = 1.5 * Math.PI;
}
if (!options.donutEndAngle) {
if (!isDefinedAndNotNull(options.donutEndAngle)) {
options.donutEndAngle = options.donutStartAngle + 2 * Math.PI;
}
}
@ -255,7 +255,7 @@ export class CanvasDigitalGauge extends BaseGauge {
const levelColor: any = options.levelColors[i];
if (levelColor !== null) {
let percentage: number;
if(isColorProperty){
if (isColorProperty) {
percentage = inc * i;
} else {
percentage = CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue);
@ -280,7 +280,7 @@ export class CanvasDigitalGauge extends BaseGauge {
options.ticksValue = [];
for (const tick of options.ticks) {
if (tick !== null) {
options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue))
options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue));
}
}
@ -294,7 +294,7 @@ export class CanvasDigitalGauge extends BaseGauge {
return options;
}
static normalizeValue (value: number, min: number, max: number): number {
static normalizeValue(value: number, min: number, max: number): number {
const normalValue = (value - min) / (max - min);
if (normalValue <= 0) {
return 0;
@ -539,8 +539,8 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D,
titleOffset += bd.fontSizeFactor * 2;
bd.titleY = bd.baseY + titleOffset;
titleOffset += bd.fontSizeFactor * 2;
bd.Cy += titleOffset/2;
bd.Ro -= titleOffset/2;
bd.Cy += titleOffset / 2;
bd.Ro -= titleOffset / 2;
}
bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2;
bd.Cx = bd.baseX + bd.width / 2;
@ -575,8 +575,8 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D,
const valueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor).height;
const labelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor).height;
const total = valueHeight + labelHeight;
bd.labelY = bd.Cy + total/2;
bd.valueY = bd.Cy - total/2 + valueHeight/2;
bd.labelY = bd.Cy + total / 2;
bd.valueY = bd.Cy - total / 2 + valueHeight / 2;
} else {
bd.valueY = bd.Cy;
}
@ -586,8 +586,8 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D,
bd.labelY = bd.Cy + (8 + options.fontLabelSize) * bd.fontSizeFactor;
bd.minY = bd.maxY = bd.labelY;
if (options.roundedLineCap) {
bd.minY += bd.strokeWidth/2;
bd.maxY += bd.strokeWidth/2;
bd.minY += bd.strokeWidth / 2;
bd.maxY += bd.strokeWidth / 2;
}
bd.minX = bd.Cx - bd.Rm;
bd.maxX = bd.Cx + bd.Rm;
@ -604,15 +604,15 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D,
if (options.hideMinMax && options.label === '') {
bd.labelY = bd.barBottom;
bd.barLeft = bd.origBaseX + options.fontMinMaxSize/3 * bd.fontSizeFactor;
bd.barRight = bd.origBaseX + w + /*bd.width*/ - options.fontMinMaxSize/3 * bd.fontSizeFactor;
bd.barLeft = bd.origBaseX + options.fontMinMaxSize / 3 * bd.fontSizeFactor;
bd.barRight = bd.origBaseX + w + /*bd.width*/ -options.fontMinMaxSize / 3 * bd.fontSizeFactor;
} else {
context.font = Drawings.font(options, 'MinMax', bd.fontSizeFactor);
const minTextWidth = context.measureText(options.minValue+'').width;
const maxTextWidth = context.measureText(options.maxValue+'').width;
const minTextWidth = context.measureText(options.minValue + '').width;
const maxTextWidth = context.measureText(options.maxValue + '').width;
const maxW = Math.max(minTextWidth, maxTextWidth);
bd.minX = bd.origBaseX + maxW/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor;
bd.maxX = bd.origBaseX + w + /*bd.width*/ - maxW/2 - options.fontMinMaxSize/3 * bd.fontSizeFactor;
bd.minX = bd.origBaseX + maxW / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor;
bd.maxX = bd.origBaseX + w + /*bd.width*/ -maxW / 2 - options.fontMinMaxSize / 3 * bd.fontSizeFactor;
bd.barLeft = bd.minX;
bd.barRight = bd.maxX;
bd.labelY = bd.barBottom + (8 + options.fontLabelSize) * bd.fontSizeFactor;
@ -632,7 +632,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D,
bd.barBottom = bd.labelY - (8 + options.fontLabelSize) * bd.fontSizeFactor;
}
bd.minX = bd.maxX =
bd.baseX + bd.width/2 + bd.strokeWidth/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor;
bd.baseX + bd.width / 2 + bd.strokeWidth / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor;
bd.minY = bd.barBottom;
bd.maxY = bd.barTop;
bd.fontMinMaxBaseline = 'middle';
@ -658,13 +658,13 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D,
// tslint:disable-next-line:no-bitwise
dashCount = (dashCount - 1) | 1;
}
bd.dashLength = Math.ceil(circumference/dashCount);
bd.dashLength = Math.ceil(circumference / dashCount);
}
return bd;
}
function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string, baseSize: number): FontHeightInfo {
function determineFontHeight(options: CanvasDigitalGaugeOptions, target: string, baseSize: number): FontHeightInfo {
const fontStyleStr = 'font-style:' + options['font' + target + 'Style'] + ';font-weight:' +
options['font' + target + 'Weight'] + ';font-size:' +
options['font' + target + 'Size'] * baseSize + 'px;font-family:' +
@ -688,9 +688,9 @@ function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string
try {
result = {};
block.css({ verticalAlign: 'baseline' });
block.css({verticalAlign: 'baseline'});
result.ascent = block.offset().top - text.offset().top;
block.css({ verticalAlign: 'bottom' });
block.css({verticalAlign: 'bottom'});
result.height = block.offset().top - text.offset().top;
result.descent = result.height - result.ascent;
} finally {
@ -720,15 +720,15 @@ function drawBackground(context: DigitalGaugeCanvasRenderingContext2D, options:
context.stroke();
} else if (options.gaugeType === 'arc') {
context.arc(context.barDimensions.Cx, context.barDimensions.Cy,
context.barDimensions.Rm, Math.PI, 2*Math.PI);
context.barDimensions.Rm, Math.PI, 2 * Math.PI);
context.stroke();
} else if (options.gaugeType === 'horizontalBar') {
context.moveTo(barLeft,barTop + strokeWidth/2);
context.lineTo(barRight,barTop + strokeWidth/2);
context.moveTo(barLeft, barTop + strokeWidth / 2);
context.lineTo(barRight, barTop + strokeWidth / 2);
context.stroke();
} else if (options.gaugeType === 'verticalBar') {
context.moveTo(baseX + width/2, barBottom);
context.lineTo(baseX + width/2, barTop);
context.moveTo(baseX + width / 2, barBottom);
context.lineTo(baseX + width / 2, barTop);
context.stroke();
}
}
@ -740,7 +740,9 @@ function drawText(context: DigitalGaugeCanvasRenderingContext2D, options: Canvas
}
function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) {
if (!options.title || typeof options.title !== 'string') return;
if (!options.title || typeof options.title !== 'string') {
return;
}
const {titleY, width, baseX, fontSizeFactor} =
context.barDimensions;
@ -756,7 +758,9 @@ function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options
}
function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) {
if (!options.label || options.label === '') return;
if (!options.label || options.label === '') {
return;
}
const {labelY, baseX, width, fontSizeFactor} =
context.barDimensions;
@ -772,7 +776,9 @@ function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options
}
function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) {
if (options.hideMinMax || options.gaugeType === 'donut') return;
if (options.hideMinMax || options.gaugeType === 'donut') {
return;
}
const {minY, maxY, minX, maxX, fontSizeFactor, fontMinMaxAlign, fontMinMaxBaseline} =
context.barDimensions;
@ -782,12 +788,14 @@ function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, option
context.textBaseline = fontMinMaxBaseline;
context.font = Drawings.font(options, 'MinMax', fontSizeFactor);
context.lineWidth = 0;
drawText(context, options, 'MinMax', options.minValue+'', minX, minY);
drawText(context, options, 'MinMax', options.maxValue+'', maxX, maxY);
drawText(context, options, 'MinMax', options.minValue + '', minX, minY);
drawText(context, options, 'MinMax', options.maxValue + '', maxX, maxY);
}
function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, value: any) {
if (options.hideValue) return;
if (options.hideValue) {
return;
}
const {valueY, baseX, width, fontSizeFactor, fontValueBaseline} =
context.barDimensions;
@ -837,16 +845,16 @@ function drawArcGlow(context: DigitalGaugeCanvasRenderingContext2D,
context.setLineDash([]);
const strokeWidth = Ro - Ri;
const blur = 0.55;
const edge = strokeWidth*blur;
context.lineWidth = strokeWidth+edge;
const stop = blur/(2*blur+2);
const glowGradient = context.createRadialGradient(Cx,Cy,Ri-edge/2,Cx,Cy,Ro+edge/2);
const edge = strokeWidth * blur;
context.lineWidth = strokeWidth + edge;
const stop = blur / (2 * blur + 2);
const glowGradient = context.createRadialGradient(Cx, Cy, Ri - edge / 2, Cx, Cy, Ro + edge / 2);
const color1 = tinycolor(color).setAlpha(0.5).toRgbString();
const color2 = tinycolor(color).setAlpha(0).toRgbString();
glowGradient.addColorStop(0,color2);
glowGradient.addColorStop(stop,color1);
glowGradient.addColorStop(1.0-stop,color1);
glowGradient.addColorStop(1,color2);
glowGradient.addColorStop(0, color2);
glowGradient.addColorStop(stop, color1);
glowGradient.addColorStop(1.0 - stop, color1);
glowGradient.addColorStop(1, color2);
context.strokeStyle = glowGradient;
context.beginPath();
const e = 0.01 * Math.PI;
@ -863,21 +871,21 @@ function drawBarGlow(context: DigitalGaugeCanvasRenderingContext2D, startX: numb
endX: number, endY: number, color: string, strokeWidth: number, isVertical: boolean) {
context.setLineDash([]);
const blur = 0.55;
const edge = strokeWidth*blur;
context.lineWidth = strokeWidth+edge;
const stop = blur/(2*blur+2);
const gradientStartX = isVertical ? startX - context.lineWidth/2 : 0;
const gradientStartY = isVertical ? 0 : startY - context.lineWidth/2;
const gradientStopX = isVertical ? startX + context.lineWidth/2 : 0;
const gradientStopY = isVertical ? 0 : startY + context.lineWidth/2;
const glowGradient = context.createLinearGradient(gradientStartX,gradientStartY,gradientStopX,gradientStopY);
const edge = strokeWidth * blur;
context.lineWidth = strokeWidth + edge;
const stop = blur / (2 * blur + 2);
const gradientStartX = isVertical ? startX - context.lineWidth / 2 : 0;
const gradientStartY = isVertical ? 0 : startY - context.lineWidth / 2;
const gradientStopX = isVertical ? startX + context.lineWidth / 2 : 0;
const gradientStopY = isVertical ? 0 : startY + context.lineWidth / 2;
const glowGradient = context.createLinearGradient(gradientStartX, gradientStartY, gradientStopX, gradientStopY);
const color1 = tinycolor(color).setAlpha(0.5).toRgbString();
const color2 = tinycolor(color).setAlpha(0).toRgbString();
glowGradient.addColorStop(0,color2);
glowGradient.addColorStop(stop,color1);
glowGradient.addColorStop(1.0-stop,color1);
glowGradient.addColorStop(1,color2);
glowGradient.addColorStop(0, color2);
glowGradient.addColorStop(stop, color1);
glowGradient.addColorStop(1.0 - stop, color1);
glowGradient.addColorStop(1, color2);
context.strokeStyle = glowGradient;
const dx = isVertical ? 0 : 0.05 * context.lineWidth;
const dy = isVertical ? 0.05 * context.lineWidth : 0;
@ -984,12 +992,12 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
context.strokeStyle = neonColor;
}
context.beginPath();
context.moveTo(barLeft,barTop + strokeWidth/2);
context.lineTo(barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2);
context.moveTo(barLeft, barTop + strokeWidth / 2);
context.lineTo(barLeft + (barRight - barLeft) * progress, barTop + strokeWidth / 2);
context.stroke();
if (options.neonGlowBrightness && !options.isMobile) {
drawBarGlow(context, barLeft, barTop + strokeWidth/2,
barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2,
drawBarGlow(context, barLeft, barTop + strokeWidth / 2,
barLeft + (barRight - barLeft) * progress, barTop + strokeWidth / 2,
neonColor, strokeWidth, false);
}
drawTickBar(context, options.ticksValue, barLeft, barTop, barRight - barLeft, strokeWidth,
@ -999,12 +1007,12 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
context.strokeStyle = neonColor;
}
context.beginPath();
context.moveTo(baseX + width/2, barBottom);
context.lineTo(baseX + width/2, barBottom - (barBottom-barTop)*progress);
context.moveTo(baseX + width / 2, barBottom);
context.lineTo(baseX + width / 2, barBottom - (barBottom - barTop) * progress);
context.stroke();
if (options.neonGlowBrightness && !options.isMobile) {
drawBarGlow(context, baseX + width/2, barBottom,
baseX + width/2, barBottom - (barBottom-barTop)*progress,
drawBarGlow(context, baseX + width / 2, barBottom,
baseX + width / 2, barBottom - (barBottom - barTop) * progress,
neonColor, strokeWidth, true);
}
drawTickBar(context, options.ticksValue, baseX + width / 2, barTop, barTop - barBottom, strokeWidth,

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

@ -694,7 +694,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -783,7 +783,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -872,7 +872,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',
@ -961,7 +961,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
},
{
value: '700',
label: '800'
label: '700'
},
{
value: '800',

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

@ -25,7 +25,7 @@ import {
FixedLevelColors
} from '@home/components/widget/lib/digital-gauge.models';
import * as tinycolor_ from 'tinycolor2';
import { isDefined } from '@core/utils';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { prepareFontSettings } from '@home/components/widget/lib/settings.models';
import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge';
import { DatePipe } from '@angular/common';
@ -53,7 +53,7 @@ export class TbCanvasDigitalGauge {
}
constructor(protected ctx: WidgetContext, canvasId: string) {
const gaugeElement = $('#'+canvasId, ctx.$container)[0];
const gaugeElement = $('#' + canvasId, ctx.$container)[0];
const settings: DigitalGaugeSettings = ctx.settings;
this.localSettings = {};
@ -80,7 +80,7 @@ export class TbCanvasDigitalGauge {
this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false;
if (!settings.useFixedLevelColor) {
if (!settings.levelColors || settings.levelColors.length <= 0) {
if (!settings.levelColors || settings.levelColors.length === 0) {
this.localSettings.levelColors = [keyColor];
} else {
this.localSettings.levelColors = settings.levelColors.slice();
@ -97,14 +97,15 @@ export class TbCanvasDigitalGauge {
this.localSettings.colorTicks = settings.colorTicks || '#666';
this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals :
((isDefined(settings.decimals) && settings.decimals !== null)
? settings.decimals : ctx.decimals);
(isDefinedAndNotNull(settings.decimals) ? settings.decimals : ctx.decimals);
this.localSettings.units = dataKey.units && dataKey.units.length ? dataKey.units :
(isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units);
this.localSettings.hideValue = settings.showValue !== true;
this.localSettings.hideMinMax = settings.showMinMax !== true;
this.localSettings.donutStartAngle = isDefinedAndNotNull(settings.donutStartAngle) ?
-TbCanvasDigitalGauge.toRadians(settings.donutStartAngle) : null;
this.localSettings.title = ((settings.showTitle === true) ?
(settings.title && settings.title.length > 0 ?
@ -191,14 +192,15 @@ export class TbCanvasDigitalGauge {
hideValue: this.localSettings.hideValue,
hideMinMax: this.localSettings.hideMinMax,
donutStartAngle: this.localSettings.donutStartAngle,
valueDec: this.localSettings.decimals,
neonGlowBrightness: this.localSettings.neonGlowBrightness,
// animations
animation: settings.animation !== false && !ctx.isMobile,
animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null)
? settings.animationDuration : 500,
animationDuration: isDefinedAndNotNull(settings.animationDuration) ? settings.animationDuration : 500,
animationRule: settings.animationRule || 'linear',
isMobile: ctx.isMobile
@ -241,7 +243,7 @@ export class TbCanvasDigitalGauge {
if (findDataKey) {
findDataKey.settings.push(settings);
} else {
datasource.dataKeys.push(dataKey)
datasource.dataKeys.push(dataKey);
}
} else {
const datasourceAttribute: Datasource = {
@ -257,17 +259,23 @@ export class TbCanvasDigitalGauge {
return datasources;
}
private static toRadians(angle: number): number {
return angle * (Math.PI / 180);
}
init() {
if (this.localSettings.useFixedLevelColor) {
if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) {
this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors);
}
let updateSetting = false;
if (this.localSettings.showTicks) {
if (this.localSettings.ticksValue && this.localSettings.ticksValue.length) {
this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue);
}
}
if (this.localSettings.useFixedLevelColor && this.localSettings.fixedLevelColors?.length > 0) {
this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors);
updateSetting = true;
}
if (this.localSettings.showTicks && this.localSettings.ticksValue?.length) {
this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue);
updateSetting = true;
}
if (updateSetting) {
this.updateSetting();
}
}
@ -281,7 +289,7 @@ export class TbCanvasDigitalGauge {
predefineLevelColors.push({
value: levelSetting.value,
color
})
});
} else if (levelSetting.entityAlias && levelSetting.attribute) {
try {
levelColorsDatasource = TbCanvasDigitalGauge.generateDatasource(this.ctx, levelColorsDatasource,
@ -293,7 +301,7 @@ export class TbCanvasDigitalGauge {
}
}
for(const levelColor of options){
for (const levelColor of options) {
if (levelColor.from) {
setLevelColor.call(this, levelColor.from, levelColor.color);
}
@ -313,9 +321,9 @@ export class TbCanvasDigitalGauge {
let ticksDatasource: Datasource[] = [];
const predefineTicks: number[] = [];
for(const tick of options){
for (const tick of options) {
if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) {
predefineTicks.push(tick.value)
predefineTicks.push(tick.value);
} else if (tick.entityAlias && tick.attribute) {
try {
ticksDatasource = TbCanvasDigitalGauge
@ -398,7 +406,7 @@ export class TbCanvasDigitalGauge {
filter.transform(timestamp, this.localSettings.timestampFormat);
}
const value = tvPair[1];
if(value !== this.gauge.value) {
if (value !== this.gauge.value) {
if (!this.gauge.options.animation) {
this.gauge._value = value;
}

4
ui-ngx/src/app/modules/home/models/services.map.ts

@ -31,6 +31,7 @@ import { CustomerService } from '@core/http/customer.service';
import { DashboardService } from '@core/http/dashboard.service';
import { UserService } from '@core/http/user.service';
import { AlarmService } from '@core/http/alarm.service';
import { Router } from '@angular/router';
export const ServicesMap = new Map<string, Type<any>>(
[
@ -49,6 +50,7 @@ export const ServicesMap = new Map<string, Type<any>>(
['date', DatePipe],
['utils', UtilsService],
['translate', TranslateService],
['http', HttpClient]
['http', HttpClient],
['router', Router]
]
);

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

@ -76,6 +76,7 @@ import { TranslateService } from '@ngx-translate/core';
import { PageLink } from '@shared/models/page/page-link';
import { SortOrder } from '@shared/models/page/sort-order';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
export interface IWidgetAction {
name: string;
@ -157,6 +158,7 @@ export class WidgetContext {
translate: TranslateService;
http: HttpClient;
sanitizer: DomSanitizer;
router: Router;
private changeDetectorValue: ChangeDetectorRef;

9
ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts

@ -100,7 +100,7 @@ export abstract class StateControllerComponent implements IStateControllerCompon
this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
const dashboardId = this.route.snapshot.params.dashboardId;
if (this.dashboardId === dashboardId) {
const newState = decodeURIComponent(paramMap.get('state'));
const newState = this.decodeStateParam(paramMap.get('state'));
if (this.currentState !== newState) {
this.currentState = newState;
if (this.inited) {
@ -147,10 +147,15 @@ export abstract class StateControllerComponent implements IStateControllerCompon
}
public reInit() {
this.currentState = decodeURIComponent(this.route.snapshot.queryParamMap.get('state'));
this.preservedState = null;
this.currentState = this.decodeStateParam(this.route.snapshot.queryParamMap.get('state'));
this.init();
}
private decodeStateParam(stateURI: string): string{
return stateURI !== null ? decodeURIComponent(stateURI) : null;
}
protected abstract init();
protected abstract onMobileChanged();

56
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts

@ -0,0 +1,56 @@
///
/// Copyright © 2016-2020 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 { RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
import { DeviceProfilesTableConfigResolver } from './device-profiles-table-config.resolver';
const routes: Routes = [
{
path: 'deviceProfiles',
data: {
breadcrumb: {
label: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'device-profile.device-profiles'
},
resolve: {
entitiesTableConfig: DeviceProfilesTableConfigResolver
}
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
DeviceProfilesTableConfigResolver
]
})
export class DeviceProfileRoutingModule { }

43
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2020 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.
-->
<mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
[entityId]="entity.id"></tb-event-table>
</mat-tab>

38
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2020 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { DeviceProfile } from '@shared/models/device.models';
@Component({
selector: 'tb-device-profile-tabs',
templateUrl: './device-profile-tabs.component.html',
styleUrls: []
})
export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfile> {
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
super.ngOnInit();
}
}

35
ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts

@ -0,0 +1,35 @@
///
/// Copyright © 2016-2020 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 '@shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DeviceProfileTabsComponent } from './device-profile-tabs.component';
import { DeviceProfileRoutingModule } from './device-profile-routing.module';
@NgModule({
declarations: [
DeviceProfileTabsComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
DeviceProfileRoutingModule
]
})
export class DeviceProfileModule { }

125
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -0,0 +1,125 @@
///
/// Copyright © 2016-2020 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 { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import {
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { DialogService } from '@core/services/dialog.service';
import { DeviceProfile, deviceProfileTypeTranslationMap } from '@shared/models/device.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { DeviceProfileComponent } from '../../components/profile/device-profile.component';
import { DeviceProfileTabsComponent } from './device-profile-tabs.component';
@Injectable()
export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> {
private readonly config: EntityTableConfig<DeviceProfile> = new EntityTableConfig<DeviceProfile>();
constructor(private deviceProfileService: DeviceProfileService,
private translate: TranslateService,
private datePipe: DatePipe,
private dialogService: DialogService) {
this.config.entityType = EntityType.DEVICE_PROFILE;
this.config.entityComponent = DeviceProfileComponent;
this.config.entityTabsComponent = DeviceProfileTabsComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE_PROFILE);
this.config.entityResources = entityTypeResources.get(EntityType.DEVICE_PROFILE);
this.config.columns.push(
new DateEntityTableColumn<DeviceProfile>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceProfile>('name', 'device-profile.name', '20%'),
new EntityTableColumn<DeviceProfile>('type', 'device-profile.type', '20%', (deviceProfile) => {
return this.translate.instant(deviceProfileTypeTranslationMap.get(deviceProfile.type));
}),
new EntityTableColumn<DeviceProfile>('description', 'device-profile.description', '60%'),
new EntityTableColumn<DeviceProfile>('isDefault', 'device-profile.default', '60px',
entity => {
return checkBoxCell(entity.default);
})
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('device-profile.set-default'),
icon: 'flag',
isEnabled: (deviceProfile) => !deviceProfile.default,
onAction: ($event, entity) => this.setDefaultDeviceProfile($event, entity)
}
);
this.config.deleteEntityTitle = deviceProfile => this.translate.instant('device-profile.delete-device-profile-title',
{ deviceProfileName: deviceProfile.name });
this.config.deleteEntityContent = () => this.translate.instant('device-profile.delete-device-profile-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('device-profile.delete-device-profiles-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('device-profile.delete-device-profiles-text');
this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink);
this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id);
this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile);
this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id);
this.config.onEntityAction = action => this.onDeviceProfileAction(action);
this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
}
resolve(): EntityTableConfig<DeviceProfile> {
this.config.tableTitle = this.translate.instant('device-profile.device-profiles');
return this.config;
}
setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('device-profile.set-default-device-profile-title', {deviceProfileName: deviceProfile.name}),
this.translate.instant('device-profile.set-default-device-profile-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((res) => {
if (res) {
this.deviceProfileService.setDefaultDeviceProfile(deviceProfile.id.id).subscribe(
() => {
this.config.table.updateData();
}
);
}
}
);
}
onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean {
switch (action.action) {
case 'setDefault':
this.setDefaultDeviceProfile(action.event, action.entity);
return true;
}
return false;
}
}

2
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -32,6 +32,7 @@ import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'
import { TenantProfileModule } from './tenant-profile/tenant-profile.module';
import { MODULES_MAP } from '@shared/public-api';
import { modulesMap } from '../../common/modules-map';
import { DeviceProfileModule } from './device-profile/device-profile.module';
@NgModule({
exports: [
@ -40,6 +41,7 @@ import { modulesMap } from '../../common/modules-map';
ProfileModule,
TenantProfileModule,
TenantModule,
DeviceProfileModule,
DeviceModule,
AssetModule,
EntityViewModule,

3
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html

@ -52,7 +52,8 @@
<tb-tenant-profile-autocomplete
[selectDefaultProfile]="isAdd"
required
formControlName="tenantProfileId">
formControlName="tenantProfileId"
(tenantProfileUpdated)="onTenantProfileUpdated()">
</tb-tenant-profile-autocomplete>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">

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

@ -88,4 +88,7 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
}));
}
onTenantProfileUpdated() {
this.entitiesTableConfig.table.updateData(false);
}
}

6
ui-ngx/src/app/shared/models/ace/service-completion.models.ts

@ -1379,5 +1379,11 @@ export const serviceCompletions: TbEditorCompletions = {
'See <a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a> for API reference.',
meta: 'service',
type: '<a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a>'
},
router: {
description: 'Router Service<br>' +
'See <a href="https://angular.io/api/router/Router">Router</a> for API reference.',
meta: 'service',
type: '<a href="https://angular.io/api/router/Router">Router</a>'
}
};

29
ui-ngx/src/app/shared/models/device.models.ts

@ -25,23 +25,38 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { EntityInfoData } from '@shared/models/entity.models';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT',
LWM2M = 'LWM2M'
DEFAULT = 'DEFAULT'
}
export const deviceProfileTypeTranslationMap = new Map<DeviceProfileType, string>(
[
[DeviceProfileType.DEFAULT, 'device-profile.type-default']
]
);
export interface DefaultDeviceProfileConfiguration {
[key: string]: any;
}
export interface Lwm2mDeviceProfileConfiguration {
[key: string]: any;
}
export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration & Lwm2mDeviceProfileConfiguration;
export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration;
export interface DeviceProfileConfiguration extends DeviceProfileConfigurations {
type: DeviceProfileType;
}
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
let configuration: DeviceProfileConfiguration = null;
if (type) {
switch (type) {
case DeviceProfileType.DEFAULT:
const defaultConfiguration: DefaultDeviceProfileConfiguration = {};
configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT};
break;
}
}
return configuration;
}
export interface DeviceProfileData {
configuration: DeviceProfileConfiguration;
}
@ -50,7 +65,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
isDefault: boolean;
default: boolean;
type: DeviceProfileType;
defaultRuleChainId?: RuleChainId;
profileData: DeviceProfileData;

10
ui-ngx/src/app/shared/models/tenant.model.ts

@ -25,11 +25,11 @@ export interface TenantProfileData {
export interface TenantProfile extends BaseData<TenantProfileId> {
name: string;
description: string;
default: boolean;
isolatedTbCore: boolean;
isolatedTbRuleEngine: boolean;
profileData: TenantProfileData;
description?: string;
default?: boolean;
isolatedTbCore?: boolean;
isolatedTbRuleEngine?: boolean;
profileData?: TenantProfileData;
}
export interface Tenant extends ContactBased<TenantId> {

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

@ -754,10 +754,34 @@
"device-profile": "Device profile",
"device-profiles": "Device profiles",
"add": "Add device profile",
"edit": "Edit device profile",
"device-profile-details": "Device profile details",
"no-device-profiles-text": "No device profiles found",
"search": "Search device profiles",
"selected-device-profiles": "{ count, plural, 1 {1 device profile} other {# device profiles} } selected"
"selected-device-profiles": "{ count, plural, 1 {1 device profile} other {# device profiles} } selected",
"no-device-profiles-matching": "No device profile matching '{{entity}}' were found.",
"device-profile-required": "Device profile is required",
"idCopiedMessage": "Device profile Id has been copied to clipboard",
"set-default": "Make device profile default",
"delete": "Delete device profile",
"copyId": "Copy device profile Id",
"name": "Name",
"name-required": "Name is required.",
"type": "Type",
"type-required": "Type is required.",
"type-default": "Default",
"description": "Description",
"default": "Default",
"profile-configuration": "Profile configuration",
"transport-configuration": "Transport configuration",
"delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?",
"delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.",
"delete-device-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 device profile} other {# device profiles} }?",
"delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data will become unrecoverable.",
"set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?",
"set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.",
"no-device-profiles-found": "No device profiles found.",
"create-new-device-profile": "Create a new one!"
},
"dialog": {
"close": "Close dialog"
@ -1680,6 +1704,7 @@
"tenant-profile": "Tenant profile",
"tenant-profiles": "Tenant profiles",
"add": "Add tenant profile",
"edit": "Edit tenant profile",
"tenant-profile-details": "Tenant profile details",
"no-tenant-profiles-text": "No tenant profiles found",
"search": "Search tenant profiles",
@ -1692,14 +1717,17 @@
"copyId": "Copy tenant profile Id",
"name": "Name",
"name-required": "Name is required.",
"data": "Profile data",
"description": "Description",
"default": "Default",
"delete-tenant-profile-title": "Are you sure you want to delete the tenant profile '{{tenantProfileName}}'?",
"delete-tenant-profile-text": "Be careful, after the confirmation the tenant profile and all related data will become unrecoverable.",
"delete-tenant-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 tenant profile} other {# tenant profiles} }?",
"delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.",
"set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' root?",
"set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified."
"set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' default?",
"set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified.",
"no-tenant-profiles-found": "No tenant profiles found.",
"create-new-tenant-profile": "Create a new one!"
},
"timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",

Loading…
Cancel
Save