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