123 changed files with 7655 additions and 1167 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.mail; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.common.util.AbstractListeningExecutor; |
||||
|
|
||||
|
/** |
||||
|
* Executor have the sole purpose to send mails. It should be used only by Mail Service. |
||||
|
* For other purposes please use the MailExecutorService component |
||||
|
* */ |
||||
|
@Component |
||||
|
public class MailSenderInternalExecutorService extends AbstractListeningExecutor { |
||||
|
|
||||
|
@Value("${actors.rule.mail_thread_pool_size}") |
||||
|
private int mailExecutorThreadPoolSize; |
||||
|
|
||||
|
@Override |
||||
|
protected int getThreadPollSize() { |
||||
|
return mailExecutorThreadPoolSize; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,322 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.queue.consumer; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.Getter; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
||||
|
import org.thingsboard.server.common.data.queue.QueueConfig; |
||||
|
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.queue.TbQueueConsumer; |
||||
|
import org.thingsboard.server.queue.TbQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.QueueKey; |
||||
|
import org.thingsboard.server.service.queue.ruleengine.QueueEvent; |
||||
|
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask; |
||||
|
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask; |
||||
|
|
||||
|
import java.util.Collection; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ConcurrentLinkedQueue; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Future; |
||||
|
import java.util.concurrent.ScheduledExecutorService; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||
|
import java.util.function.Function; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfig> { |
||||
|
|
||||
|
protected final QueueKey queueKey; |
||||
|
@Getter |
||||
|
protected C config; |
||||
|
protected final MsgPackProcessor<M, C> msgPackProcessor; |
||||
|
protected final Function<C, TbQueueConsumer<M>> consumerCreator; |
||||
|
protected final ExecutorService consumerExecutor; |
||||
|
protected final ScheduledExecutorService scheduler; |
||||
|
protected final ExecutorService taskExecutor; |
||||
|
|
||||
|
private final java.util.Queue<TbQueueConsumerManagerTask> tasks = new ConcurrentLinkedQueue<>(); |
||||
|
private final ReentrantLock lock = new ReentrantLock(); |
||||
|
|
||||
|
@Getter |
||||
|
private volatile Set<TopicPartitionInfo> partitions; |
||||
|
protected volatile ConsumerWrapper<M> consumerWrapper; |
||||
|
protected volatile boolean stopped; |
||||
|
|
||||
|
@Builder |
||||
|
public MainQueueConsumerManager(QueueKey queueKey, C config, |
||||
|
MsgPackProcessor<M, C> msgPackProcessor, |
||||
|
Function<C, TbQueueConsumer<M>> consumerCreator, |
||||
|
ExecutorService consumerExecutor, |
||||
|
ScheduledExecutorService scheduler, |
||||
|
ExecutorService taskExecutor) { |
||||
|
this.queueKey = queueKey; |
||||
|
this.config = config; |
||||
|
this.msgPackProcessor = msgPackProcessor; |
||||
|
this.consumerCreator = consumerCreator; |
||||
|
this.consumerExecutor = consumerExecutor; |
||||
|
this.scheduler = scheduler; |
||||
|
this.taskExecutor = taskExecutor; |
||||
|
if (config != null) { |
||||
|
init(config); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void init(C config) { |
||||
|
this.config = config; |
||||
|
if (config.isConsumerPerPartition()) { |
||||
|
this.consumerWrapper = new ConsumerPerPartitionWrapper(); |
||||
|
} else { |
||||
|
this.consumerWrapper = new SingleConsumerWrapper(); |
||||
|
} |
||||
|
log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); |
||||
|
} |
||||
|
|
||||
|
public void update(C config) { |
||||
|
addTask(TbQueueConsumerManagerTask.configUpdate(config)); |
||||
|
} |
||||
|
|
||||
|
public void update(Set<TopicPartitionInfo> partitions) { |
||||
|
addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); |
||||
|
} |
||||
|
|
||||
|
protected void addTask(TbQueueConsumerManagerTask todo) { |
||||
|
if (stopped) { |
||||
|
return; |
||||
|
} |
||||
|
tasks.add(todo); |
||||
|
log.trace("[{}] Added task: {}", queueKey, todo); |
||||
|
tryProcessTasks(); |
||||
|
} |
||||
|
|
||||
|
private void tryProcessTasks() { |
||||
|
taskExecutor.submit(() -> { |
||||
|
if (lock.tryLock()) { |
||||
|
try { |
||||
|
C newConfig = null; |
||||
|
Set<TopicPartitionInfo> newPartitions = null; |
||||
|
while (!stopped) { |
||||
|
TbQueueConsumerManagerTask task = tasks.poll(); |
||||
|
if (task == null) { |
||||
|
break; |
||||
|
} |
||||
|
log.trace("[{}] Processing task: {}", queueKey, task); |
||||
|
|
||||
|
if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { |
||||
|
newPartitions = task.getPartitions(); |
||||
|
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { |
||||
|
newConfig = (C) task.getConfig(); |
||||
|
} else { |
||||
|
processTask(task); |
||||
|
} |
||||
|
} |
||||
|
if (stopped) { |
||||
|
return; |
||||
|
} |
||||
|
if (newConfig != null) { |
||||
|
doUpdate(newConfig); |
||||
|
} |
||||
|
if (newPartitions != null) { |
||||
|
doUpdate(newPartitions); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.error("[{}] Failed to process tasks", queueKey, e); |
||||
|
} finally { |
||||
|
lock.unlock(); |
||||
|
} |
||||
|
} else { |
||||
|
log.trace("[{}] Failed to acquire lock", queueKey); |
||||
|
scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected void processTask(TbQueueConsumerManagerTask task) { |
||||
|
} |
||||
|
|
||||
|
private void doUpdate(C newConfig) { |
||||
|
log.info("[{}] Processing queue update: {}", queueKey, newConfig); |
||||
|
var oldConfig = this.config; |
||||
|
this.config = newConfig; |
||||
|
if (log.isTraceEnabled()) { |
||||
|
log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig); |
||||
|
log.trace("[{}] New queue configuration: {}", queueKey, newConfig); |
||||
|
} |
||||
|
|
||||
|
if (oldConfig == null) { |
||||
|
init(config); |
||||
|
} else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) { |
||||
|
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); |
||||
|
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); |
||||
|
|
||||
|
init(config); |
||||
|
if (partitions != null) { |
||||
|
doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
|
||||
|
} |
||||
|
} else { |
||||
|
log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey); |
||||
|
// do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
|
||||
|
// and changes to other config values will be picked up by consumer on the fly,
|
||||
|
// and queue topic and name are immutable
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void doUpdate(Set<TopicPartitionInfo> partitions) { |
||||
|
this.partitions = partitions; |
||||
|
consumerWrapper.updatePartitions(partitions); |
||||
|
} |
||||
|
|
||||
|
private void launchConsumer(TbQueueConsumerTask<M> consumerTask) { |
||||
|
log.info("[{}] Launching consumer", consumerTask.getKey()); |
||||
|
Future<?> consumerLoop = consumerExecutor.submit(() -> { |
||||
|
ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); |
||||
|
try { |
||||
|
consumerLoop(consumerTask.getConsumer()); |
||||
|
} catch (Throwable e) { |
||||
|
log.error("Failure in consumer loop", e); |
||||
|
} |
||||
|
log.info("[{}] Consumer stopped", consumerTask.getKey()); |
||||
|
}); |
||||
|
consumerTask.setTask(consumerLoop); |
||||
|
} |
||||
|
|
||||
|
private void consumerLoop(TbQueueConsumer<M> consumer) { |
||||
|
while (!stopped && !consumer.isStopped()) { |
||||
|
try { |
||||
|
List<M> msgs = consumer.poll(config.getPollInterval()); |
||||
|
if (msgs.isEmpty()) { |
||||
|
continue; |
||||
|
} |
||||
|
processMsgs(msgs, consumer, config); |
||||
|
} catch (Exception e) { |
||||
|
if (!consumer.isStopped()) { |
||||
|
log.warn("Failed to process messages from queue", e); |
||||
|
try { |
||||
|
Thread.sleep(config.getPollInterval()); |
||||
|
} catch (InterruptedException e2) { |
||||
|
log.trace("Failed to wait until the server has capacity to handle new requests", e2); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (consumer.isStopped()) { |
||||
|
consumer.unsubscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void processMsgs(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception { |
||||
|
msgPackProcessor.process(msgs, consumer, config); |
||||
|
} |
||||
|
|
||||
|
public void stop() { |
||||
|
log.debug("[{}] Stopping consumers", queueKey); |
||||
|
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); |
||||
|
stopped = true; |
||||
|
} |
||||
|
|
||||
|
public void awaitStop() { |
||||
|
log.debug("[{}] Waiting for consumers to stop", queueKey); |
||||
|
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); |
||||
|
log.debug("[{}] Unsubscribed and stopped consumers", queueKey); |
||||
|
} |
||||
|
|
||||
|
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) { |
||||
|
return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); |
||||
|
} |
||||
|
|
||||
|
public interface MsgPackProcessor<M extends TbQueueMsg, C extends QueueConfig> { |
||||
|
void process(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception; |
||||
|
} |
||||
|
|
||||
|
public interface ConsumerWrapper<M extends TbQueueMsg> { |
||||
|
|
||||
|
void updatePartitions(Set<TopicPartitionInfo> partitions); |
||||
|
|
||||
|
Collection<TbQueueConsumerTask<M>> getConsumers(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
class ConsumerPerPartitionWrapper implements ConsumerWrapper<M> { |
||||
|
private final Map<TopicPartitionInfo, TbQueueConsumerTask<M>> consumers = new HashMap<>(); |
||||
|
|
||||
|
@Override |
||||
|
public void updatePartitions(Set<TopicPartitionInfo> partitions) { |
||||
|
Set<TopicPartitionInfo> addedPartitions = new HashSet<>(partitions); |
||||
|
addedPartitions.removeAll(consumers.keySet()); |
||||
|
|
||||
|
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(consumers.keySet()); |
||||
|
removedPartitions.removeAll(partitions); |
||||
|
log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); |
||||
|
|
||||
|
removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); |
||||
|
removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); |
||||
|
|
||||
|
addedPartitions.forEach((tpi) -> { |
||||
|
String key = queueKey + "-" + tpi.getPartition().orElse(-1); |
||||
|
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); |
||||
|
consumers.put(tpi, consumer); |
||||
|
consumer.subscribe(Set.of(tpi)); |
||||
|
launchConsumer(consumer); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Collection<TbQueueConsumerTask<M>> getConsumers() { |
||||
|
return consumers.values(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class SingleConsumerWrapper implements ConsumerWrapper<M> { |
||||
|
private TbQueueConsumerTask<M> consumer; |
||||
|
|
||||
|
@Override |
||||
|
public void updatePartitions(Set<TopicPartitionInfo> partitions) { |
||||
|
log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); |
||||
|
if (partitions.isEmpty()) { |
||||
|
if (consumer != null && consumer.isRunning()) { |
||||
|
consumer.initiateStop(); |
||||
|
consumer.awaitCompletion(); |
||||
|
} |
||||
|
consumer = null; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (consumer == null) { |
||||
|
consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config)); |
||||
|
} |
||||
|
consumer.subscribe(partitions); |
||||
|
if (!consumer.isRunning()) { |
||||
|
launchConsumer(consumer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Collection<TbQueueConsumerTask<M>> getConsumers() { |
||||
|
if (consumer == null) { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
return List.of(consumer); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.exception; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.limit.LimitedApi; |
||||
|
|
||||
|
public class RateLimitExceededException extends AbstractRateLimitException { |
||||
|
|
||||
|
public RateLimitExceededException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public RateLimitExceededException(LimitedApi api) { |
||||
|
super("Rate limit for " + api.getLabel() + " is exceeded"); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.queue; |
||||
|
|
||||
|
public interface QueueConfig { |
||||
|
|
||||
|
boolean isConsumerPerPartition(); |
||||
|
|
||||
|
int getPollInterval(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,299 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.rule.engine.flow; |
||||
|
|
||||
|
import org.assertj.core.api.Assertions; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||
|
import org.junit.jupiter.params.provider.Arguments; |
||||
|
import org.junit.jupiter.params.provider.MethodSource; |
||||
|
import org.junit.jupiter.params.provider.ValueSource; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||
|
import org.springframework.test.util.ReflectionTestUtils; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; |
||||
|
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; |
||||
|
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.DeviceProfile; |
||||
|
import org.thingsboard.server.common.data.asset.AssetProfile; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.msg.TbMsgType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.assertj.core.api.Assertions.assertThatCode; |
||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.any; |
||||
|
import static org.mockito.Mockito.spy; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbRuleChainInputNodeTest extends AbstractRuleNodeUpgradeTest { |
||||
|
|
||||
|
private final TenantId TENANT_ID = new TenantId(UUID.fromString("4ba69ea5-6b27-42df-ab66-e7a727a67027")); |
||||
|
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("97731954-2147-4176-8f1a-d14f1b73e4e6")); |
||||
|
private final AssetId ASSET_ID = new AssetId(UUID.fromString("841a47bd-4e8e-4ea5-88e6-420da0d70e51")); |
||||
|
|
||||
|
private TbRuleChainInputNode node; |
||||
|
private TbRuleChainInputNodeConfiguration config; |
||||
|
private TbNodeConfiguration nodeConfiguration; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private RuleEngineDeviceProfileCache deviceProfileCacheMock; |
||||
|
@Mock |
||||
|
private RuleEngineAssetProfileCache assetProfileCacheMock; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
node = spy(new TbRuleChainInputNode()); |
||||
|
config = new TbRuleChainInputNodeConfiguration().defaultConfiguration(); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource |
||||
|
public void givenValidConfig_whenInit_thenOk(String ruleChainIdStr, boolean forwardMsgToDefaultRuleChain) throws TbNodeException { |
||||
|
//GIVEN
|
||||
|
config.setRuleChainId(ruleChainIdStr); |
||||
|
config.setForwardMsgToDefaultRuleChain(forwardMsgToDefaultRuleChain); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
//WHEN
|
||||
|
assertThatCode(() -> node.init(ctxMock, nodeConfiguration)) |
||||
|
.doesNotThrowAnyException(); |
||||
|
|
||||
|
//THEN
|
||||
|
verify(ctxMock).checkTenantEntity(new RuleChainId(UUID.fromString(ruleChainIdStr))); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenValidConfig_whenInit_thenOk() { |
||||
|
return Stream.of( |
||||
|
Arguments.of("45bba7c4-04bf-419b-ae03-6ceb9724f10e", false), |
||||
|
Arguments.of("52d57e1b-70bb-480e-bcc4-6710e1dcc9d8", true) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@ValueSource(strings = {"91acbce0-079fdb", "", " ", "my test string"}) |
||||
|
public void givenInvalidRuleChainId_whenInit_thenThrowsException(String ruleChainIdStr) { |
||||
|
//GIVEN
|
||||
|
config.setRuleChainId(ruleChainIdStr); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
//WHEN-THEN
|
||||
|
Assertions.assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) |
||||
|
.isInstanceOf(TbNodeException.class) |
||||
|
.hasMessage("Failed to parse rule chain id: " + ruleChainIdStr); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRuleChainIdIsNotSet_whenInit_thenThrowsException() { |
||||
|
assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) |
||||
|
.isInstanceOf(TbNodeException.class) |
||||
|
.hasMessage("Rule chain must be set!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToDeviceDefaultRuleChain() throws TbNodeException { |
||||
|
//GIVEN
|
||||
|
DeviceProfile deviceProfile = new DeviceProfile(); |
||||
|
RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("196e3cd5-68b8-421e-a0cf-1d44fa377cdf")); |
||||
|
deviceProfile.setDefaultRuleChainId(defaultRuleChainId); |
||||
|
|
||||
|
TbMsg msg = getMsg(DEVICE_ID); |
||||
|
|
||||
|
String ruleChainIdFromConfigStr = "acbc924f-7f95-4a9b-a854-e4822deb74c7"; |
||||
|
config.setRuleChainId(ruleChainIdFromConfigStr); |
||||
|
config.setForwardMsgToDefaultRuleChain(true); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock); |
||||
|
when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); |
||||
|
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
//WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
//THEN
|
||||
|
ArgumentCaptor<RuleChainId> ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); |
||||
|
verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); |
||||
|
RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue(); |
||||
|
assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId); |
||||
|
|
||||
|
RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId"); |
||||
|
assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr))); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToAssetDefaultRuleChain() throws TbNodeException { |
||||
|
//GIVEN
|
||||
|
AssetProfile assetProfile = new AssetProfile(); |
||||
|
RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("f0a3cd58-980c-4730-a40c-8f59064d2065")); |
||||
|
assetProfile.setDefaultRuleChainId(defaultRuleChainId); |
||||
|
|
||||
|
TbMsg msg = getMsg(ASSET_ID); |
||||
|
|
||||
|
String ruleChainIdFromConfigStr = "56f1c0b8-1a00-4ce0-b3ab-a1416d7cc429"; |
||||
|
config.setRuleChainId(ruleChainIdFromConfigStr); |
||||
|
config.setForwardMsgToDefaultRuleChain(true); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock); |
||||
|
when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile); |
||||
|
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
//WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
//THEN
|
||||
|
ArgumentCaptor<RuleChainId> ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); |
||||
|
verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); |
||||
|
RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue(); |
||||
|
assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId); |
||||
|
|
||||
|
RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId"); |
||||
|
assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr))); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenForwardMsgToDefaultIsTrueWithoutDeviceDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { |
||||
|
//GIVEN
|
||||
|
DeviceProfile deviceProfile = new DeviceProfile(); |
||||
|
|
||||
|
TbMsg msg = getMsg(DEVICE_ID); |
||||
|
|
||||
|
String ruleChainIdFromConfigStr = "357c2785-e7cc-46a8-9797-957180dabdeb"; |
||||
|
RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); |
||||
|
config.setRuleChainId(ruleChainIdFromConfigStr); |
||||
|
config.setForwardMsgToDefaultRuleChain(true); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock); |
||||
|
when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); |
||||
|
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
//WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
//THEN
|
||||
|
ArgumentCaptor<RuleChainId> ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); |
||||
|
verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); |
||||
|
assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenForwardMsgToDefaultIsTrueWithoutAssetDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { |
||||
|
//GIVEN
|
||||
|
AssetProfile assetProfile = new AssetProfile(); |
||||
|
|
||||
|
TbMsg msg = getMsg(ASSET_ID); |
||||
|
|
||||
|
String ruleChainIdFromConfigStr = "12883c3d-c10b-4d5b-b606-a59385a920bc"; |
||||
|
RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); |
||||
|
config.setRuleChainId(ruleChainIdFromConfigStr); |
||||
|
config.setForwardMsgToDefaultRuleChain(true); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock); |
||||
|
when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile); |
||||
|
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
//WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
//THEN
|
||||
|
ArgumentCaptor<RuleChainId> ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); |
||||
|
verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); |
||||
|
assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRuleChainInConfig_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { |
||||
|
//GIVEN
|
||||
|
String ruleChainIdFromConfigStr = "3c02c8b3-645c-4e67-aac5-f984f59471d1"; |
||||
|
RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); |
||||
|
|
||||
|
TbMsg msg = getMsg(DEVICE_ID); |
||||
|
|
||||
|
config.setRuleChainId(ruleChainIdFromConfigStr); |
||||
|
config.setForwardMsgToDefaultRuleChain(false); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
//WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
//THEN
|
||||
|
ArgumentCaptor<RuleChainId> ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); |
||||
|
verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); |
||||
|
assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { |
||||
|
return Stream.of( |
||||
|
//config for version 0
|
||||
|
Arguments.of(0, |
||||
|
"{\"ruleChainId\": null}", |
||||
|
true, |
||||
|
"{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}" |
||||
|
), |
||||
|
//config for version 1 with upgrade from version 0
|
||||
|
Arguments.of(1, |
||||
|
"{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}", |
||||
|
false, |
||||
|
"{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}" |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected TbNode getTestNode() { |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
private TbMsg getMsg(EntityId entityId) { |
||||
|
return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-form-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup"> |
||||
|
<div class="tb-flex row space-between align-center no-gap fill-width"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.security</div> |
||||
|
<tb-toggle-select formControlName="type" appearance="fill"> |
||||
|
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type"> |
||||
|
{{ SecurityTypeTranslationsMap.get(type) | translate }} |
||||
|
</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<ng-container [ngSwitch]="securityFormGroup.get('type').value"> |
||||
|
<ng-template [ngSwitchCase]="BrokerSecurityType.BASIC"> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.username</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.username-required') | translate" |
||||
|
*ngIf="securityFormGroup.get('username').hasError('required') |
||||
|
&& securityFormGroup.get('username').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.password</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<div class="tb-flex no-gap align-center fill-height" matSuffix> |
||||
|
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="BrokerSecurityType.CERTIFICATES"> |
||||
|
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.path-hint' | translate }}</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.CA-certificate-path</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="pathToCACert" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.private-key-path</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="pathToPrivateKey" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.client-cert-path</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="pathToClientCert" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</div> |
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: block; |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
NgZone, |
||||
|
OnDestroy, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { Overlay } from '@angular/cdk/overlay'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
BrokerSecurityType, |
||||
|
BrokerSecurityTypeTranslationsMap, |
||||
|
noLeadTrailSpacesRegex |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-broker-security', |
||||
|
templateUrl: './broker-security.component.html', |
||||
|
styleUrls: ['./broker-security.component.scss'], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => BrokerSecurityComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => BrokerSecurityComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy { |
||||
|
|
||||
|
BrokerSecurityType = BrokerSecurityType; |
||||
|
|
||||
|
securityTypes = Object.values(BrokerSecurityType); |
||||
|
|
||||
|
SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap; |
||||
|
|
||||
|
securityFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = (v: any) => {}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
private overlay: Overlay, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private dialogService: DialogService, |
||||
|
private entityService: EntityService, |
||||
|
private utils: UtilsService, |
||||
|
private zone: NgZone, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private elementRef: ElementRef, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store); |
||||
|
this.securityFormGroup = this.fb.group({ |
||||
|
type: [BrokerSecurityType.ANONYMOUS, []], |
||||
|
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]] |
||||
|
}); |
||||
|
this.securityFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
this.updateView(value); |
||||
|
}); |
||||
|
this.securityFormGroup.get('type').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((type) => { |
||||
|
this.updateValidators(type); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
super.ngOnDestroy(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void {} |
||||
|
|
||||
|
writeValue(deviceInfo: any) { |
||||
|
if (!deviceInfo.type) { |
||||
|
deviceInfo.type = BrokerSecurityType.ANONYMOUS; |
||||
|
} |
||||
|
this.securityFormGroup.reset(deviceInfo); |
||||
|
this.updateView(deviceInfo); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.securityFormGroup.valid ? null : { |
||||
|
securityForm: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
updateView(value: any) { |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
|
||||
|
private updateValidators(type) { |
||||
|
if (type) { |
||||
|
this.securityFormGroup.get('username').disable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('password').disable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('pathToCACert').disable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false}); |
||||
|
if (type === BrokerSecurityType.BASIC) { |
||||
|
this.securityFormGroup.get('username').enable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('password').enable({emitEvent: false}); |
||||
|
} else if (type === BrokerSecurityType.CERTIFICATES) { |
||||
|
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-form-panel stroked" [formGroup]="mappingFormGroup"> |
||||
|
<div class="tb-form-panel-title" [class.tb-required]="required" translate>device.device</div> |
||||
|
<div class="tb-form-table no-padding no-gap"> |
||||
|
<div class="tb-form-table-header"> |
||||
|
<div class="tb-form-table-header-cell table-name-column" translate>gateway.device-info.entity-field</div> |
||||
|
<div *ngIf="useSource" class="tb-form-table-header-cell table-column" translate>gateway.device-info.source</div> |
||||
|
<div class="tb-form-table-header-cell table-column" translate> |
||||
|
gateway.device-info.expression |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-table-body no-gap"> |
||||
|
<div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding" |
||||
|
[class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL"> |
||||
|
<div class="fixed-title-width" translate>gateway.device-info.name</div> |
||||
|
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="deviceNameExpressionSource"> |
||||
|
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
||||
|
{{ SourceTypeTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-table-row-cell tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate" |
||||
|
*ngIf="mappingFormGroup.get('deviceNameExpression').hasError('required') && |
||||
|
mappingFormGroup.get('deviceNameExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding" |
||||
|
*ngIf="deviceInfoType === DeviceInfoType.FULL"> |
||||
|
<div class="fixed-title-width" translate>gateway.device-info.profile-name</div> |
||||
|
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="deviceProfileExpressionSource"> |
||||
|
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
||||
|
{{ SourceTypeTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-table-row-cell tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceProfileExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-info.device-profile-expression-required') | translate" |
||||
|
*ngIf="mappingFormGroup.get('deviceProfileExpression').hasError('required') && |
||||
|
mappingFormGroup.get('deviceProfileExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,57 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: block; |
||||
|
|
||||
|
.tb-form-row { |
||||
|
&.bottom-same-padding { |
||||
|
padding-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
&.top-same-padding { |
||||
|
padding-top: 16px; |
||||
|
} |
||||
|
|
||||
|
.fixed-title-width { |
||||
|
width: 19%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.table-column { |
||||
|
width: 40%; |
||||
|
} |
||||
|
|
||||
|
.table-name-column { |
||||
|
width: 20%; |
||||
|
} |
||||
|
|
||||
|
.raw-name { |
||||
|
width: 19%; |
||||
|
} |
||||
|
|
||||
|
.raw-value-option { |
||||
|
max-width: 40%; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-mdc-form-field-icon-suffix { |
||||
|
display: flex; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,179 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
NgZone, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { Overlay } from '@angular/cdk/overlay'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
DeviceInfoType, |
||||
|
noLeadTrailSpacesRegex, |
||||
|
SourceTypes, |
||||
|
SourceTypeTranslationsMap |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-device-info-table', |
||||
|
templateUrl: './device-info-table.component.html', |
||||
|
styleUrls: ['./device-info-table.component.scss'], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DeviceInfoTableComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DeviceInfoTableComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { |
||||
|
|
||||
|
SourceTypeTranslationsMap = SourceTypeTranslationsMap; |
||||
|
|
||||
|
DeviceInfoType = DeviceInfoType; |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() |
||||
|
useSource = true; |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() |
||||
|
required = false; |
||||
|
|
||||
|
@Input() |
||||
|
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes); |
||||
|
|
||||
|
deviceInfoTypeValue: any; |
||||
|
|
||||
|
get deviceInfoType(): any { |
||||
|
return this.deviceInfoTypeValue; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set deviceInfoType(value: any) { |
||||
|
if (this.deviceInfoTypeValue !== value) { |
||||
|
this.deviceInfoTypeValue = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
mappingFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = (v: any) => {}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
private overlay: Overlay, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private dialogService: DialogService, |
||||
|
private entityService: EntityService, |
||||
|
private utils: UtilsService, |
||||
|
private zone: NgZone, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private elementRef: ElementRef, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.mappingFormGroup = this.fb.group({ |
||||
|
deviceNameExpression: ['', this.required ? |
||||
|
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]] |
||||
|
}); |
||||
|
|
||||
|
if (this.useSource) { |
||||
|
this.mappingFormGroup.addControl('deviceNameExpressionSource', |
||||
|
this.fb.control(SourceTypes.MSG, [])); |
||||
|
} |
||||
|
|
||||
|
if (this.deviceInfoType === DeviceInfoType.FULL) { |
||||
|
if (this.useSource) { |
||||
|
this.mappingFormGroup.addControl('deviceProfileExpressionSource', |
||||
|
this.fb.control(SourceTypes.MSG, [])); |
||||
|
} |
||||
|
this.mappingFormGroup.addControl('deviceProfileExpression', |
||||
|
this.fb.control('', this.required ? |
||||
|
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)])); |
||||
|
} |
||||
|
|
||||
|
this.mappingFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
this.updateView(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void {} |
||||
|
|
||||
|
writeValue(deviceInfo: any) { |
||||
|
this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.mappingFormGroup.valid ? null : { |
||||
|
mappingForm: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
updateView(value: any) { |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,128 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Directive, |
||||
|
ElementRef, |
||||
|
Inject, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
Renderer2 |
||||
|
} from '@angular/core'; |
||||
|
import { isEqual } from '@core/utils'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { WINDOW } from '@core/services/window.service'; |
||||
|
import { fromEvent, Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Directive({ |
||||
|
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||
|
selector: '[tb-ellipsis-chip-list]' |
||||
|
}) |
||||
|
export class EllipsisChipListDirective implements OnDestroy { |
||||
|
|
||||
|
chipsValue: string[]; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
@Input('tb-ellipsis-chip-list') |
||||
|
set chips(value: string[]) { |
||||
|
if (!isEqual(this.chipsValue, value)) { |
||||
|
this.chipsValue = value; |
||||
|
setTimeout(() => { |
||||
|
this.adjustChips(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constructor(private el: ElementRef, |
||||
|
private renderer: Renderer2, |
||||
|
private translate: TranslateService, |
||||
|
@Inject(WINDOW) private window: Window) { |
||||
|
this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px'); |
||||
|
this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto'); |
||||
|
fromEvent(window, 'resize').pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(() => { |
||||
|
this.adjustChips(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private adjustChips(): void { |
||||
|
const chipListElement = this.el.nativeElement; |
||||
|
const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip'); |
||||
|
const margin = parseFloat(this.window.getComputedStyle(ellipsisChip).marginLeft) || 0; |
||||
|
const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)'); |
||||
|
|
||||
|
if (this.chipsValue.length > 1) { |
||||
|
const ellipsisText = this.el.nativeElement.querySelector('.ellipsis-text'); |
||||
|
this.renderer.setStyle(ellipsisChip, 'display', 'inline-flex'); |
||||
|
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', |
||||
|
{count: (this.chipsValue.length)}); |
||||
|
|
||||
|
const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin); |
||||
|
let usedWidth = 0; |
||||
|
let visibleChipsCount = 0; |
||||
|
|
||||
|
chipNodes.forEach((chip) => { |
||||
|
this.renderer.setStyle(chip, 'display', 'inline-flex'); |
||||
|
const textLabelContainer = chip.querySelector('.mdc-evolution-chip__text-label'); |
||||
|
|
||||
|
this.applyMaxChipTextWidth(textLabelContainer, (availableWidth / 3)); |
||||
|
|
||||
|
if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) { |
||||
|
visibleChipsCount++; |
||||
|
usedWidth += chip.offsetWidth + margin; |
||||
|
} else { |
||||
|
this.renderer.setStyle(chip, 'display', 'none'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', |
||||
|
{count: (this.chipsValue.length - visibleChipsCount)}); |
||||
|
|
||||
|
if (visibleChipsCount === this.chipsValue?.length) { |
||||
|
this.renderer.setStyle(ellipsisChip, 'display', 'none'); |
||||
|
} |
||||
|
} else if (this.chipsValue.length === 1) { |
||||
|
const chipLabelContainer = chipNodes[0].querySelector('.mdc-evolution-chip__action'); |
||||
|
const textLabelContainer = chipLabelContainer.querySelector('.mdc-evolution-chip__text-label'); |
||||
|
const leftPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingLeft) || 0; |
||||
|
const rightPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingRight) || 0; |
||||
|
const computedTextWidth = chipListElement.offsetWidth - margin - |
||||
|
(leftPadding + rightPadding); |
||||
|
|
||||
|
this.renderer.setStyle(ellipsisChip, 'display', 'none'); |
||||
|
this.renderer.setStyle(chipNodes[0], 'display', 'inline-flex'); |
||||
|
|
||||
|
this.applyMaxChipTextWidth(textLabelContainer, computedTextWidth); |
||||
|
} else { |
||||
|
this.renderer.setStyle(ellipsisChip, 'display', 'none'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private applyMaxChipTextWidth(element: HTMLElement, widthLimit: number): void { |
||||
|
this.renderer.setStyle(element, 'max-width', widthLimit + 'px'); |
||||
|
this.renderer.setStyle(element, 'overflow', 'hidden'); |
||||
|
this.renderer.setStyle(element, 'text-overflow', 'ellipsis'); |
||||
|
this.renderer.setStyle(element, 'white-space', 'nowrap'); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,195 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-mapping-keys-panel"> |
||||
|
<div class="tb-form-panel no-border no-padding"> |
||||
|
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div> |
||||
|
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys"> |
||||
|
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width" |
||||
|
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;"> |
||||
|
<div class="tb-form-panel stroked tb-flex"> |
||||
|
<ng-container [formGroup]="keyControl"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="last"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<div class="title-container">{{ keyControl.get('key').value }}</div> |
||||
|
{{ '-' }} |
||||
|
<div class="title-container">{{ valueTitle(keyControl.get('value').value) }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-panel no-border no-padding" |
||||
|
*ngIf="keysType !== MappingKeysType.CUSTOM; else customPanel"> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.platform-side</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" |
||||
|
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate> |
||||
|
gateway.key |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.key-required') | translate" |
||||
|
*ngIf="keyControl.get('key').hasError('required') && |
||||
|
keyControl.get('key').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.connector-side</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.type</div> |
||||
|
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select name="valueType" formControlName="type"> |
||||
|
<mat-select-trigger *ngIf="!rawData"> |
||||
|
<div class="tb-flex align-center"> |
||||
|
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon"> |
||||
|
</mat-icon> |
||||
|
<span> |
||||
|
{{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} |
||||
|
</span> |
||||
|
</div> |
||||
|
</mat-select-trigger> |
||||
|
<ng-container *ngIf="!rawData; else rawOption"> |
||||
|
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType"> |
||||
|
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}"> |
||||
|
</mat-icon> |
||||
|
<span>{{ valueTypes.get(valueType).name | translate }}</span> |
||||
|
</mat-option> |
||||
|
</ng-container> |
||||
|
<ng-template #rawOption> |
||||
|
<mat-option [value]="'raw'"> |
||||
|
<span>{{ 'gateway.raw' | translate }}</span> |
||||
|
</mat-option> |
||||
|
</ng-template> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" |
||||
|
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate> |
||||
|
gateway.value |
||||
|
</div> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap"> |
||||
|
<input matInput required formControlName="value" |
||||
|
placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.value-required') | translate" |
||||
|
*ngIf="keyControl.get('value').hasError('required') && |
||||
|
keyControl.get('value').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #customPanel> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.key</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.key-required') | translate" |
||||
|
*ngIf="keyControl.get('key').hasError('required') && keyControl.get('key').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.value</div> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute"> |
||||
|
<input matInput required formControlName="value" |
||||
|
placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.value-required') | translate" |
||||
|
*ngIf="keyControl.get('value').hasError('required') && keyControl.get('value').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
(click)="deleteKey($event, $index)" |
||||
|
[matTooltip]="deleteKeyTitle | translate" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
||||
|
{{ addKeyTitle | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #noKeys> |
||||
|
<div class="tb-flex no-flex center align-center key-panel"> |
||||
|
<span class="tb-prompt" translate>{{ noKeysText }}</span> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<div class="tb-flex flex-end"> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyKeysData()" |
||||
|
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,60 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
.tb-mapping-keys-panel { |
||||
|
width: 77vw; |
||||
|
max-width: 700px; |
||||
|
|
||||
|
.title-container { |
||||
|
max-width: 11vw; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap |
||||
|
} |
||||
|
|
||||
|
.key-panel { |
||||
|
height: 500px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
tb-value-input { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.tb-form-panel { |
||||
|
.mat-mdc-icon-button { |
||||
|
width: 56px; |
||||
|
height: 56px; |
||||
|
padding: 16px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.see-example { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
margin: 4px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-mdc-form-field-icon-suffix { |
||||
|
display: flex; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,173 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
EventEmitter, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Output |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { |
||||
|
MappingDataKey, |
||||
|
MappingKeysType, |
||||
|
MappingValueType, |
||||
|
mappingValueTypesMap, |
||||
|
noLeadTrailSpacesRegex |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mapping-data-keys-panel', |
||||
|
templateUrl: './mapping-data-keys-panel.component.html', |
||||
|
styleUrls: ['./mapping-data-keys-panel.component.scss'], |
||||
|
providers: [] |
||||
|
}) |
||||
|
export class MappingDataKeysPanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
panelTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
addKeyTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
deleteKeyTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
noKeysText: string; |
||||
|
|
||||
|
@Input() |
||||
|
keys: Array<MappingDataKey> | {[key: string]: any}; |
||||
|
|
||||
|
@Input() |
||||
|
keysType: string; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
rawData = false; |
||||
|
|
||||
|
@Input() |
||||
|
popover: TbPopoverComponent<MappingDataKeysPanelComponent>; |
||||
|
|
||||
|
@Output() |
||||
|
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: any}>(); |
||||
|
|
||||
|
valueTypeKeys = Object.values(MappingValueType); |
||||
|
|
||||
|
MappingKeysType = MappingKeysType; |
||||
|
|
||||
|
valueTypeEnum = MappingValueType; |
||||
|
|
||||
|
valueTypes = mappingValueTypesMap; |
||||
|
|
||||
|
dataKeyType: DataKeyType; |
||||
|
|
||||
|
keysListFormArray: UntypedFormArray; |
||||
|
|
||||
|
errorText = ''; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.keysListFormArray = this.prepareKeysFormArray(this.keys) |
||||
|
} |
||||
|
|
||||
|
trackByKey(index: number, keyControl: AbstractControl): any { |
||||
|
return keyControl; |
||||
|
} |
||||
|
|
||||
|
addKey(): void { |
||||
|
const dataKeyFormGroup = this.fb.group({ |
||||
|
key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]] |
||||
|
}); |
||||
|
if (this.keysType !== MappingKeysType.CUSTOM) { |
||||
|
dataKeyFormGroup.addControl('type', this.fb.control(this.rawData ? 'raw' : MappingValueType.STRING)); |
||||
|
} |
||||
|
this.keysListFormArray.push(dataKeyFormGroup); |
||||
|
} |
||||
|
|
||||
|
deleteKey($event: Event, index: number): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.keysListFormArray.removeAt(index); |
||||
|
this.keysListFormArray.markAsDirty(); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyKeysData() { |
||||
|
let keys = this.keysListFormArray.value; |
||||
|
if (this.keysType === MappingKeysType.CUSTOM) { |
||||
|
keys = {}; |
||||
|
for (let key of this.keysListFormArray.value) { |
||||
|
keys[key.key] = key.value; |
||||
|
} |
||||
|
} |
||||
|
this.keysDataApplied.emit(keys); |
||||
|
} |
||||
|
|
||||
|
private prepareKeysFormArray(keys: Array<MappingDataKey> | {[key: string]: any}): UntypedFormArray { |
||||
|
const keysControlGroups: Array<AbstractControl> = []; |
||||
|
if (keys) { |
||||
|
if (this.keysType === MappingKeysType.CUSTOM) { |
||||
|
keys = Object.keys(keys).map(key => { |
||||
|
return {key, value: keys[key], type: ''}; |
||||
|
}); |
||||
|
} |
||||
|
keys.forEach((keyData) => { |
||||
|
const { key, value, type } = keyData; |
||||
|
const dataKeyFormGroup = this.fb.group({ |
||||
|
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
type: [type, []] |
||||
|
}); |
||||
|
keysControlGroups.push(dataKeyFormGroup); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(keysControlGroups); |
||||
|
} |
||||
|
|
||||
|
valueTitle(value: any): string { |
||||
|
if (isDefinedAndNotNull(value)) { |
||||
|
if (typeof value === 'object') { |
||||
|
return JSON.stringify(value); |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-mapping-table tb-absolute-fill"> |
||||
|
<div fxFlex fxLayout="column" class="tb-mapping-table-content"> |
||||
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> |
||||
|
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span> |
||||
|
</div> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="manageMapping($event)" |
||||
|
matTooltip="{{ 'action.add' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>add</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
(click)="enterFilterMode()" |
||||
|
matTooltip="{{ 'action.search' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.search' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label> </mat-label> |
||||
|
<input #searchInput matInput |
||||
|
[formControl]="textSearch" |
||||
|
placeholder="{{ 'common.enter-search' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<button mat-icon-button (click)="exitFilterMode()" |
||||
|
matTooltip="{{ 'action.close' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<div class="table-container"> |
||||
|
<table mat-table [dataSource]="dataSource"> |
||||
|
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index"> |
||||
|
<mat-header-cell *matHeaderCellDef class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS"> |
||||
|
{{ column.title | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping" class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS"> |
||||
|
{{ mapping[column.def] }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<mat-header-cell *matHeaderCellDef |
||||
|
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}"> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping; let i = index" |
||||
|
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> |
||||
|
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-icon-button |
||||
|
(click)="manageMapping($event, i)"> |
||||
|
<tb-icon>edit</tb-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
(click)="deleteMapping($event, i)"> |
||||
|
<tb-icon>delete</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-icon-button |
||||
|
(click)="$event.stopPropagation()" |
||||
|
[matMenuTriggerFor]="cellActionsMenu"> |
||||
|
<mat-icon class="material-icons">more_vert</mat-icon> |
||||
|
</button> |
||||
|
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
||||
|
<button mat-icon-button |
||||
|
(click)="manageMapping($event, i)"> |
||||
|
<tb-icon>edit</tb-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
(click)="deleteMapping($event, i)"> |
||||
|
<tb-icon>delete</tb-icon> |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</div> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row *matRowDef="let mapping; columns: displayedColumns;"></mat-row> |
||||
|
</table> |
||||
|
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" |
||||
|
class="mat-headline-5 tb-absolute-fill tb-add-new"> |
||||
|
<button mat-button class="connector" |
||||
|
(click)="manageMapping($event)"> |
||||
|
<mat-icon class="tb-mat-96">add</mat-icon> |
||||
|
<span>{{ 'gateway.add-mapping' | translate }}</span> |
||||
|
</button> |
||||
|
</section> |
||||
|
</div> |
||||
|
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found" translate> |
||||
|
widget.no-data-found |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,101 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import '../scss/constants'; |
||||
|
|
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: block; |
||||
|
.tb-mapping-table { |
||||
|
.tb-mapping-table-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: #fff; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
&.tb-outlined-border { |
||||
|
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); |
||||
|
border: solid 1px #e0e0e0; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.mat-toolbar-tools{ |
||||
|
min-height: auto; |
||||
|
} |
||||
|
|
||||
|
.title-container{ |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.tb-mapping-table-title { |
||||
|
padding-right: 20px; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.table-container { |
||||
|
overflow: auto; |
||||
|
.mat-mdc-table { |
||||
|
table-layout: fixed; |
||||
|
min-width: 450px; |
||||
|
|
||||
|
.table-value-column { |
||||
|
padding: 0 12px; |
||||
|
width: 23%; |
||||
|
|
||||
|
&.request-column { |
||||
|
width: 38%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ellipsis { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.no-data-found { |
||||
|
height: calc(100% - 120px); |
||||
|
} |
||||
|
|
||||
|
@media #{$mat-xs} { |
||||
|
.mat-toolbar { |
||||
|
height: auto; |
||||
|
min-height: 100px; |
||||
|
|
||||
|
.tb-mapping-table-title{ |
||||
|
padding-bottom: 5px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
mat-cell.tb-value-cell { |
||||
|
cursor: pointer; |
||||
|
.mat-icon { |
||||
|
height: 24px; |
||||
|
width: 24px; |
||||
|
font-size: 24px; |
||||
|
color: #757575 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,339 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
NgZone, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
ViewChild, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { BehaviorSubject, Observable, Subject } from 'rxjs'; |
||||
|
import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; |
||||
|
import { Overlay } from '@angular/cdk/overlay'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, UntypedFormArray } from '@angular/forms'; |
||||
|
import { |
||||
|
ConvertorTypeTranslationsMap, |
||||
|
MappingInfo, |
||||
|
MappingType, |
||||
|
MappingTypeTranslationsMap, |
||||
|
RequestType, |
||||
|
RequestTypesTranslationsMap |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
||||
|
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; |
||||
|
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mapping-table', |
||||
|
templateUrl: './mapping-table.component.html', |
||||
|
styleUrls: ['./mapping-table.component.scss'], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => MappingTableComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { |
||||
|
|
||||
|
mappingTypeTranslationsMap = MappingTypeTranslationsMap; |
||||
|
mappingTypeEnum = MappingType; |
||||
|
displayedColumns = []; |
||||
|
mappingColumns = []; |
||||
|
textSearchMode = false; |
||||
|
dataSource: MappingDatasource; |
||||
|
hidePageSize = false; |
||||
|
|
||||
|
activeValue = false; |
||||
|
dirtyValue = false; |
||||
|
|
||||
|
viewsInited = false; |
||||
|
|
||||
|
mappingTypeValue: MappingType; |
||||
|
|
||||
|
get mappingType(): MappingType { |
||||
|
return this.mappingTypeValue; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set mappingType(value: MappingType) { |
||||
|
if (this.mappingTypeValue !== value) { |
||||
|
this.mappingTypeValue = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@ViewChild('searchInput') searchInputField: ElementRef; |
||||
|
|
||||
|
mappingFormGroup: UntypedFormArray; |
||||
|
textSearch = this.fb.control('', {nonNullable: true}); |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = (v: any) => {}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
private overlay: Overlay, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private dialogService: DialogService, |
||||
|
private entityService: EntityService, |
||||
|
private utils: UtilsService, |
||||
|
private zone: NgZone, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private elementRef: ElementRef, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store); |
||||
|
this.mappingFormGroup = this.fb.array([]); |
||||
|
this.dirtyValue = !this.activeValue; |
||||
|
this.dataSource = new MappingDatasource(); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
if (this.mappingType === MappingType.DATA) { |
||||
|
this.mappingColumns.push( |
||||
|
{def: 'topicFilter', title: 'gateway.topic-filter'}, |
||||
|
{def: 'QoS', title: 'gateway.mqtt-qos'}, |
||||
|
{def: 'converter', title: 'gateway.payload-type'} |
||||
|
) |
||||
|
} else { |
||||
|
this.mappingColumns.push( |
||||
|
{def: 'type', title: 'gateway.type'}, |
||||
|
{def: 'details', title: 'gateway.details'} |
||||
|
); |
||||
|
} |
||||
|
this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions'); |
||||
|
this.mappingFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
this.updateTableData(value); |
||||
|
this.updateView(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.textSearch.valueChanges.pipe( |
||||
|
debounceTime(150), |
||||
|
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((text) => { |
||||
|
const searchText = text.trim(); |
||||
|
this.updateTableData(this.mappingFormGroup.value, searchText.trim()) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void {} |
||||
|
|
||||
|
writeValue(config: any) { |
||||
|
if (isUndefinedOrNull(config)) { |
||||
|
config = this.mappingType === MappingType.REQUESTS ? {} : []; |
||||
|
} |
||||
|
let mappingConfigs = config; |
||||
|
if (this.mappingType === MappingType.REQUESTS) { |
||||
|
mappingConfigs = []; |
||||
|
|
||||
|
Object.keys(config).forEach((configKey) => { |
||||
|
for (let mapping of config[configKey]) { |
||||
|
mappingConfigs.push({ |
||||
|
requestType: configKey, |
||||
|
requestValue: mapping |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
this.mappingFormGroup.clear({emitEvent: false}); |
||||
|
for (let mapping of mappingConfigs) { |
||||
|
this.mappingFormGroup.push(this.fb.group(mapping), {emitEvent: false}); |
||||
|
} |
||||
|
this.updateTableData(mappingConfigs); |
||||
|
} |
||||
|
|
||||
|
updateView(mappingConfigs: Array<{[key: string]: any}>) { |
||||
|
let config; |
||||
|
if (this.mappingType === MappingType.REQUESTS) { |
||||
|
config = {}; |
||||
|
for (let mappingConfig of mappingConfigs) { |
||||
|
if (config[mappingConfig.requestType]) { |
||||
|
config[mappingConfig.requestType].push(mappingConfig.requestValue); |
||||
|
} else { |
||||
|
config[mappingConfig.requestType] = [mappingConfig.requestValue]; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
config = mappingConfigs; |
||||
|
} |
||||
|
|
||||
|
this.propagateChange(config); |
||||
|
} |
||||
|
|
||||
|
enterFilterMode() { |
||||
|
this.textSearchMode = true; |
||||
|
setTimeout(() => { |
||||
|
this.searchInputField.nativeElement.focus(); |
||||
|
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
||||
|
}, 10); |
||||
|
} |
||||
|
|
||||
|
exitFilterMode() { |
||||
|
this.updateTableData(this.mappingFormGroup.value); |
||||
|
this.textSearchMode = false; |
||||
|
this.textSearch.reset(); |
||||
|
} |
||||
|
|
||||
|
manageMapping($event: Event, index?: number) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {}; |
||||
|
this.dialog.open<MappingDialogComponent, MappingInfo, boolean>(MappingDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
mappingType: this.mappingType, |
||||
|
value, |
||||
|
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' |
||||
|
} |
||||
|
}).afterClosed().subscribe( |
||||
|
(res) => { |
||||
|
if (res) { |
||||
|
if (isDefinedAndNotNull(index)) { |
||||
|
this.mappingFormGroup.at(index).patchValue(res); |
||||
|
} else { |
||||
|
this.mappingFormGroup.push(this.fb.group(res)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateTableData(value: Array<{[key: string]: any}>, textSearch?: string): void { |
||||
|
let tableValue = value; |
||||
|
if (this.mappingType === MappingType.DATA) { |
||||
|
tableValue = tableValue.map((value) => { |
||||
|
return { |
||||
|
topicFilter: value.topicFilter, |
||||
|
QoS: value.subscriptionQos, |
||||
|
converter: this.translate.instant(ConvertorTypeTranslationsMap.get(value.converter.type)) |
||||
|
}; |
||||
|
}); |
||||
|
} else { |
||||
|
tableValue = tableValue.map((value) => { |
||||
|
let details; |
||||
|
if (value.requestType === RequestType.ATTRIBUTE_UPDATE) { |
||||
|
details = value.requestValue.attributeFilter; |
||||
|
} else if (value.requestType === RequestType.SERVER_SIDE_RPC) { |
||||
|
details = value.requestValue.methodFilter; |
||||
|
} else { |
||||
|
details = value.requestValue.topicFilter; |
||||
|
} |
||||
|
return { |
||||
|
type: this.translate.instant(RequestTypesTranslationsMap.get(value.requestType)), |
||||
|
details |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
if (textSearch) { |
||||
|
tableValue = tableValue.filter(value => |
||||
|
Object.values(value).some(val => |
||||
|
val.toString().toLowerCase().includes(textSearch.toLowerCase()) |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
this.dataSource.loadMappings(tableValue); |
||||
|
} |
||||
|
|
||||
|
deleteMapping($event: Event, index: number) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialogService.confirm( |
||||
|
this.translate.instant('gateway.delete-mapping-title'), |
||||
|
'', |
||||
|
this.translate.instant('action.no'), |
||||
|
this.translate.instant('action.yes'), |
||||
|
true |
||||
|
).subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.mappingFormGroup.removeAt(index); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export class MappingDatasource implements DataSource<{[key: string]: any}> { |
||||
|
|
||||
|
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]); |
||||
|
|
||||
|
private allMappings: Observable<Array<{[key: string]: any}>>; |
||||
|
|
||||
|
constructor() {} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<Array<{[key: string]: any}>> { |
||||
|
return this.mappingSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.mappingSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadMappings(mappings: Array<{[key: string]: any}>, pageLink?: PageLink, reload: boolean = false): void { |
||||
|
if (reload) { |
||||
|
this.allMappings = null; |
||||
|
} |
||||
|
this.mappingSubject.next(mappings); |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.mappingSubject.pipe( |
||||
|
map((mappings) => !mappings.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.mappingSubject.pipe( |
||||
|
map((mappings) => mappings.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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]="connectorForm" class="add-connector"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ "gateway.add-connector" | translate}}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<div [tb-help]="helpLinkId()"></div> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<div class="tb-form-panel no-border no-padding" fxLayout="column"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.type</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="type"> |
||||
|
<mat-option *ngFor="let type of gatewayConnectorDefaultTypesTranslatesMap | keyvalue" [value]="type.key"> |
||||
|
{{ type.value }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ? |
||||
|
'gateway.connector-duplicate-name' :'gateway.name-required') | translate" |
||||
|
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched) |
||||
|
|| connectorForm.get('name').hasError('duplicateName')" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.remote-logging-level</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="logLevel"> |
||||
|
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="useDefaults"> |
||||
|
<mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}"> |
||||
|
{{ 'gateway.fill-connector-defaults' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="sendDataOnlyOnChange"> |
||||
|
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}"> |
||||
|
{{ 'gateway.send-change-data' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
(click)="add()" |
||||
|
[disabled]="connectorForm.invalid || !connectorForm.dirty"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
.add-connector { |
||||
|
min-width: 400px; |
||||
|
width: 500px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, OnDestroy } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; |
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { |
||||
|
AddConnectorConfigData, |
||||
|
ConnectorType, |
||||
|
CreatedConnectorConfigData, |
||||
|
GatewayConnectorDefaultTypesTranslatesMap, |
||||
|
GatewayLogLevel, |
||||
|
getDefaultConfig, |
||||
|
noLeadTrailSpacesRegex |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { ResourcesService } from '@core/services/resources.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-add-connector-dialog', |
||||
|
templateUrl: './add-connector-dialog.component.html', |
||||
|
styleUrls: ['./add-connector-dialog.component.scss'], |
||||
|
providers: [], |
||||
|
}) |
||||
|
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy { |
||||
|
|
||||
|
connectorForm: UntypedFormGroup; |
||||
|
|
||||
|
connectorType = ConnectorType; |
||||
|
|
||||
|
gatewayConnectorDefaultTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; |
||||
|
gatewayLogLevel = Object.values(GatewayLogLevel); |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData, |
||||
|
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>, |
||||
|
private fb: FormBuilder, |
||||
|
private resourcesService: ResourcesService) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.connectorForm = this.fb.group({ |
||||
|
type: [ConnectorType.MQTT, []], |
||||
|
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
logLevel: [GatewayLogLevel.INFO, []], |
||||
|
useDefaults: [true, []], |
||||
|
sendDataOnlyOnChange: [false, []], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
super.ngOnDestroy(); |
||||
|
} |
||||
|
|
||||
|
helpLinkId(): string { |
||||
|
return 'https://thingsboard.io/docs/iot-gateway/configuration/'; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
this.submitted = true; |
||||
|
const value = this.connectorForm.getRawValue(); |
||||
|
if (value.useDefaults) { |
||||
|
getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => { |
||||
|
value.configurationJson = defaultConfig; |
||||
|
if (this.connectorForm.valid) { |
||||
|
this.dialogRef.close(value); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
if (this.connectorForm.valid) { |
||||
|
this.dialogRef.close(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private uniqNameRequired(): ValidatorFn { |
||||
|
return (c: UntypedFormControl) => { |
||||
|
const newName = c.value.trim().toLowerCase(); |
||||
|
const found = this.data.dataSourceData.find((connectorAttr) => { |
||||
|
const connectorData = connectorAttr.value; |
||||
|
return connectorData.name.toLowerCase() === newName; |
||||
|
}); |
||||
|
if (found) { |
||||
|
if (c.hasError('required')) { |
||||
|
return c.getError('required'); |
||||
|
} |
||||
|
return { |
||||
|
duplicateName: { |
||||
|
valid: false |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,634 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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]="mappingForm" class="key-mapping"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<div [tb-help]="helpLinkId()"></div> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<div class="tb-form-panel no-border no-padding" fxLayout="column"> |
||||
|
<div class="tb-form-hint tb-primary-fill"> |
||||
|
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }} |
||||
|
</div> |
||||
|
<ng-container [ngSwitch]="data.mappingType"> |
||||
|
<ng-template [ngSwitchCase]="MappingType.DATA"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="topicFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.topic-required') | translate" |
||||
|
*ngIf="mappingForm.get('topicFilter').hasError('required') && |
||||
|
mappingForm.get('topicFilter').touched;" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate> |
||||
|
gateway.mqtt-qos |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="subscriptionQos"> |
||||
|
<mat-option *ngFor="let type of qualityTypes" [value]="type"> |
||||
|
{{ QualityTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-container formGroupName="converter"> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.payload-type</div> |
||||
|
<tb-toggle-select formControlName="type" appearance="fill"> |
||||
|
<tb-toggle-option *ngFor="let type of convertorTypes" [value]="type"> |
||||
|
{{ ConvertorTypeTranslationsMap.get(type) | translate }} |
||||
|
</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.data-conversion</div> |
||||
|
<div class="tb-form-hint tb-primary-fill"> |
||||
|
{{ DataConversionTranslationsMap.get(converterType) | translate }} |
||||
|
</div> |
||||
|
<ng-container [formGroupName]="converterType" [ngSwitch]="converterType"> |
||||
|
<ng-template [ngSwitchCase]="ConvertorTypeEnum.JSON"> |
||||
|
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true"> |
||||
|
</tb-device-info-table> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="ConvertorTypeEnum.BYTES"> |
||||
|
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" |
||||
|
[sourceTypes]="[sourceTypesEnum.MSG, sourceTypesEnum.CONST]" required="true"> |
||||
|
</tb-device-info-table> |
||||
|
</ng-template> |
||||
|
<div class="tb-form-panel no-border no-padding" |
||||
|
*ngIf="converterType === ConvertorTypeEnum.BYTES || converterType === ConvertorTypeEnum.JSON"> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.attributes</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox [tb-ellipsis-chip-list]="converterAttributes" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let attribute of converterAttributes"> |
||||
|
{{ attribute }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#attributesButton |
||||
|
(click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.timeseries</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="converterTelemetry"> |
||||
|
<mat-chip *ngFor="let telemetry of converterTelemetry"> |
||||
|
{{ telemetry }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#telemetryButton |
||||
|
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" |
||||
|
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate> |
||||
|
gateway.extension |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="extension" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.extension-required') | translate" |
||||
|
*ngIf="mappingForm.get('converter.custom.extension').hasError('required') && |
||||
|
mappingForm.get('converter.custom.extension').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between same-padding tb-flex column"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.extension-configuration</div> |
||||
|
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.extension-configuration-hint' | translate }}</div> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.keys</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox [tb-ellipsis-chip-list]="customKeys" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let telemetry of customKeys"> |
||||
|
{{ telemetry }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#keysButton |
||||
|
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="MappingType.REQUESTS"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.request-type</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="requestType"> |
||||
|
<mat-option *ngFor="let type of requestTypes" [value]="type"> |
||||
|
{{ RequestTypesTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-container formGroupName="requestValue"> |
||||
|
<ng-container [formGroup]="mappingForm.get('requestValue').get(requestMappingType)" [ngSwitch]="requestMappingType"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" |
||||
|
*ngIf="requestMappingType === RequestTypeEnum.ATTRIBUTE_REQUEST || |
||||
|
requestMappingType === RequestTypeEnum.CONNECT_REQUEST || |
||||
|
requestMappingType === RequestTypeEnum.DISCONNECT_REQUEST"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" [formControl]="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter')" |
||||
|
placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.topic-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').hasError('required') && |
||||
|
mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template [ngSwitchCase]="RequestTypeEnum.CONNECT_REQUEST"> |
||||
|
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true"> |
||||
|
</tb-device-info-table> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="RequestTypeEnum.DISCONNECT_REQUEST"> |
||||
|
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.PARTIAL" required="true"> |
||||
|
</tb-device-info-table> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_REQUEST"> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title tb-required" translate>gateway.from-device-request-settings</div> |
||||
|
<div class="tb-form-hint tb-primary-fill" translate> |
||||
|
gateway.from-device-request-settings-hint |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" formGroupName="deviceInfo"> |
||||
|
<div class="fixed-title-width tb-flex no-flex align-center" translate> |
||||
|
<div class="tb-required" translate>gateway.device-info.device-name-expression</div> |
||||
|
</div> |
||||
|
<div class="tb-flex"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="deviceNameExpressionSource"> |
||||
|
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
||||
|
{{ SourceTypeTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate" |
||||
|
*ngIf="(mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').touched)" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.attribute-name-expression</div> |
||||
|
<div class="tb-flex"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="attributeNameExpressionSource"> |
||||
|
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
||||
|
{{ SourceTypeTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="attributeNameExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.attribute-name-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeRequests.attributeNameExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeRequests.attributeNameExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title tb-required" translate>gateway.to-device-response-settings</div> |
||||
|
<div class="tb-form-hint tb-primary-fill" translate> |
||||
|
gateway.to-device-response-settings-hint |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.response-value-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeRequests.valueExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeRequests.valueExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.response-topic-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeRequests.topicExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeRequests.topicExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="retain"> |
||||
|
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}"> |
||||
|
{{ 'gateway.retain' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" |
||||
|
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate> |
||||
|
gateway.device-name-filter |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-name-filter-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}" translate> |
||||
|
gateway.attribute-filter |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="attributeFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.attribute-filter-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeUpdates.attributeFilter').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeUpdates.attributeFilter').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.response-value-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeUpdates.valueExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeUpdates.valueExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.response-topic-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.attributeUpdates.topicExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.attributeUpdates.topicExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="retain"> |
||||
|
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}"> |
||||
|
{{ 'gateway.retain' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="RequestTypeEnum.SERVER_SIDE_RPC"> |
||||
|
<div class="tb-flex row center align-center no-gap fill-width"> |
||||
|
<tb-toggle-select formControlName="type" appearance="fill"> |
||||
|
<tb-toggle-option [value]="ServerSideRPCType.TWO_WAY"> |
||||
|
{{ 'gateway.with-response' | translate }} |
||||
|
</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="ServerSideRPCType.ONE_WAY"> |
||||
|
{{ 'gateway.without-response' | translate }} |
||||
|
</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate> |
||||
|
gateway.device-name-filter |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-name-filter-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').hasError('required') && |
||||
|
mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}" translate> |
||||
|
gateway.method-filter |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="methodFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.method-filter-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.serverSideRpc.methodFilter').hasError('required') && |
||||
|
mappingForm.get('requestValue.serverSideRpc.methodFilter').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.request-topic-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="requestTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.request-topic-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.value-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.value-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.serverSideRpc.valueExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.serverSideRpc.valueExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-container *ngIf="mappingForm.get('requestValue.serverSideRpc.type').value === ServerSideRPCType.TWO_WAY"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="responseTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.response-topic-expression-required') | translate" |
||||
|
*ngIf="mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').hasError('required') && |
||||
|
mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div matSuffix |
||||
|
class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate> |
||||
|
gateway.response-topic-Qos |
||||
|
</div> |
||||
|
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="responseTopicQoS"> |
||||
|
<mat-option *ngFor="let type of qualityTypes" [value]="type"> |
||||
|
{{ QualityTranslationsMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.response-timeout</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" type="number" min="1" formControlName="responseTimeout" |
||||
|
placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="responseTimeoutErrorTooltip" |
||||
|
*ngIf="(mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('required') || |
||||
|
mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('min')) && |
||||
|
mappingForm.get('requestValue.serverSideRpc.responseTimeout').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
(click)="add()" |
||||
|
[disabled]="mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed"> |
||||
|
{{ this.data.buttonTitle | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,76 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
display: grid; |
||||
|
height: 100%; |
||||
|
|
||||
|
.key-mapping { |
||||
|
max-width: 900px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
|
||||
|
.mat-toolbar { |
||||
|
min-height: 64px; |
||||
|
} |
||||
|
|
||||
|
tb-toggle-select { |
||||
|
padding: 4px 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-dialog-content { |
||||
|
max-height: 670px; |
||||
|
height: 670px; |
||||
|
} |
||||
|
|
||||
|
.ellipsis-chips-container { |
||||
|
max-width: 70%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.key-mapping { |
||||
|
.mat-mdc-chip-listbox { |
||||
|
.mdc-evolution-chip-set__chips { |
||||
|
justify-content: flex-end; |
||||
|
align-items: center; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-form-row { |
||||
|
.fixed-title-width { |
||||
|
min-width: 40px; |
||||
|
width: 35%; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
.mat-mdc-form-field { |
||||
|
width: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.see-example { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
margin: 4px; |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-form-field-icon-suffix { |
||||
|
display: flex; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,360 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { |
||||
|
ConvertorType, |
||||
|
ConvertorTypeTranslationsMap, |
||||
|
DataConversionTranslationsMap, |
||||
|
DeviceInfoType, |
||||
|
MappingHintTranslationsMap, |
||||
|
MappingInfo, |
||||
|
MappingKeysAddKeyTranslationsMap, |
||||
|
MappingKeysDeleteKeyTranslationsMap, |
||||
|
MappingKeysNoKeysTextTranslationsMap, |
||||
|
MappingKeysPanelTitleTranslationsMap, |
||||
|
MappingKeysType, |
||||
|
MappingType, |
||||
|
MappingTypeTranslationsMap, |
||||
|
noLeadTrailSpacesRegex, |
||||
|
QualityTypes, |
||||
|
QualityTypeTranslationsMap, |
||||
|
RequestType, |
||||
|
RequestTypesTranslationsMap, |
||||
|
ServerSideRPCType, |
||||
|
SourceTypes, |
||||
|
SourceTypeTranslationsMap |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { startWith, takeUntil } from 'rxjs/operators'; |
||||
|
import { MatButton } from '@angular/material/button'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mapping-dialog', |
||||
|
templateUrl: './mapping-dialog.component.html', |
||||
|
styleUrls: ['./mapping-dialog.component.scss'], |
||||
|
providers: [], |
||||
|
}) |
||||
|
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnDestroy { |
||||
|
|
||||
|
mappingForm: UntypedFormGroup; |
||||
|
|
||||
|
MappingType = MappingType; |
||||
|
|
||||
|
qualityTypes = QualityTypes; |
||||
|
QualityTranslationsMap = QualityTypeTranslationsMap; |
||||
|
|
||||
|
convertorTypes = Object.values(ConvertorType); |
||||
|
ConvertorTypeEnum = ConvertorType; |
||||
|
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap; |
||||
|
|
||||
|
sourceTypes = Object.values(SourceTypes); |
||||
|
sourceTypesEnum = SourceTypes; |
||||
|
SourceTypeTranslationsMap = SourceTypeTranslationsMap; |
||||
|
|
||||
|
requestTypes = Object.values(RequestType); |
||||
|
RequestTypeEnum = RequestType; |
||||
|
RequestTypesTranslationsMap = RequestTypesTranslationsMap; |
||||
|
|
||||
|
DeviceInfoType = DeviceInfoType; |
||||
|
|
||||
|
ServerSideRPCType = ServerSideRPCType; |
||||
|
|
||||
|
MappingKeysType = MappingKeysType; |
||||
|
|
||||
|
MappingHintTranslationsMap = MappingHintTranslationsMap; |
||||
|
|
||||
|
MappingTypeTranslationsMap = MappingTypeTranslationsMap; |
||||
|
|
||||
|
DataConversionTranslationsMap = DataConversionTranslationsMap; |
||||
|
|
||||
|
hiddenAttributesCount = 0; |
||||
|
|
||||
|
keysPopupClosed = true; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: MappingInfo, |
||||
|
public dialogRef: MatDialogRef<MappingDialogComponent, {[key: string]: any}>, |
||||
|
private fb: FormBuilder, |
||||
|
private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private translate: TranslateService) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.createMappingForm(); |
||||
|
} |
||||
|
|
||||
|
get converterAttributes(): Array<string> { |
||||
|
if (this.converterType) { |
||||
|
return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get converterTelemetry(): Array<string> { |
||||
|
if (this.converterType) { |
||||
|
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get converterType(): ConvertorType { |
||||
|
return this.mappingForm.get('converter').get('type').value; |
||||
|
} |
||||
|
|
||||
|
get customKeys(): Array<string> { |
||||
|
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig); |
||||
|
} |
||||
|
|
||||
|
get requestMappingType(): RequestType { |
||||
|
return this.mappingForm.get('requestType').value; |
||||
|
} |
||||
|
|
||||
|
get responseTimeoutErrorTooltip(): string { |
||||
|
const control = this.mappingForm.get('requestValue.serverSideRpc.responseTimeout'); |
||||
|
if (control.hasError('required')) { |
||||
|
return this.translate.instant('gateway.response-timeout-required'); |
||||
|
} else if (control.hasError('min')) { |
||||
|
return this.translate.instant('gateway.response-timeout-limits-error', {min: 1}); |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
super.ngOnDestroy(); |
||||
|
} |
||||
|
|
||||
|
private createMappingForm(): void { |
||||
|
this.mappingForm = this.fb.group({}); |
||||
|
if (this.data.mappingType === MappingType.DATA) { |
||||
|
this.mappingForm.addControl('topicFilter', |
||||
|
this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)])); |
||||
|
this.mappingForm.addControl('subscriptionQos', this.fb.control(0)); |
||||
|
this.mappingForm.addControl('converter', this.fb.group({ |
||||
|
type: [ConvertorType.JSON, []], |
||||
|
json: this.fb.group({ |
||||
|
deviceInfo: [{}, []], |
||||
|
attributes: [[], []], |
||||
|
timeseries: [[], []] |
||||
|
}), |
||||
|
bytes: this.fb.group({ |
||||
|
deviceInfo: [{}, []], |
||||
|
attributes: [[], []], |
||||
|
timeseries: [[], []] |
||||
|
}), |
||||
|
custom: this.fb.group({ |
||||
|
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
extensionConfig: [{}, []] |
||||
|
}), |
||||
|
})); |
||||
|
this.mappingForm.patchValue(this.prepareFormValueData()); |
||||
|
this.mappingForm.get('converter.type').valueChanges.pipe( |
||||
|
startWith(this.mappingForm.get('converter.type').value), |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
const converterGroup = this.mappingForm.get('converter'); |
||||
|
converterGroup.get('json').disable({emitEvent: false}); |
||||
|
converterGroup.get('bytes').disable({emitEvent: false}); |
||||
|
converterGroup.get('custom').disable({emitEvent: false}); |
||||
|
converterGroup.get(value).enable({emitEvent: false}); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if (this.data.mappingType === MappingType.REQUESTS) { |
||||
|
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, [])); |
||||
|
this.mappingForm.addControl('requestValue', this.fb.group({ |
||||
|
connectRequests: this.fb.group({ |
||||
|
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceInfo: [{}, []] |
||||
|
}), |
||||
|
disconnectRequests: this.fb.group({ |
||||
|
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceInfo: [{}, []] |
||||
|
}), |
||||
|
attributeRequests: this.fb.group({ |
||||
|
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceInfo: this.fb.group({ |
||||
|
deviceNameExpressionSource: [SourceTypes.MSG, []], |
||||
|
deviceNameExpression: ['', [Validators.required]], |
||||
|
}), |
||||
|
attributeNameExpressionSource: [SourceTypes.MSG, []], |
||||
|
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
retain: [false, []] |
||||
|
}), |
||||
|
attributeUpdates: this.fb.group({ |
||||
|
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
retain: [true, []] |
||||
|
}), |
||||
|
serverSideRpc: this.fb.group({ |
||||
|
type: [ServerSideRPCType.TWO_WAY, []], |
||||
|
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
responseTopicQoS: [0, []], |
||||
|
responseTimeout: [10000, [Validators.required, Validators.min(1)]], |
||||
|
}) |
||||
|
})); |
||||
|
this.mappingForm.get('requestType').valueChanges.pipe( |
||||
|
startWith(this.mappingForm.get('requestType').value), |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
const requestValueGroup = this.mappingForm.get('requestValue'); |
||||
|
requestValueGroup.get('connectRequests').disable({emitEvent: false}); |
||||
|
requestValueGroup.get('disconnectRequests').disable({emitEvent: false}); |
||||
|
requestValueGroup.get('attributeRequests').disable({emitEvent: false}); |
||||
|
requestValueGroup.get('attributeUpdates').disable({emitEvent: false}); |
||||
|
requestValueGroup.get('serverSideRpc').disable({emitEvent: false}); |
||||
|
requestValueGroup.get(value).enable(); |
||||
|
}); |
||||
|
this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc'); |
||||
|
if (value === ServerSideRPCType.ONE_WAY) { |
||||
|
requestValueGroup.get('responseTopicExpression').disable({emitEvent: false}); |
||||
|
requestValueGroup.get('responseTopicQoS').disable({emitEvent: false}); |
||||
|
requestValueGroup.get('responseTimeout').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
requestValueGroup.get('responseTopicExpression').enable({emitEvent: false}); |
||||
|
requestValueGroup.get('responseTopicQoS').enable({emitEvent: false}); |
||||
|
requestValueGroup.get('responseTimeout').enable({emitEvent: false}); |
||||
|
} |
||||
|
}); |
||||
|
this.mappingForm.patchValue(this.prepareFormValueData()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
helpLinkId(): string { |
||||
|
return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
if (this.keysPopupClosed) { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
this.submitted = true; |
||||
|
if (this.mappingForm.valid) { |
||||
|
this.dialogRef.close(this.prepareMappingData()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType); |
||||
|
const ctx: any = { |
||||
|
keys: keysControl.value, |
||||
|
keysType: keysType, |
||||
|
rawData: this.mappingForm.get('converter.type').value === ConvertorType.BYTES, |
||||
|
panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType), |
||||
|
addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType), |
||||
|
deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType), |
||||
|
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType) |
||||
|
}; |
||||
|
this.keysPopupClosed = false; |
||||
|
const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
||||
|
this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null, |
||||
|
ctx, |
||||
|
{}, |
||||
|
{}, {}, true); |
||||
|
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; |
||||
|
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => { |
||||
|
dataKeysPanelPopover.hide(); |
||||
|
keysControl.patchValue(keysData); |
||||
|
keysControl.markAsDirty(); |
||||
|
}); |
||||
|
dataKeysPanelPopover.tbHideStart.subscribe(() => { |
||||
|
this.keysPopupClosed = true; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private prepareMappingData(): {[key: string]: any} { |
||||
|
const formValue = this.mappingForm.value; |
||||
|
if (this.data.mappingType === MappingType.DATA) { |
||||
|
const { converter, topicFilter, subscriptionQos } = formValue; |
||||
|
return { |
||||
|
topicFilter, |
||||
|
subscriptionQos, |
||||
|
converter: { |
||||
|
type: converter.type, |
||||
|
...converter[converter.type] |
||||
|
} |
||||
|
}; |
||||
|
} else { |
||||
|
return { |
||||
|
requestType: formValue.requestType, |
||||
|
requestValue: formValue.requestValue[formValue.requestType] |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private prepareFormValueData(): {[key: string]: any} { |
||||
|
if (this.data.value && Object.keys(this.data.value).length) { |
||||
|
if (this.data.mappingType === MappingType.DATA) { |
||||
|
const { converter, topicFilter, subscriptionQos } = this.data.value; |
||||
|
return { |
||||
|
topicFilter, |
||||
|
subscriptionQos, |
||||
|
converter: { |
||||
|
type: converter.type, |
||||
|
[converter.type]: { ...converter } |
||||
|
} |
||||
|
}; |
||||
|
} else { |
||||
|
return { |
||||
|
requestType: this.data.value.requestType, |
||||
|
requestValue: { |
||||
|
[this.data.value.requestType]: this.data.value.requestValue |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
return this.data.value; |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue