From e5311984bba0d6449e6db65c9fdfac22cf2e98b7 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 7 Feb 2024 15:24:40 +0200 Subject: [PATCH 001/147] Housekeeper API --- .../DefaultHousekeeperService.java | 144 ++++++++++++++++++ .../HousekeeperReprocessingService.java | 134 ++++++++++++++++ .../AttributesDeletionTaskProcessor.java | 41 +++++ .../EntityDeletionTaskProcessor.java | 37 +++++ .../EventsDeletionTaskProcessor.java | 38 +++++ .../processor/HousekeeperTaskProcessor.java | 27 ++++ .../TelemetryDeletionTaskProcessor.java | 41 +++++ .../src/main/resources/thingsboard.yml | 7 + common/proto/src/main/proto/queue.proto | 8 + .../queue/common/AbstractTbQueueTemplate.java | 4 +- .../queue/kafka/TbKafkaTopicConfigs.java | 5 + .../provider/AwsSqsMonolithQueueFactory.java | 21 +++ .../provider/AwsSqsTbCoreQueueFactory.java | 21 +++ .../InMemoryMonolithQueueFactory.java | 20 +++ .../provider/KafkaMonolithQueueFactory.java | 51 ++++++- .../provider/KafkaTbCoreQueueFactory.java | 50 ++++++ .../provider/PubSubMonolithQueueFactory.java | 21 +++ .../provider/PubSubTbCoreQueueFactory.java | 21 +++ .../RabbitMqMonolithQueueFactory.java | 21 +++ .../provider/RabbitMqTbCoreQueueFactory.java | 20 +++ .../ServiceBusMonolithQueueFactory.java | 21 +++ .../ServiceBusTbCoreQueueFactory.java | 21 +++ .../queue/provider/TbCoreQueueFactory.java | 11 ++ .../provider/TbCoreQueueProducerProvider.java | 17 +++ .../provider/TbQueueProducerProvider.java | 6 + .../TbRuleEngineProducerProvider.java | 11 ++ .../TbTransportQueueProducerProvider.java | 10 ++ .../TbVersionControlProducerProvider.java | 10 ++ .../queue/settings/TbQueueCoreSettings.java | 6 + .../server/dao/device/DeviceServiceImpl.java | 5 + .../dao/entity/AbstractEntityService.java | 6 + .../dao/housekeeper/HousekeeperService.java | 24 +++ .../dao/housekeeper/data/HousekeeperTask.java | 51 +++++++ .../housekeeper/data/HousekeeperTaskType.java | 23 +++ 34 files changed, 951 insertions(+), 3 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java new file mode 100644 index 0000000000..7c7e72b511 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -0,0 +1,144 @@ +/** + * 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.housekeeper; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.housekeeper.HousekeeperService; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +@TbCoreComponent +@Service +@Slf4j +public class DefaultHousekeeperService implements HousekeeperService { + + private final Map taskProcessors; + + private final TbQueueConsumer> consumer; + private final TbQueueProducer> producer; + private final HousekeeperReprocessingService reprocessingService; + private final DataDecodingEncodingService dataDecodingEncodingService; + private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); + + @Value("${queue.core.housekeeper.poll-interval-ms:10000}") + private int pollInterval; + + private boolean stopped; + + public DefaultHousekeeperService(HousekeeperReprocessingService reprocessingService, + TbCoreQueueFactory queueFactory, + TbQueueProducerProvider producerProvider, + DataDecodingEncodingService dataDecodingEncodingService, List taskProcessors) { + this.consumer = queueFactory.createHousekeeperMsgConsumer(); + this.producer = producerProvider.getHousekeeperMsgProducer(); + this.reprocessingService = reprocessingService; + this.taskProcessors = taskProcessors.stream().collect(Collectors.toMap(HousekeeperTaskProcessor::getTaskType, p -> p)); + this.dataDecodingEncodingService = dataDecodingEncodingService; + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + consumer.subscribe(); + consumerExecutor.submit(() -> { + while (!stopped && !consumer.isStopped()) { + try { + List> msgs = consumer.poll(pollInterval); + if (msgs.isEmpty()) { + continue; + } + + for (TbProtoQueueMsg msg : msgs) { + try { + processTask(msg); + } catch (Exception e) { + log.error("Message processing failed", e); + } + } + consumer.commit(); + } catch (Throwable t) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", t); + try { + Thread.sleep(pollInterval); + } catch (InterruptedException interruptedException) { + log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); + } + } + } + } + }); + log.info("Started Housekeeper service"); + } + + protected void processTask(TbProtoQueueMsg msg) { + HousekeeperTask task = dataDecodingEncodingService.decode(msg.getValue().getTask().getValue().toByteArray()).get(); + HousekeeperTaskProcessor taskProcessor = taskProcessors.get(task.getTaskType()); + if (taskProcessor == null) { + log.error("Unsupported task type {}: {}", task.getTaskType(), task); + return; + } + + log.info("[{}] Processing task: {}", task.getTenantId(), task); + try { + taskProcessor.process(task); + } catch (Exception e) { + log.error("[{}] Task processing failed: {}", task.getTenantId(), task, e); + reprocessingService.submitForReprocessing(msg); + } + } + + @Override + public void submitTask(HousekeeperTask task) { + TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); + producer.send(tpi, new TbProtoQueueMsg<>(Uuids.timeBased(), ToHousekeeperServiceMsg.newBuilder() + .setTask(HousekeeperTaskProto.newBuilder() + .setValue(ByteString.copyFrom(dataDecodingEncodingService.encode(task))) + .build()) + .build()), null); + } + + @PreDestroy + private void stop() { + log.info("Stopped Housekeeper service"); + stopped = true; + consumer.unsubscribe(); + consumerExecutor.shutdownNow(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java new file mode 100644 index 0000000000..234cd07c36 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -0,0 +1,134 @@ +/** + * 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.housekeeper; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsgHeaders; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToLong; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.longToBytes; + +@TbCoreComponent +@Service +@Slf4j +public class HousekeeperReprocessingService { + + private final DefaultHousekeeperService housekeeperService; + private final TbQueueProducerProvider producerProvider; + private final TbQueueConsumer> consumer; + private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); + + @Value("${queue.core.housekeeper.poll-interval-ms:10000}") + private int pollInterval; + + private final long startTs = System.currentTimeMillis(); + private boolean stopped; + // todo: stats + + public HousekeeperReprocessingService(@Lazy DefaultHousekeeperService housekeeperService, + TbCoreQueueFactory queueFactory, + TbQueueProducerProvider producerProvider) { + this.housekeeperService = housekeeperService; + this.consumer = queueFactory.createHousekeeperDelayedMsgConsumer(); + this.producerProvider = producerProvider; + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + consumer.subscribe(); + consumerExecutor.submit(() -> { + while (!stopped && !consumer.isStopped()) { + try { + List> msgs = consumer.poll(pollInterval); + if (msgs.isEmpty()) { + stop(); + return; + } + + for (TbProtoQueueMsg msg : msgs) { + long msgTs = Uuids.unixTimestamp(msg.getKey()); + if (msgTs >= startTs) { + stop(); + return; // fixme: we should commit already reprocessed messages + } + + try { + reprocessTask(msg); + } catch (Exception e) { + log.error("Message processing failed", e); + } + } + consumer.commit(); + } catch (Throwable t) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", t); + try { + Thread.sleep(pollInterval); + } catch (InterruptedException interruptedException) { + log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); + } + } + } + } + }); + log.info("Started Housekeeper tasks reprocessing"); + } + + private void reprocessTask(TbProtoQueueMsg msg) { + housekeeperService.processTask(msg);// fixme: or should we submit to queue? + } + + public void submitForReprocessing(TbProtoQueueMsg msg) { + TbQueueMsgHeaders msgHeaders = msg.getHeaders(); + long reprocessingAttempts = Optional.ofNullable(msgHeaders.get("reprocessingAttempts")) + .map(header -> bytesToLong(header)) + .orElse(0L); + reprocessingAttempts++; + msgHeaders.put("reprocessingAttempts", longToBytes(reprocessingAttempts)); + + var producer = producerProvider.getHousekeeperDelayedMsgProducer(); + TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); + producer.send(tpi, new TbProtoQueueMsg<>(Uuids.timeBased(), msg.getValue(), msgHeaders), null); + } + + @PreDestroy + private void stop() { + log.info("Stopped Housekeeper tasks reprocessing"); + stopped = true; + consumer.unsubscribe(); + consumerExecutor.shutdownNow(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java new file mode 100644 index 0000000000..34bd95a901 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java @@ -0,0 +1,41 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; + +@Component +@RequiredArgsConstructor +public class AttributesDeletionTaskProcessor implements HousekeeperTaskProcessor { + + private final AttributesService attributesService; + + @Override + public void process(HousekeeperTask task) throws Exception { +// attributesService.removeAll(task.getTenantId(), task.getEntityId(), DataConstants.CLIENT_SCOPE); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_ATTRIBUTES; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java new file mode 100644 index 0000000000..e1b0877d2c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java @@ -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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; + +@Component +@RequiredArgsConstructor +public class EntityDeletionTaskProcessor implements HousekeeperTaskProcessor { + + @Override + public void process(HousekeeperTask task) throws Exception { + + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_ENTITY; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java new file mode 100644 index 0000000000..7b8fdb1242 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java @@ -0,0 +1,38 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; + +@Component +@RequiredArgsConstructor +public class EventsDeletionTaskProcessor implements HousekeeperTaskProcessor { + private final EventService eventService; + + @Override + public void process(HousekeeperTask task) throws Exception { + eventService.removeEvents(task.getTenantId(), task.getEntityId(), null, 0L, System.currentTimeMillis()); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_EVENTS; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java new file mode 100644 index 0000000000..bfa7466dc3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java @@ -0,0 +1,27 @@ +/** + * 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.housekeeper.processor; + +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; + +public interface HousekeeperTaskProcessor { + + void process(HousekeeperTask task) throws Exception; + + HousekeeperTaskType getTaskType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java new file mode 100644 index 0000000000..feb5b0ddc1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -0,0 +1,41 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +@Component +@RequiredArgsConstructor +public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor { + + private final TimeseriesService timeseriesService; + + @Override + public void process(HousekeeperTask task) throws Exception { +// timeseriesService.remove() +// timeseriesService.removeAllLatest(); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_ATTRIBUTES; + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 70a5146bb7..17b4c85cfb 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1383,6 +1383,9 @@ queue: # tb_rule_engine.sq: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" + tb_housekeeper: + - key: max.poll.records + value: "1" other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms @@ -1404,6 +1407,7 @@ queue: ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Version Control topic version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + housekeeper: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1559,6 +1563,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" + housekeeper: + topic: "tb_housekeeper" + poll-interval-ms: "1000" vc: # Default topic name for Kafka, RabbitMQ, etc. topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 4da2883291..0dbd96a53d 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1391,3 +1391,11 @@ message LifecycleEventProto { bool success = 7; string error = 8; } + +message ToHousekeeperServiceMsg { + HousekeeperTaskProto task = 1; +} + +message HousekeeperTaskProto { + bytes value = 1; +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java index f11eaaef48..9340cafa67 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java @@ -46,13 +46,13 @@ public class AbstractTbQueueTemplate { return new String(data, StandardCharsets.UTF_8); } - protected static byte[] longToBytes(long x) { + public static byte[] longToBytes(long x) { ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); longBuffer.putLong(0, x); return longBuffer.array(); } - protected static long bytesToLong(byte[] bytes) { + public static long bytesToLong(byte[] bytes) { return ByteBuffer.wrap(bytes).getLong(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index d23d92ee26..d3a5eb821d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -43,6 +43,8 @@ public class TbKafkaTopicConfigs { private String fwUpdatesProperties; @Value("${queue.kafka.topic-properties.version-control:}") private String vcProperties; + @Value("${queue.kafka.topic-properties.housekeeper:}") + private String housekeeperProperties; @Getter private Map coreConfigs; @@ -62,6 +64,8 @@ public class TbKafkaTopicConfigs { private Map fwUpdatesConfigs; @Getter private Map vcConfigs; + @Getter + private Map housekeeperConfigs; @PostConstruct private void init() { @@ -76,6 +80,7 @@ public class TbKafkaTopicConfigs { jsExecutorResponseConfigs.put(NUM_PARTITIONS_SETTING, "1"); fwUpdatesConfigs = PropertyUtils.getProps(fwUpdatesProperties); vcConfigs = PropertyUtils.getProps(vcProperties); + housekeeperConfigs = PropertyUtils.getProps(housekeeperProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index 8a127384be..5a61bc1d80 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -226,6 +227,26 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng return new TbAwsSqsProducerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index 6c69b2ec10..a1be25f29d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -24,6 +24,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -205,6 +206,26 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { return new TbAwsSqsProducerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index a681038aa9..1ee8b404a5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -160,6 +160,26 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(vcSettings.getTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())); + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())); + } + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") private void printInMemoryStats() { storage.printStats(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 662d29af5b..5ee9b6f53b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -25,6 +25,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -39,8 +40,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -82,6 +83,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin housekeeperAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -115,6 +117,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); + this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); } @Override @@ -346,6 +349,52 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi return requestBuilder.build(); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("monolith-housekeeper-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .admin(housekeeperAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .clientId("monolith-housekeeper-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("monolith-housekeeper-consumer")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(housekeeperAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("monolith-housekeeper-delayed-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) + .admin(housekeeperAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) + .clientId("monolith-housekeeper-delayed-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("monolith-housekeeper-delayed-consumer")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(housekeeperAdmin) + .statsService(consumerStatsService) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index db6ee52181..c01ba32f70 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; @@ -80,6 +81,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin housekeeperAdmin; public KafkaTbCoreQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -112,6 +114,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); + this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); } @Override @@ -303,6 +306,53 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { return requestBuilder.build(); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("tb-core-housekeeper-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .admin(housekeeperAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .clientId("tb-core-housekeeper-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("tb-core-housekeeper-consumer")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(housekeeperAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("tb-core-housekeeper-delayed-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) + .admin(housekeeperAdmin) + .build(); + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) + .clientId("tb-core-housekeeper-delayed-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("tb-core-housekeeper-delayed-consumer")) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(housekeeperAdmin) + .statsService(consumerStatsService) + .build(); + } + + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 62fe40f853..a63a9fc16b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -227,6 +228,26 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng return new TbPubSubProducerTemplate<>(vcAdmin, pubSubSettings, vcSettings.getTopic()); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 0e7e1ba241..5f1f282e39 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -24,6 +24,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -198,6 +199,26 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { return null; } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 1b7f705175..802b08773d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -224,6 +225,26 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new TbRabbitMqProducerTemplate<>(vcAdmin, rabbitMqSettings, topicService.buildTopicName(vcSettings.getTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index e0d0ea3b3a..ceb6e03ca5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -198,6 +198,26 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 359cbf67b8..1e9c1cadaf 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -25,6 +25,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -223,6 +224,26 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul return new TbServiceBusProducerTemplate<>(vcAdmin, serviceBusSettings, topicService.buildTopicName(vcSettings.getTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index d2ef072ffa..e39df45b82 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -24,6 +24,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -198,6 +199,26 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { return null; } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return null; + } + + @Override + public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + return null; + } + + @Override + public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + return null; + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 5aa7866033..279a469415 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -16,8 +16,10 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -130,4 +132,13 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory { * @return */ TbQueueProducer> createVersionControlMsgProducer(); + + TbQueueProducer> createHousekeeperMsgProducer(); + + TbQueueConsumer> createHousekeeperMsgConsumer(); + + TbQueueProducer> createHousekeeperDelayedMsgProducer(); + + TbQueueConsumer> createHousekeeperDelayedMsgConsumer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 5a99f361b4..4fa22cb7e3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -16,8 +16,10 @@ package org.thingsboard.server.queue.provider; import org.springframework.stereotype.Service; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -41,6 +43,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCoreNotifications; private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; + private TbQueueProducer> toHousekeeper; + private TbQueueProducer> toHousekeeperDelayed; public TbCoreQueueProducerProvider(TbCoreQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -55,6 +59,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toVersionControl = tbQueueProvider.createVersionControlMsgProducer(); + this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); + this.toHousekeeperDelayed = tbQueueProvider.createHousekeeperDelayedMsgProducer(); } @Override @@ -91,4 +97,15 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { public TbQueueProducer> getTbVersionControlMsgProducer() { return toVersionControl; } + + @Override + public TbQueueProducer> getHousekeeperMsgProducer() { + return toHousekeeper; + } + + @Override + public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + return toHousekeeperDelayed; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index 4dbdca4375..5cdf390f47 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -78,4 +79,9 @@ public interface TbQueueProducerProvider { * @return */ TbQueueProducer> getTbVersionControlMsgProducer(); + + TbQueueProducer> getHousekeeperMsgProducer(); + + TbQueueProducer> getHousekeeperDelayedMsgProducer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index e4d98796f4..c5ddf57fc0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -89,4 +89,15 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Rule Engine!"); } + + @Override + public TbQueueProducer> getHousekeeperMsgProducer() { + return null; // fixme + } + + @Override + public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Rule Engine!"); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index c83b905a7c..e8dd7431a6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -85,4 +85,14 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } + + @Override + public TbQueueProducer> getHousekeeperMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Transport!"); + } + + @Override + public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Transport!"); + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index dbbaa9f072..7ddd2109ba 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -81,4 +81,14 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } + + @Override + public TbQueueProducer> getHousekeeperMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } + + @Override + public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java index 0a8c12eb8d..4edb688235 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java @@ -34,6 +34,12 @@ public class TbQueueCoreSettings { @Value("${queue.core.usage-stats-topic:tb_usage_stats}") private String usageStatsTopic; + @Value("${queue.core.housekeeper.topic:tb_housekeeper}") + private String housekeeperTopic; + + @Value("${queue.core.housekeeper.topic:tb_housekeeper.delayed}") + private String housekeeperDelayedTopic; + @Value("${queue.core.partitions}") private int partitions; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index f190923167..f0e5dc1a4a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -75,6 +75,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -328,6 +329,10 @@ public class DeviceServiceImpl extends AbstractCachedEntityService Date: Wed, 7 Feb 2024 16:12:11 +0200 Subject: [PATCH 002/147] Submit cleanup tasks on DeleteEntityEvent --- .../EntityAlarmsDeletionTaskProcessor.java | 40 +++++++++++++++++++ .../server/dao/alarm/AlarmService.java | 2 +- .../server/dao/alarm/BaseAlarmService.java | 3 +- .../dao/asset/AssetProfileServiceImpl.java | 1 - .../server/dao/asset/BaseAssetService.java | 3 -- .../dao/customer/CustomerServiceImpl.java | 1 - .../dao/dashboard/DashboardServiceImpl.java | 1 - .../dao/device/DeviceProfileServiceImpl.java | 1 - .../server/dao/device/DeviceServiceImpl.java | 8 ---- .../server/dao/edge/EdgeServiceImpl.java | 7 +--- .../dao/entity/AbstractEntityService.java | 31 +++++++++++--- .../dao/entityview/EntityViewServiceImpl.java | 1 - .../dao/housekeeper/data/HousekeeperTask.java | 9 +++++ .../housekeeper/data/HousekeeperTaskType.java | 4 +- .../server/dao/rule/BaseRuleChainService.java | 7 ++-- .../dao/tenant/TenantProfileServiceImpl.java | 1 - .../server/dao/tenant/TenantServiceImpl.java | 1 - .../server/dao/user/UserServiceImpl.java | 1 - 18 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java new file mode 100644 index 0000000000..f9e61fc1cb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java @@ -0,0 +1,40 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; + +@Component +@RequiredArgsConstructor +public class EntityAlarmsDeletionTaskProcessor implements HousekeeperTaskProcessor { + + private final AlarmService alarmService; + + @Override + public void process(HousekeeperTask task) throws Exception { + alarmService.deleteEntityAlarmRecords(task.getTenantId(), task.getEntityId()); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_ENTITY_ALARMS; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 203fe8e744..7a55348bed 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -106,7 +106,7 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); - void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); + void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); void deleteEntityAlarmRecordsByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 72e1b9c7bf..a14c172a57 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -189,7 +189,6 @@ public class BaseAlarmService extends AbstractCachedEntityService event) { + TenantId tenantId = event.getTenantId(); + EntityId entityId = event.getEntityId(); + log.trace("[{}] DeleteEntityEvent handler: {}", tenantId, event); + + cleanUpRelatedData(tenantId, entityId); + if (EntityType.USER.equals(entityId.getEntityType())) { +// housekeeperService.submitTask(HousekeeperTask.unassignAlarms(tenantId, entityId)); +// unassignDeletedUserAlarms(tenantId, (User) event.getEntity(), event.getTs()); + } + } + + protected void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { + // todo: skipped entities list + relationService.deleteEntityRelations(tenantId, entityId); + housekeeperService.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); + } + protected void createRelation(TenantId tenantId, EntityRelation relation) { log.debug("Creating relation: {}", relation); relationService.saveRelation(tenantId, relation); @@ -79,11 +103,6 @@ public abstract class AbstractEntityService { relationService.deleteRelation(tenantId, relation); } - protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) { - relationService.deleteEntityRelations(tenantId, entityId); - alarmService.deleteEntityAlarmRelations(tenantId, entityId); - } - protected static Optional extractConstraintViolationException(Exception t) { if (t instanceof ConstraintViolationException) { return Optional.of((ConstraintViolationException) t); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index ed962ce84c..6940b42794 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -331,7 +331,6 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService updatedRuleNodes = new ArrayList<>(); List existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId()); for (RuleNode existingNode : existingRuleNodes) { - deleteEntityRelations(tenantId, existingNode.getId()); + cleanUpRelatedData(tenantId, existingNode.getId()); // fixme: for sure? Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { @@ -771,7 +771,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private void deleteRuleNodes(TenantId tenantId, List ruleNodes) { List ruleNodeIds = ruleNodes.stream().map(RuleNode::getId).collect(Collectors.toList()); for (var node : ruleNodes) { - deleteEntityRelations(tenantId, node.getId()); + cleanUpRelatedData(tenantId, node.getId()); } ruleNodeDao.deleteByIdIn(ruleNodeIds); } @@ -783,7 +783,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (EntityRelation relation : nodeRelations) { deleteRuleNode(tenantId, relation.getTo()); } - deleteEntityRelations(tenantId, ruleChainId); } @@ -820,8 +819,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } private void deleteRuleNode(TenantId tenantId, EntityId entityId) { - deleteEntityRelations(tenantId, entityId); ruleNodeDao.removeById(tenantId, entityId.getId()); + cleanUpRelatedData(tenantId, entityId); } private final PaginatedRemover tenantRuleChainsRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 31a419a5d0..2462ca19d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -130,7 +130,6 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService Date: Wed, 7 Feb 2024 16:50:03 +0200 Subject: [PATCH 003/147] Alarms unassign housekeeper task; remove in-memory housekeeper service --- .../entitiy/alarm/DefaultTbAlarmService.java | 16 +-- .../service/entitiy/alarm/TbAlarmService.java | 2 +- .../InMemoryHouseKeeperServiceService.java | 103 ------------------ .../AlarmsUnassignTaskProcessor.java | 41 +++++++ .../AttributesDeletionTaskProcessor.java | 3 +- .../EntityAlarmsDeletionTaskProcessor.java | 2 +- .../EntityDeletionTaskProcessor.java | 2 +- .../EventsDeletionTaskProcessor.java | 2 +- .../processor/HousekeeperTaskProcessor.java | 4 +- .../TelemetryDeletionTaskProcessor.java | 2 +- .../alarm/DefaultTbAlarmServiceTest.java | 2 +- .../dao/entity/AbstractEntityService.java | 6 +- .../data/AlarmsUnassignHousekeeperTask.java | 18 +-- .../dao/housekeeper/data/HousekeeperTask.java | 19 ++-- 14 files changed, 82 insertions(+), 140 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java rename common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java => dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java (59%) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 5305018b84..deefdaa614 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -176,16 +176,16 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public List unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs) { + public List unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, long unassignTs) { List totalAlarmIds = new ArrayList<>(); PageLink pageLink = new PageLink(100, 0, null, new SortOrder("id", SortOrder.Direction.ASC)); while (true) { - PageData pageData = alarmService.findAlarmIdsByAssigneeId(user.getTenantId(), user.getId(), pageLink); + PageData pageData = alarmService.findAlarmIdsByAssigneeId(tenantId, userId, pageLink); List alarmIds = pageData.getData(); if (alarmIds.isEmpty()) { break; } - processAlarmsUnassignment(tenantId, user, alarmIds, unassignTs); + processAlarmsUnassignment(tenantId, userId, userTitle, alarmIds, unassignTs); totalAlarmIds.addAll(alarmIds); pageLink = pageLink.nextPageLink(); } @@ -204,16 +204,16 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb return ts > 0 ? ts : System.currentTimeMillis(); } - private void processAlarmsUnassignment(TenantId tenantId, User user, List alarmIds, long unassignTs) { + private void processAlarmsUnassignment(TenantId tenantId, UserId userId, String userTitle, List alarmIds, long unassignTs) { for (AlarmId alarmId : alarmIds) { - log.trace("[{}] Unassigning alarm {} userId {}", tenantId, alarmId, user.getId()); - AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(user.getTenantId(), alarmId, unassignTs); + log.trace("[{}] Unassigning alarm {} userId {}", tenantId, alarmId, userId); + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarmId, unassignTs); if (!result.isSuccessful()) { - log.error("[{}] Cannot unassign alarm {} userId {}", tenantId, alarmId, user.getId()); + log.error("[{}] Cannot unassign alarm {} userId {}", tenantId, alarmId, userId); continue; } if (result.isModified()) { - String comment = String.format("Alarm was unassigned because user %s - was deleted", user.getTitle()); + String comment = String.format("Alarm was unassigned because user %s - was deleted", userTitle); addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment); notificationEntityService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 34dc3d11f2..c975291b58 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -41,7 +41,7 @@ public interface TbAlarmService { AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; - List unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs); + List unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, long unassignTs); Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java deleted file mode 100644 index 4870990606..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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.housekeeper; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionalEventListener; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; -import org.thingsboard.server.dao.housekeeper.HouseKeeperService; -import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; - -@Component -@RequiredArgsConstructor -@Slf4j -public class InMemoryHouseKeeperServiceService implements HouseKeeperService { - - final TbAlarmService alarmService; - - ListeningExecutorService executor; - - AtomicInteger queueSize = new AtomicInteger(); - AtomicInteger totalProcessedCounter = new AtomicInteger(); - - @PostConstruct - public void init() { - log.debug("Starting HouseKeeper service"); - executor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper"))); - } - - @PreDestroy - public void destroy() { - if (executor != null) { - log.debug("Stopping HouseKeeper service"); - executor.shutdown(); - } - } - - @TransactionalEventListener(fallbackExecution = true) - public void handleEvent(DeleteEntityEvent event) { - log.trace("[{}] DeleteEntityEvent handler: {}", event.getTenantId(), event); - EntityId entityId = event.getEntityId(); - if (EntityType.USER.equals(entityId.getEntityType())) { - unassignDeletedUserAlarms(event.getTenantId(), (User) event.getEntity(), event.getTs()); - } - } - - @Override - public ListenableFuture> unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs) { - log.debug("[{}][{}] unassignDeletedUserAlarms submitting, pending queue size: {} ", tenantId, user.getId().getId(), queueSize.get()); - queueSize.incrementAndGet(); - ListenableFuture> future = executor.submit(() -> alarmService.unassignDeletedUserAlarms(tenantId, user, unassignTs)); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(List alarmIds) { - queueSize.decrementAndGet(); - totalProcessedCounter.incrementAndGet(); - log.debug("[{}][{}] unassignDeletedUserAlarms finished, pending queue size: {}, total processed count: {} ", - tenantId, user.getId().getId(), queueSize.get(), totalProcessedCounter.get()); - } - - @Override - public void onFailure(Throwable throwable) { - queueSize.decrementAndGet(); - totalProcessedCounter.incrementAndGet(); - log.error("[{}][{}] unassignDeletedUserAlarms failed, pending queue size: {}, total processed count: {}", - tenantId, user.getId().getId(), queueSize.get(), totalProcessedCounter.get(), throwable); - } - }, MoreExecutors.directExecutor()); - return future; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java new file mode 100644 index 0000000000..54bf7989af --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java @@ -0,0 +1,41 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.dao.housekeeper.data.AlarmsUnassignHousekeeperTask; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; + +@Component +@RequiredArgsConstructor +public class AlarmsUnassignTaskProcessor implements HousekeeperTaskProcessor { + + private final TbAlarmService alarmService; + + @Override + public void process(AlarmsUnassignHousekeeperTask task) throws Exception { + alarmService.unassignDeletedUserAlarms(task.getTenantId(), (UserId) task.getEntityId(), task.getUserTitle(), task.getTs()); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.UNASSIGN_ALARMS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java index 34bd95a901..169ac3e573 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java @@ -17,14 +17,13 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor -public class AttributesDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class AttributesDeletionTaskProcessor implements HousekeeperTaskProcessor { private final AttributesService attributesService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java index f9e61fc1cb..a91f2262f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java @@ -23,7 +23,7 @@ import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor -public class EntityAlarmsDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class EntityAlarmsDeletionTaskProcessor implements HousekeeperTaskProcessor { private final AlarmService alarmService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java index e1b0877d2c..deb693c466 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java @@ -22,7 +22,7 @@ import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor -public class EntityDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class EntityDeletionTaskProcessor implements HousekeeperTaskProcessor { @Override public void process(HousekeeperTask task) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java index 7b8fdb1242..56edf01637 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java @@ -23,7 +23,7 @@ import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor -public class EventsDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class EventsDeletionTaskProcessor implements HousekeeperTaskProcessor { private final EventService eventService; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java index bfa7466dc3..7daa99540e 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java @@ -18,9 +18,9 @@ package org.thingsboard.server.service.housekeeper.processor; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; -public interface HousekeeperTaskProcessor { +public interface HousekeeperTaskProcessor { - void process(HousekeeperTask task) throws Exception; + void process(T task) throws Exception; HousekeeperTaskType getTaskType(); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java index feb5b0ddc1..2a491148ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -23,7 +23,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; @Component @RequiredArgsConstructor -public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor { private final TimeseriesService timeseriesService; diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index 5d493193db..38b64d50ed 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -173,7 +173,7 @@ public class DefaultTbAlarmServiceTest { User user = new User(); user.setEmail("testEmail@gmail.com"); user.setId(new UserId(UUID.randomUUID())); - service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user, System.currentTimeMillis()); + service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user.getId(), user.getTitle(), System.currentTimeMillis()); ObjectNode commentNode = JacksonUtil.newObjectNode(); commentNode.put("subtype", "ASSIGN"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index ba37737b84..7bfe76e753 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -24,6 +24,7 @@ import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -78,9 +79,8 @@ public abstract class AbstractEntityService { log.trace("[{}] DeleteEntityEvent handler: {}", tenantId, event); cleanUpRelatedData(tenantId, entityId); - if (EntityType.USER.equals(entityId.getEntityType())) { -// housekeeperService.submitTask(HousekeeperTask.unassignAlarms(tenantId, entityId)); -// unassignDeletedUserAlarms(tenantId, (User) event.getEntity(), event.getTs()); + if (entityId.getEntityType() == EntityType.USER) { + housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java similarity index 59% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java rename to dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java index 55b164df52..876a8f736b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java @@ -13,17 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.housekeeper; +package org.thingsboard.server.dao.housekeeper.data; -import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.id.TenantId; -import java.util.List; +@Getter +public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { -public interface HouseKeeperService { + private final String userTitle; + private final long ts; - ListenableFuture> unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs); + protected AlarmsUnassignHousekeeperTask(User user) { + super(user.getTenantId(), user.getId(), HousekeeperTaskType.UNASSIGN_ALARMS); + this.userTitle = user.getTitle(); + this.ts = System.currentTimeMillis(); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java index 7ab415fe7b..726e07a790 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java @@ -15,27 +15,28 @@ */ package org.thingsboard.server.dao.housekeeper.data; -import lombok.Data; +import lombok.Getter; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import java.io.Serializable; /* * on start, read the retry queue and put the messages back to main queue (save offset) * */ -@Data +@Getter public class HousekeeperTask implements Serializable { private final TenantId tenantId; private final EntityId entityId; private final HousekeeperTaskType taskType; - // maybe we should not delete relations asynchronously -// public static HousekeeperTask deleteRelations(TenantId tenantId, EntityId entityId) { -// return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_RELATIONS); -// } + protected HousekeeperTask(TenantId tenantId, EntityId entityId, HousekeeperTaskType taskType) { + this.tenantId = tenantId; + this.entityId = entityId; + this.taskType = taskType; + } public static HousekeeperTask deleteAttributes(TenantId tenantId, EntityId entityId) { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ATTRIBUTES); @@ -49,8 +50,8 @@ public class HousekeeperTask implements Serializable { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_EVENTS); } - public static HousekeeperTask unassignAlarms(TenantId tenantId, UserId userId) { - return new HousekeeperTask(tenantId, userId, HousekeeperTaskType.UNASSIGN_ALARMS); + public static HousekeeperTask unassignAlarms(User user) { + return new AlarmsUnassignHousekeeperTask(user); } public static HousekeeperTask deleteEntityAlarms(TenantId tenantId, EntityId entityId) { From 7e07771accb561d487a57f28f6e72b8e2a8437b1 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 14 Feb 2024 20:20:08 +0200 Subject: [PATCH 004/147] Housekeeper tasks reprocessing --- .../DefaultHousekeeperService.java | 42 ++++++++----- .../HousekeeperReprocessingService.java | 50 ++++++++-------- .../AlarmsUnassignTaskProcessor.java | 8 ++- .../TelemetryDeletionTaskProcessor.java | 2 +- .../src/main/resources/thingsboard.yml | 1 + common/proto/src/main/proto/queue.proto | 2 + .../queue/common/AbstractTbQueueTemplate.java | 4 +- .../queue/settings/TbQueueCoreSettings.java | 2 +- .../dao/entity/AbstractEntityService.java | 31 +--------- .../dao/housekeeper/CleanUpService.java | 60 +++++++++++++++++++ .../data/AlarmsUnassignHousekeeperTask.java | 2 - .../dao/housekeeper/data/HousekeeperTask.java | 9 ++- .../server/dao/rule/BaseRuleChainService.java | 6 +- .../dao/sql/attributes/JpaAttributeDao.java | 21 +++++-- 14 files changed, 153 insertions(+), 87 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 7c7e72b511..f669df5305 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.service.housekeeper; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -40,6 +40,8 @@ import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProce import javax.annotation.PreDestroy; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -49,7 +51,7 @@ import java.util.stream.Collectors; @Slf4j public class DefaultHousekeeperService implements HousekeeperService { - private final Map taskProcessors; + private final Map> taskProcessors; private final TbQueueConsumer> consumer; private final TbQueueProducer> producer; @@ -65,7 +67,8 @@ public class DefaultHousekeeperService implements HousekeeperService { public DefaultHousekeeperService(HousekeeperReprocessingService reprocessingService, TbCoreQueueFactory queueFactory, TbQueueProducerProvider producerProvider, - DataDecodingEncodingService dataDecodingEncodingService, List taskProcessors) { + DataDecodingEncodingService dataDecodingEncodingService, + @Lazy List> taskProcessors) { this.consumer = queueFactory.createHousekeeperMsgConsumer(); this.producer = producerProvider.getHousekeeperMsgProducer(); this.reprocessingService = reprocessingService; @@ -86,7 +89,7 @@ public class DefaultHousekeeperService implements HousekeeperService { for (TbProtoQueueMsg msg : msgs) { try { - processTask(msg); + processTask(msg.getValue()); } catch (Exception e) { log.error("Message processing failed", e); } @@ -107,19 +110,18 @@ public class DefaultHousekeeperService implements HousekeeperService { log.info("Started Housekeeper service"); } - protected void processTask(TbProtoQueueMsg msg) { - HousekeeperTask task = dataDecodingEncodingService.decode(msg.getValue().getTask().getValue().toByteArray()).get(); - HousekeeperTaskProcessor taskProcessor = taskProcessors.get(task.getTaskType()); - if (taskProcessor == null) { - log.error("Unsupported task type {}: {}", task.getTaskType(), task); - return; - } + @SuppressWarnings("unchecked") + protected void processTask(ToHousekeeperServiceMsg msg) { + HousekeeperTask task = dataDecodingEncodingService.decode(msg.getTask().getValue().toByteArray()).get(); + HousekeeperTaskProcessor taskProcessor = getTaskProcessor(task.getTaskType()); - log.info("[{}] Processing task: {}", task.getTenantId(), task); + log.info("[{}][{}][{}] Processing task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); try { - taskProcessor.process(task); + taskProcessor.process((T) task); } catch (Exception e) { - log.error("[{}] Task processing failed: {}", task.getTenantId(), task, e); + log.error("[{}][{}][{}] {} task processing failed, submitting for reprocessing (attempt {}): {}", + task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), + task.getTaskType(), msg.getTask().getAttempt(), task, e); reprocessingService.submitForReprocessing(msg); } } @@ -127,11 +129,20 @@ public class DefaultHousekeeperService implements HousekeeperService { @Override public void submitTask(HousekeeperTask task) { TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - producer.send(tpi, new TbProtoQueueMsg<>(Uuids.timeBased(), ToHousekeeperServiceMsg.newBuilder() + producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ToHousekeeperServiceMsg.newBuilder() .setTask(HousekeeperTaskProto.newBuilder() .setValue(ByteString.copyFrom(dataDecodingEncodingService.encode(task))) + .setTs(task.getTs()) + .setAttempt(0) .build()) .build()), null); + log.trace("[{}][{}][{}] Submitted task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); + } + + @SuppressWarnings("unchecked") + private HousekeeperTaskProcessor getTaskProcessor(HousekeeperTaskType taskType) { + return Optional.ofNullable((HousekeeperTaskProcessor) taskProcessors.get(taskType)) + .orElseThrow(() -> new IllegalArgumentException("Unsupported task type " + taskType)); } @PreDestroy @@ -141,4 +152,5 @@ public class DefaultHousekeeperService implements HousekeeperService { consumer.unsubscribe(); consumerExecutor.shutdownNow(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 234cd07c36..4fca14e44f 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -15,16 +15,15 @@ */ package org.thingsboard.server.service.housekeeper; -import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.TbQueueMsgHeaders; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @@ -33,13 +32,10 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import javax.annotation.PreDestroy; import java.util.List; -import java.util.Optional; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToLong; -import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.longToBytes; - @TbCoreComponent @Service @Slf4j @@ -77,15 +73,21 @@ public class HousekeeperReprocessingService { return; } - for (TbProtoQueueMsg msg : msgs) { - long msgTs = Uuids.unixTimestamp(msg.getKey()); - if (msgTs >= startTs) { - stop(); - return; // fixme: we should commit already reprocessed messages + if (msgs.stream().anyMatch(msg -> { + boolean newMsg = msg.getValue().getTask().getTs() >= startTs; + if (newMsg) { + log.info("Stopping reprocessing due to msg is new {}", msg); } - + return newMsg; + })) { + stop(); // fixme: we should commit already reprocessed messages; maybe submit for reprocessing again and commit? + // msg batch size should be 1. otherwise some tasks won't be reprocessed + return; + } + for (TbProtoQueueMsg msg : msgs) { try { - reprocessTask(msg); + housekeeperService.processTask(msg.getValue());// fixme: or should we submit to queue? + Thread.sleep(1000); } catch (Exception e) { log.error("Message processing failed", e); } @@ -106,21 +108,19 @@ public class HousekeeperReprocessingService { log.info("Started Housekeeper tasks reprocessing"); } - private void reprocessTask(TbProtoQueueMsg msg) { - housekeeperService.processTask(msg);// fixme: or should we submit to queue? - } - - public void submitForReprocessing(TbProtoQueueMsg msg) { - TbQueueMsgHeaders msgHeaders = msg.getHeaders(); - long reprocessingAttempts = Optional.ofNullable(msgHeaders.get("reprocessingAttempts")) - .map(header -> bytesToLong(header)) - .orElse(0L); - reprocessingAttempts++; - msgHeaders.put("reprocessingAttempts", longToBytes(reprocessingAttempts)); + public void submitForReprocessing(ToHousekeeperServiceMsg msg) { + HousekeeperTaskProto task = msg.getTask(); + int attempt = task.getAttempt() + 1; + msg = msg.toBuilder() + .setTask(task.toBuilder() + .setAttempt(attempt) + .setTs(System.currentTimeMillis()) + .build()) + .build(); var producer = producerProvider.getHousekeeperDelayedMsgProducer(); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - producer.send(tpi, new TbProtoQueueMsg<>(Uuids.timeBased(), msg.getValue(), msgHeaders), null); + producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); } @PreDestroy diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java index 54bf7989af..247466af65 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java @@ -16,21 +16,27 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; import org.thingsboard.server.dao.housekeeper.data.AlarmsUnassignHousekeeperTask; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; +import java.util.List; + @Component @RequiredArgsConstructor +@Slf4j public class AlarmsUnassignTaskProcessor implements HousekeeperTaskProcessor { private final TbAlarmService alarmService; @Override public void process(AlarmsUnassignHousekeeperTask task) throws Exception { - alarmService.unassignDeletedUserAlarms(task.getTenantId(), (UserId) task.getEntityId(), task.getUserTitle(), task.getTs()); + List alarms = alarmService.unassignDeletedUserAlarms(task.getTenantId(), (UserId) task.getEntityId(), task.getUserTitle(), task.getTs()); + log.trace("[{}][{}] Unassigned {} alarms", task.getTenantId(), task.getEntityId(), alarms.size()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java index 2a491148ea..e925c9f20c 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -35,7 +35,7 @@ public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor< @Override public HousekeeperTaskType getTaskType() { - return HousekeeperTaskType.DELETE_ATTRIBUTES; + return HousekeeperTaskType.DELETE_TELEMETRY; } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 17b4c85cfb..6a96c0a3db 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1565,6 +1565,7 @@ queue: print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" housekeeper: topic: "tb_housekeeper" + reprocessing-topic: "tb_housekeeper.reprocessing" poll-interval-ms: "1000" vc: # Default topic name for Kafka, RabbitMQ, etc. diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 0dbd96a53d..c4e8e64e42 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1398,4 +1398,6 @@ message ToHousekeeperServiceMsg { message HousekeeperTaskProto { bytes value = 1; + int64 ts = 2; + int32 attempt = 3; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java index 9340cafa67..f11eaaef48 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java @@ -46,13 +46,13 @@ public class AbstractTbQueueTemplate { return new String(data, StandardCharsets.UTF_8); } - public static byte[] longToBytes(long x) { + protected static byte[] longToBytes(long x) { ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); longBuffer.putLong(0, x); return longBuffer.array(); } - public static long bytesToLong(byte[] bytes) { + protected static long bytesToLong(byte[] bytes) { return ByteBuffer.wrap(bytes).getLong(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java index 4edb688235..0b66c2fa75 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java @@ -37,7 +37,7 @@ public class TbQueueCoreSettings { @Value("${queue.core.housekeeper.topic:tb_housekeeper}") private String housekeeperTopic; - @Value("${queue.core.housekeeper.topic:tb_housekeeper.delayed}") + @Value("${queue.core.housekeeper.reprocessing-topic:tb_housekeeper.reprocessing}") private String housekeeperDelayedTopic; @Value("${queue.core.partitions}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 7bfe76e753..6b88f19699 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -20,11 +20,8 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; -import org.springframework.transaction.event.TransactionalEventListener; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,10 +30,8 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.housekeeper.HousekeeperService; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.CleanUpService; import org.thingsboard.server.dao.relation.RelationService; import java.util.Collections; @@ -70,28 +65,8 @@ public abstract class AbstractEntityService { protected EdgeService edgeService; @Autowired - protected HousekeeperService housekeeperService; - - @TransactionalEventListener(fallbackExecution = true) // todo: consider moving this to HousekeeperService - public void onEntityDeleted(DeleteEntityEvent event) { - TenantId tenantId = event.getTenantId(); - EntityId entityId = event.getEntityId(); - log.trace("[{}] DeleteEntityEvent handler: {}", tenantId, event); - - cleanUpRelatedData(tenantId, entityId); - if (entityId.getEntityType() == EntityType.USER) { - housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); - } - } - - protected void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { - // todo: skipped entities list - relationService.deleteEntityRelations(tenantId, entityId); - housekeeperService.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); - } + @Lazy + protected CleanUpService cleanUpService; protected void createRelation(TenantId tenantId, EntityRelation relation) { log.debug("Creating relation: {}", relation); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java new file mode 100644 index 0000000000..0086e17926 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -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. + */ +package org.thingsboard.server.dao.housekeeper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.relation.RelationService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CleanUpService { + + private final HousekeeperService housekeeperService; + private final RelationService relationService; + + @TransactionalEventListener(fallbackExecution = true) // todo: consider moving this to HousekeeperService + public void handleEntityDeletionEvent(DeleteEntityEvent event) { + TenantId tenantId = event.getTenantId(); + EntityId entityId = event.getEntityId(); + log.trace("[{}] DeleteEntityEvent handler: {}", tenantId, event); + + log.info("[{}][{}][{}] Handling DeleteEntityEvent", tenantId, entityId.getEntityType(), entityId.getId()); + cleanUpRelatedData(tenantId, entityId); + if (entityId.getEntityType() == EntityType.USER) { + housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); + } + } + + public void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { + // todo: skipped entities list + relationService.deleteEntityRelations(tenantId, entityId); + housekeeperService.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java index 876a8f736b..aa1858f59f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java @@ -22,12 +22,10 @@ import org.thingsboard.server.common.data.User; public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { private final String userTitle; - private final long ts; protected AlarmsUnassignHousekeeperTask(User user) { super(user.getTenantId(), user.getId(), HousekeeperTaskType.UNASSIGN_ALARMS); this.userTitle = user.getTitle(); - this.ts = System.currentTimeMillis(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java index 726e07a790..9fcbbfe43d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java @@ -15,27 +15,26 @@ */ package org.thingsboard.server.dao.housekeeper.data; -import lombok.Getter; +import lombok.Data; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import java.io.Serializable; -/* - * on start, read the retry queue and put the messages back to main queue (save offset) - * */ -@Getter +@Data public class HousekeeperTask implements Serializable { private final TenantId tenantId; private final EntityId entityId; private final HousekeeperTaskType taskType; + private final long ts; protected HousekeeperTask(TenantId tenantId, EntityId entityId, HousekeeperTaskType taskType) { this.tenantId = tenantId; this.entityId = entityId; this.taskType = taskType; + this.ts = System.currentTimeMillis(); } public static HousekeeperTask deleteAttributes(TenantId tenantId, EntityId entityId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 88fa6f1351..ebbec8969f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -177,7 +177,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC List updatedRuleNodes = new ArrayList<>(); List existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId()); for (RuleNode existingNode : existingRuleNodes) { - cleanUpRelatedData(tenantId, existingNode.getId()); // fixme: for sure? + cleanUpService.cleanUpRelatedData(tenantId, existingNode.getId()); // fixme: for sure? Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { @@ -771,7 +771,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private void deleteRuleNodes(TenantId tenantId, List ruleNodes) { List ruleNodeIds = ruleNodes.stream().map(RuleNode::getId).collect(Collectors.toList()); for (var node : ruleNodes) { - cleanUpRelatedData(tenantId, node.getId()); + cleanUpService.cleanUpRelatedData(tenantId, node.getId()); } ruleNodeDao.deleteByIdIn(ruleNodeIds); } @@ -820,7 +820,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private void deleteRuleNode(TenantId tenantId, EntityId entityId) { ruleNodeDao.removeById(tenantId, entityId.getId()); - cleanUpRelatedData(tenantId, entityId); + cleanUpService.cleanUpRelatedData(tenantId, entityId); } private final PaginatedRemover tenantRuleChainsRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index cb51a7d8ad..d8cf6aab32 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -20,9 +20,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -31,6 +33,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.attributes.AttributesDao; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; @@ -132,10 +135,10 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Override public List findAll(TenantId tenantId, EntityId entityId, String attributeType) { return DaoUtil.convertDataList(Lists.newArrayList( - attributeKvRepository.findAllByEntityTypeAndEntityIdAndAttributeType( - entityId.getEntityType(), - entityId.getId(), - attributeType))); + attributeKvRepository.findAllByEntityTypeAndEntityIdAndAttributeType( + entityId.getEntityType(), + entityId.getId(), + attributeType))); } @Override @@ -188,6 +191,16 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl return futuresList; } + @Transactional + @Override + public List> removeAllByEntityId(TenantId tenantId, EntityId entityId) { + return jdbcTemplate.queryForList("DELETE FROM attribute_kv WHERE entity_type = ? and entity_id = ? " + + "RETURNING attribute_type, attribute_key", entityId.getEntityType().name(), entityId.getId()).stream() + .map(deleted -> Pair.of((String) deleted.get(ModelConstants.ATTRIBUTE_TYPE_COLUMN), + (String) deleted.get(ModelConstants.ATTRIBUTE_KEY_COLUMN))) + .collect(Collectors.toList()); + } + private AttributeKvCompositeKey getAttributeKvCompositeKey(EntityId entityId, String attributeType, String attributeKey) { return new AttributeKvCompositeKey( entityId.getEntityType(), From a1bede3cbb4ab914b21b05c37b51afd0006809a8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 14 Feb 2024 20:20:26 +0200 Subject: [PATCH 005/147] Attributes deletion task processor --- .../AttributesDeletionTaskProcessor.java | 5 +- .../dao/attributes/AttributesService.java | 2 + .../server/dao/attributes/AttributesDao.java | 4 + .../dao/attributes/BaseAttributesService.java | 8 ++ .../attributes/CachedAttributesService.java | 94 +++++++++++-------- 5 files changed, 71 insertions(+), 42 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java index 169ac3e573..e3bac35e9b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; @@ -23,13 +24,15 @@ import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor +@Slf4j public class AttributesDeletionTaskProcessor implements HousekeeperTaskProcessor { private final AttributesService attributesService; @Override public void process(HousekeeperTask task) throws Exception { -// attributesService.removeAll(task.getTenantId(), task.getEntityId(), DataConstants.CLIENT_SCOPE); + int deletedCount = attributesService.removeAllByEntityId(task.getTenantId(), task.getEntityId()); + log.trace("[{}][{}][{}] Deleted {} attributes", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), deletedCount); } @Override diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 3dd9626739..4f0840c704 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -49,4 +49,6 @@ public interface AttributesService { List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds, String scope); + int removeAllByEntityId(TenantId tenantId, EntityId entityId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index 08a9419885..8cfc6be57b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -46,4 +47,7 @@ public interface AttributesDao { List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds); List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, EntityType entityType, List entityIds, String attributeType); + + List> removeAllByEntityId(TenantId tenantId, EntityId entityId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 342cb26485..1cace5b71c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; @@ -113,4 +114,11 @@ public class BaseAttributesService implements AttributesService { validate(entityId, scope); return Futures.allAsList(attributesDao.removeAll(tenantId, entityId, scope, attributeKeys)); } + + @Override + public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { + List> deleted = attributesDao.removeAllByEntityId(tenantId, entityId); + return deleted.size(); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 1666015a48..1751d33312 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; @@ -147,47 +148,47 @@ public class CachedAttributesService implements AttributesService { return Futures.transformAsync(cacheExecutor.submit(() -> findCachedAttributes(entityId, scope, attributeKeys)), wrappedCachedAttributes -> { - List cachedAttributes = wrappedCachedAttributes.values().stream() - .map(TbCacheValueWrapper::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - if (wrappedCachedAttributes.size() == attributeKeys.size()) { - log.trace("[{}][{}] Found all attributes from cache: {}", entityId, scope, attributeKeys); - return Futures.immediateFuture(cachedAttributes); - } - - Set notFoundAttributeKeys = new HashSet<>(attributeKeys); - notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); - - List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); - - // DB call should run in DB executor, not in cache-related executor - return jpaExecutorService.submit(() -> { - var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); - try { - log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); - List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); - for (AttributeKvEntry foundInDbAttribute : result) { - AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); - cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); - notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); - } - for (String key : notFoundAttributeKeys) { - cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); - } - List mergedAttributes = new ArrayList<>(cachedAttributes); - mergedAttributes.addAll(result); - cacheTransaction.commit(); - log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys); - return mergedAttributes; - } catch (Throwable e) { - cacheTransaction.rollback(); - log.debug("Could not find attributes from cache: [{}] [{}] [{}]", entityId, scope, notFoundAttributeKeys, e); - throw e; - } - }); - - }, MoreExecutors.directExecutor()); // cacheExecutor analyse and returns results or submit to DB executor + List cachedAttributes = wrappedCachedAttributes.values().stream() + .map(TbCacheValueWrapper::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (wrappedCachedAttributes.size() == attributeKeys.size()) { + log.trace("[{}][{}] Found all attributes from cache: {}", entityId, scope, attributeKeys); + return Futures.immediateFuture(cachedAttributes); + } + + Set notFoundAttributeKeys = new HashSet<>(attributeKeys); + notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); + + List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); + + // DB call should run in DB executor, not in cache-related executor + return jpaExecutorService.submit(() -> { + var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); + try { + log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); + List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); + for (AttributeKvEntry foundInDbAttribute : result) { + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); + cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); + notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); + } + for (String key : notFoundAttributeKeys) { + cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); + } + List mergedAttributes = new ArrayList<>(cachedAttributes); + mergedAttributes.addAll(result); + cacheTransaction.commit(); + log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys); + return mergedAttributes; + } catch (Throwable e) { + cacheTransaction.rollback(); + log.debug("Could not find attributes from cache: [{}] [{}] [{}]", entityId, scope, notFoundAttributeKeys, e); + throw e; + } + }); + + }, MoreExecutors.directExecutor()); // cacheExecutor analyse and returns results or submit to DB executor } private Map> findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { @@ -268,4 +269,15 @@ public class CachedAttributesService implements AttributesService { }, cacheExecutor)).collect(Collectors.toList())); } + @Override + public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { + List> result = attributesDao.removeAllByEntityId(tenantId, entityId); + result.forEach(deleted -> { + String scope = deleted.getKey(); + String key = deleted.getValue(); + cache.evict(new AttributeCacheKey(scope, entityId, key)); + }); + return result.size(); + } + } From faa93de1027bacffe781611b5dfbc60f25b268c5 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 15 Feb 2024 14:26:33 +0200 Subject: [PATCH 006/147] Tenant deletion with housekeeper --- .../DefaultHousekeeperService.java | 7 +++- .../HousekeeperReprocessingService.java | 5 ++- ...ava => EntitiesDeletionTaskProcessor.java} | 18 +++++--- .../server/dao/entity/EntityDaoService.java | 4 ++ .../server/common/data/EntityType.java | 3 +- .../dao/asset/AssetProfileServiceImpl.java | 5 +++ .../server/dao/asset/BaseAssetService.java | 5 +++ .../dao/customer/CustomerServiceImpl.java | 5 +++ .../dao/dashboard/DashboardServiceImpl.java | 5 +++ .../dao/device/DeviceProfileServiceImpl.java | 5 +++ .../server/dao/device/DeviceServiceImpl.java | 6 +++ .../server/dao/edge/EdgeServiceImpl.java | 5 +++ .../dao/entityview/EntityViewServiceImpl.java | 5 +++ .../dao/housekeeper/CleanUpService.java | 9 ++++ .../dao/housekeeper/HousekeeperService.java | 5 +++ .../data/EntitiesDeletionHousekeeperTask.java | 32 +++++++++++++++ .../dao/housekeeper/data/HousekeeperTask.java | 5 +++ .../housekeeper/data/HousekeeperTaskType.java | 2 +- .../DefaultNotificationRequestService.java | 5 +++ .../DefaultNotificationRuleService.java | 5 +++ .../DefaultNotificationTargetService.java | 5 +++ .../DefaultNotificationTemplateService.java | 5 +++ .../server/dao/ota/BaseOtaPackageService.java | 5 +++ .../server/dao/queue/BaseQueueService.java | 5 +++ .../dao/resource/BaseResourceService.java | 5 +++ .../server/dao/rpc/BaseRpcService.java | 5 +++ .../server/dao/rule/BaseRuleChainService.java | 5 +++ .../settings/AdminSettingsServiceImpl.java | 23 ++++++++++- .../dao/sql/attributes/JpaAttributeDao.java | 4 +- .../dao/sql/user/JpaUserSettingsDao.java | 6 +++ .../dao/sql/user/UserSettingsRepository.java | 11 +++++ .../server/dao/tenant/TenantServiceImpl.java | 41 ++++++------------- .../usagerecord/ApiUsageStateServiceImpl.java | 5 +++ .../server/dao/user/UserServiceImpl.java | 20 +++++++++ .../server/dao/user/UserSettingsDao.java | 3 ++ .../dao/widget/WidgetTypeServiceImpl.java | 5 +++ .../dao/widget/WidgetsBundleServiceImpl.java | 5 +++ 37 files changed, 258 insertions(+), 41 deletions(-) rename application/src/main/java/org/thingsboard/server/service/housekeeper/processor/{EntityDeletionTaskProcessor.java => EntitiesDeletionTaskProcessor.java} (53%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index f669df5305..9361baae2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -128,8 +128,13 @@ public class DefaultHousekeeperService implements HousekeeperService { @Override public void submitTask(HousekeeperTask task) { + submitTask(UUID.randomUUID(), task); + } + + @Override + public void submitTask(UUID key, HousekeeperTask task) { TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ToHousekeeperServiceMsg.newBuilder() + producer.send(tpi, new TbProtoQueueMsg<>(key, ToHousekeeperServiceMsg.newBuilder() .setTask(HousekeeperTaskProto.newBuilder() .setValue(ByteString.copyFrom(dataDecodingEncodingService.encode(task))) .setTs(task.getTs()) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 4fca14e44f..ac0051def1 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -49,7 +49,7 @@ public class HousekeeperReprocessingService { @Value("${queue.core.housekeeper.poll-interval-ms:10000}") private int pollInterval; - private final long startTs = System.currentTimeMillis(); + private final long startTs = System.currentTimeMillis(); // fixme: some other tb-core might start earlier and submit for reprocessing private boolean stopped; // todo: stats @@ -114,12 +114,13 @@ public class HousekeeperReprocessingService { msg = msg.toBuilder() .setTask(task.toBuilder() .setAttempt(attempt) - .setTs(System.currentTimeMillis()) + .setTs(System.currentTimeMillis()) // maybe set ts + 1 hour so that no-one reprocesses it immediately .build()) .build(); var producer = producerProvider.getHousekeeperDelayedMsgProducer(); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); + // fixme submit with the same msg key, so that the messages goes to this consumer and will not be processed by anyone else producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java similarity index 53% rename from application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java rename to application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java index deb693c466..da7af3c42b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java @@ -17,21 +17,29 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; +import org.thingsboard.server.dao.housekeeper.data.EntitiesDeletionHousekeeperTask; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor -public class EntityDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class EntitiesDeletionTaskProcessor implements HousekeeperTaskProcessor { - @Override - public void process(HousekeeperTask task) throws Exception { + private final EntityServiceRegistry entityServiceRegistry; + @Override + public void process(EntitiesDeletionHousekeeperTask task) throws Exception { + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(task.getEntityType()); + if (entityService == null) { + throw new IllegalArgumentException("Unsupported entity type " + task.getEntityType()); + } + entityService.deleteByTenantId(task.getTenantId()); } @Override public HousekeeperTaskType getTaskType() { - return HousekeeperTaskType.DELETE_ENTITY; + return HousekeeperTaskType.DELETE_ENTITIES; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java index b1f6ab9cdb..992c4fad94 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java @@ -34,6 +34,10 @@ public interface EntityDaoService { throw new IllegalArgumentException(getEntityType().getNormalName() + " deletion not supported"); } + default void deleteByTenantId(TenantId tenantId) { + throw new IllegalArgumentException("Deletion by tenant id not supported for " + getEntityType().getNormalName()); + } + EntityType getEntityType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index fb4fe1011e..6316ab1281 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -58,7 +58,8 @@ public enum EntityType { NOTIFICATION_TEMPLATE (30), NOTIFICATION_REQUEST (31), NOTIFICATION (32), - NOTIFICATION_RULE (33); + NOTIFICATION_RULE (33), + ADMIN_SETTINGS(34); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 851e68d3dc..686b75befb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -304,6 +304,11 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findAssetProfileById(tenantId, new AssetProfileId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index c6550f7e25..a57b232ec2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -279,6 +279,11 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 028b6ff59b..bd8ddb4059 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -180,6 +180,11 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom customersByTenantRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteCustomersByTenantId(tenantId); + } + private PaginatedRemover customersByTenantRemover = new PaginatedRemover() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index e08653856b..37abeb993c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -272,6 +272,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb tenantDashboardsRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteDashboardsByTenantId(tenantId); + } + @Override public PageData findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index f9be1c9988..ceebe8f7b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -359,6 +359,11 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findDeviceProfileById(tenantId, new DeviceProfileId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 01894a8cb8..7a8daa6d7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -422,6 +422,12 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 118449b698..0c95a6f0c7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -266,6 +266,11 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findEdgesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 6940b42794..7a5f873a25 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -344,6 +344,11 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService> findEntityViewTypesByTenantId(TenantId tenantId) { log.trace("Executing findEntityViewTypesByTenantId, tenantId [{}]", tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 0086e17926..1097ec5ece 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -27,6 +27,8 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; import org.thingsboard.server.dao.relation.RelationService; +import java.util.UUID; + @Component @RequiredArgsConstructor @Slf4j @@ -57,4 +59,11 @@ public class CleanUpService { housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); } + public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { + UUID tasksKey = UUID.randomUUID(); // so that all tasks are processed synchronously from one partition + for (EntityType entityType : entityTypes) { + housekeeperService.submitTask(tasksKey, HousekeeperTask.deleteEntities(tenantId, entityType)); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java index 85577a2296..5e55318953 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java @@ -17,8 +17,13 @@ package org.thingsboard.server.dao.housekeeper; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import java.util.UUID; + public interface HousekeeperService { void submitTask(HousekeeperTask task); + // tasks with the same key will be pushed to the same partition and thus processed synchronously + void submitTask(UUID key, HousekeeperTask task); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java new file mode 100644 index 0000000000..6c87fa9d3c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java @@ -0,0 +1,32 @@ +/** + * 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.dao.housekeeper.data; + +import lombok.Getter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; + +@Getter +public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { + + private final EntityType entityType; + + protected EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { + super(tenantId, null, HousekeeperTaskType.DELETE_ENTITIES); + this.entityType = entityType; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java index 9fcbbfe43d..dfcf445d8e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.housekeeper.data; import lombok.Data; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -57,4 +58,8 @@ public class HousekeeperTask implements Serializable { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ENTITY_ALARMS); } + public static HousekeeperTask deleteEntities(TenantId tenantId, EntityType entityType) { + return new EntitiesDeletionHousekeeperTask(tenantId, entityType); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java index 22951280c1..51fb9ab926 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.housekeeper.data; public enum HousekeeperTaskType { - DELETE_ENTITY, + DELETE_ENTITIES, DELETE_ATTRIBUTES, DELETE_TELEMETRY, // maybe divide into latest and ts kv history? DELETE_EVENTS, diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java index 2ad0a59824..9573539297 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java @@ -104,6 +104,11 @@ public class DefaultNotificationRequestService implements NotificationRequestSer notificationRequestDao.removeByTenantId(tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteNotificationRequestsByTenantId(tenantId); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findNotificationRequestById(tenantId, new NotificationRequestId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java index 909febe6a8..77a7cd96e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java @@ -93,6 +93,11 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem notificationRuleDao.removeByTenantId(tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteNotificationRulesByTenantId(tenantId); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findNotificationRuleById(tenantId, new NotificationRuleId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index a92e4f6a4e..5996ec6c90 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -201,6 +201,11 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl notificationTargetDao.removeByTenantId(tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteNotificationTargetsByTenantId(tenantId); + } + @Override public long countNotificationTargetsByTenantId(TenantId tenantId) { return notificationTargetDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index 17b166c3ec..6cf8239f47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -89,6 +89,11 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im notificationTemplateDao.removeByTenantId(tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteNotificationTemplatesByTenantId(tenantId); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findNotificationTemplateById(tenantId, new NotificationTemplateId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 53a585c358..e4a4865f02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -230,6 +230,11 @@ public class BaseOtaPackageService extends AbstractCachedEntityService tenantOtaPackageRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index 57c45b1d0b..ebbb69078c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -129,6 +129,11 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ tenantQueuesRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteQueuesByTenantId(tenantId); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findQueueById(tenantId, new QueueId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 35e13bff3a..a80772b506 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -203,6 +203,11 @@ public class BaseResourceService extends AbstractCachedEntityService> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findResourceInfoById(tenantId, new TbResourceId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java index 54d7a2170c..9c65d1efe9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java @@ -66,6 +66,11 @@ public class BaseRpcService implements RpcService { tenantRpcRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteAllRpcByTenantId(tenantId); + } + @Override public Rpc findById(TenantId tenantId, RpcId rpcId) { log.trace("Executing findById, tenantId [{}], rpcId [{}]", tenantId, rpcId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index ebbec8969f..fe685d61e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -443,6 +443,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC tenantRuleChainsRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteRuleChainsByTenantId(tenantId); + } + @Override public RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) { Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request."); diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index bfc99d1b54..1100d40d22 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -21,14 +21,20 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AdminSettingsId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; +import java.util.Optional; + @Service @Slf4j -public class AdminSettingsServiceImpl implements AdminSettingsService { +public class AdminSettingsServiceImpl implements AdminSettingsService, EntityDaoService { @Autowired private AdminSettingsDao adminSettingsDao; @@ -105,4 +111,19 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { } } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteAdminSettingsByTenantId(tenantId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.empty(); + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index d8cf6aab32..3730718afa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -196,8 +196,8 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl public List> removeAllByEntityId(TenantId tenantId, EntityId entityId) { return jdbcTemplate.queryForList("DELETE FROM attribute_kv WHERE entity_type = ? and entity_id = ? " + "RETURNING attribute_type, attribute_key", entityId.getEntityType().name(), entityId.getId()).stream() - .map(deleted -> Pair.of((String) deleted.get(ModelConstants.ATTRIBUTE_TYPE_COLUMN), - (String) deleted.get(ModelConstants.ATTRIBUTE_KEY_COLUMN))) + .map(row -> Pair.of((String) row.get(ModelConstants.ATTRIBUTE_TYPE_COLUMN), + (String) row.get(ModelConstants.ATTRIBUTE_KEY_COLUMN))) .collect(Collectors.toList()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java index 1feabaacb5..914806e829 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.dao.DaoUtil; @@ -50,4 +51,9 @@ public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService i userSettingsRepository.deleteById(id); } + @Override + public void removeByUserId(TenantId tenantId, UserId userId) { + userSettingsRepository.deleteByUserId(userId.getId()); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java index cb81675a80..0a8d3e431a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java @@ -16,9 +16,20 @@ package org.thingsboard.server.dao.sql.user; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.dao.model.sql.UserSettingsEntity; +import java.util.UUID; + public interface UserSettingsRepository extends JpaRepository { + @Transactional + @Modifying + @Query("DELETE FROM UserSettingsEntity s WHERE s.userId = :userId") + void deleteByUserId(@Param("userId") UUID userId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 6fb2a93d54..0ca5e76a93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -211,41 +211,26 @@ public class TenantServiceImpl extends AbstractCachedEntityService userValidator; private final DataValidator userCredentialsValidator; private final ApplicationEventPublisher eventPublisher; @@ -255,6 +256,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic validateId(userId, INCORRECT_USER_ID + userId); userCredentialsDao.removeByUserId(tenantId, userId); userAuthSettingsDao.removeByUserId(userId); + userSettingsDao.removeByUserId(tenantId, userId); userDao.removeById(tenantId, userId.getId()); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); @@ -313,6 +315,12 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic tenantAdminsRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId); + usersRemover.removeEntities(tenantId, tenantId); + } + @Override public PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findCustomerUsers, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); @@ -464,6 +472,18 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } }; + private final PaginatedRemover usersRemover = new PaginatedRemover<>() { + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return findUsersByTenantId(tenantId, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, User user) { + deleteUser(tenantId, user); + } + }; + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findUserById(tenantId, new UserId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java index f2e5474c58..aac0b8f4de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.user; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; @@ -27,4 +28,6 @@ public interface UserSettingsDao { void removeById(TenantId tenantId, UserSettingsCompositeKey key); + void removeByUserId(TenantId tenantId, UserId userId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 7f7d4d5ae8..aad37a8f66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -226,6 +226,11 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { tenantWidgetTypeRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteWidgetTypesByTenantId(tenantId); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findWidgetTypeById(tenantId, new WidgetTypeId(entityId.getId()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 9d6960a7d1..1631bfbdbf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -179,6 +179,11 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { tenantWidgetsBundleRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteWidgetsBundlesByTenantId(tenantId); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId()))); From 4cbd57c923f2509fe9449c1bff21c06fdf698977 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 16 Feb 2024 13:34:26 +0200 Subject: [PATCH 007/147] Telemetry deletion task; task processing timeout --- .../DefaultHousekeeperService.java | 37 ++++++++++++++----- .../HousekeeperReprocessingService.java | 31 +++++----------- .../TelemetryDeletionTaskProcessor.java | 14 ++++++- .../dao/attributes/BaseAttributesService.java | 1 + .../data/EntitiesDeletionHousekeeperTask.java | 2 +- .../dao/housekeeper/data/HousekeeperTask.java | 3 +- 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 9361baae2b..10d53f3500 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -42,8 +42,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @TbCoreComponent @@ -57,10 +61,13 @@ public class DefaultHousekeeperService implements HousekeeperService { private final TbQueueProducer> producer; private final HousekeeperReprocessingService reprocessingService; private final DataDecodingEncodingService dataDecodingEncodingService; + private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); + private final ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); @Value("${queue.core.housekeeper.poll-interval-ms:10000}") private int pollInterval; + private int taskProcessingTimeout = 120; private boolean stopped; @@ -89,9 +96,10 @@ public class DefaultHousekeeperService implements HousekeeperService { for (TbProtoQueueMsg msg : msgs) { try { - processTask(msg.getValue()); - } catch (Exception e) { - log.error("Message processing failed", e); + processTask(msg); + } catch (Throwable e) { + log.error("Unexpected error during message processing [{}]", msg, e); + reprocessingService.submitForReprocessing(msg); } } consumer.commit(); @@ -111,18 +119,29 @@ public class DefaultHousekeeperService implements HousekeeperService { } @SuppressWarnings("unchecked") - protected void processTask(ToHousekeeperServiceMsg msg) { + protected void processTask(TbProtoQueueMsg queueMsg) throws Exception { + ToHousekeeperServiceMsg msg = queueMsg.getValue(); HousekeeperTask task = dataDecodingEncodingService.decode(msg.getTask().getValue().toByteArray()).get(); HousekeeperTaskProcessor taskProcessor = getTaskProcessor(task.getTaskType()); - log.info("[{}][{}][{}] Processing task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); + log.info("[{}][{}][{}] Processing task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task); try { - taskProcessor.process((T) task); - } catch (Exception e) { + Future future = executor.submit(() -> { + taskProcessor.process((T) task); + return null; + }); + future.get(taskProcessingTimeout, TimeUnit.SECONDS); + } catch (ExecutionException executionException) { + Throwable error = executionException.getCause(); log.error("[{}][{}][{}] {} task processing failed, submitting for reprocessing (attempt {}): {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), - task.getTaskType(), msg.getTask().getAttempt(), task, e); - reprocessingService.submitForReprocessing(msg); + task.getTaskType(), msg.getTask().getAttempt(), task, error); + reprocessingService.submitForReprocessing(queueMsg); + } catch (TimeoutException timeoutException) { + log.error("[{}][{}][{}] {} task processing timeout after {} seconds, submitting for reprocessing (attempt {}): {}", + task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), + task.getTaskType(), taskProcessingTimeout, msg.getTask().getAttempt(), task); + reprocessingService.submitForReprocessing(queueMsg); // fixme: we should schedule such task for later (separate queue?) } } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index ac0051def1..9d7114476b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -32,9 +32,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import javax.annotation.PreDestroy; import java.util.List; -import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @TbCoreComponent @Service @@ -46,7 +46,7 @@ public class HousekeeperReprocessingService { private final TbQueueConsumer> consumer; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); - @Value("${queue.core.housekeeper.poll-interval-ms:10000}") + @Value("${queue.core.housekeeper.poll-interval-ms:500}") private int pollInterval; private final long startTs = System.currentTimeMillis(); // fixme: some other tb-core might start earlier and submit for reprocessing @@ -68,28 +68,17 @@ public class HousekeeperReprocessingService { while (!stopped && !consumer.isStopped()) { try { List> msgs = consumer.poll(pollInterval); - if (msgs.isEmpty()) { + if (msgs.isEmpty() || msgs.stream().anyMatch(msg -> msg.getValue().getTask().getTs() >= startTs)) { // msg batch size should be 1. otherwise some tasks won't be reprocessed immediately stop(); return; } - if (msgs.stream().anyMatch(msg -> { - boolean newMsg = msg.getValue().getTask().getTs() >= startTs; - if (newMsg) { - log.info("Stopping reprocessing due to msg is new {}", msg); - } - return newMsg; - })) { - stop(); // fixme: we should commit already reprocessed messages; maybe submit for reprocessing again and commit? - // msg batch size should be 1. otherwise some tasks won't be reprocessed - return; - } for (TbProtoQueueMsg msg : msgs) { try { - housekeeperService.processTask(msg.getValue());// fixme: or should we submit to queue? - Thread.sleep(1000); + housekeeperService.processTask(msg); } catch (Exception e) { - log.error("Message processing failed", e); + log.error("Unexpected error during message reprocessing [{}]", msg, e); + submitForReprocessing(msg); } } consumer.commit(); @@ -108,20 +97,20 @@ public class HousekeeperReprocessingService { log.info("Started Housekeeper tasks reprocessing"); } - public void submitForReprocessing(ToHousekeeperServiceMsg msg) { + public void submitForReprocessing(TbProtoQueueMsg queueMsg) { + ToHousekeeperServiceMsg msg = queueMsg.getValue(); HousekeeperTaskProto task = msg.getTask(); int attempt = task.getAttempt() + 1; msg = msg.toBuilder() .setTask(task.toBuilder() .setAttempt(attempt) - .setTs(System.currentTimeMillis()) // maybe set ts + 1 hour so that no-one reprocesses it immediately + .setTs(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)) // so that it is not reprocessed by anyone in the nearest hour .build()) .build(); var producer = producerProvider.getHousekeeperDelayedMsgProducer(); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - // fixme submit with the same msg key, so that the messages goes to this consumer and will not be processed by anyone else - producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); + producer.send(tpi, new TbProtoQueueMsg<>(queueMsg.getKey(), msg), null); } @PreDestroy diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java index e925c9f20c..7159c43f46 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -16,21 +16,31 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import java.util.List; + @Component @RequiredArgsConstructor +@Slf4j public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor { private final TimeseriesService timeseriesService; @Override public void process(HousekeeperTask task) throws Exception { -// timeseriesService.remove() -// timeseriesService.removeAllLatest(); + List keys = timeseriesService.findAllKeysByEntityIds(task.getTenantId(), List.of(task.getEntityId())); + for (String key : keys) { + DeleteTsKvQuery deleteQuery = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false, true); + timeseriesService.remove(task.getTenantId(), task.getEntityId(), List.of(deleteQuery)).get(); + } + log.trace("[{}][{}][{}] Deleted {} telemetry keys", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), keys.size()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 1cace5b71c..9359383071 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -118,6 +118,7 @@ public class BaseAttributesService implements AttributesService { @Override public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { List> deleted = attributesDao.removeAllByEntityId(tenantId, entityId); + // todo: probably better to retrieve by scope in case there are too many attributes return deleted.size(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java index 6c87fa9d3c..8f4a8d38b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java @@ -25,7 +25,7 @@ public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { private final EntityType entityType; protected EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { - super(tenantId, null, HousekeeperTaskType.DELETE_ENTITIES); + super(tenantId, tenantId, HousekeeperTaskType.DELETE_ENTITIES); this.entityType = entityType; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java index dfcf445d8e..f9389e4566 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.housekeeper.data; import lombok.Data; +import lombok.NonNull; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; @@ -31,7 +32,7 @@ public class HousekeeperTask implements Serializable { private final HousekeeperTaskType taskType; private final long ts; - protected HousekeeperTask(TenantId tenantId, EntityId entityId, HousekeeperTaskType taskType) { + protected HousekeeperTask(@NonNull TenantId tenantId, @NonNull EntityId entityId, @NonNull HousekeeperTaskType taskType) { this.tenantId = tenantId; this.entityId = entityId; this.taskType = taskType; From 721854c8793e5f89c71cfdf05fb2c2320ed6a62b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 20 Feb 2024 18:50:29 +0200 Subject: [PATCH 008/147] Housekeeper: reprocessing improvements --- .../DefaultHousekeeperService.java | 20 ++--- .../HousekeeperReprocessingService.java | 52 ++++++++---- .../EventsDeletionTaskProcessor.java | 1 + common/proto/src/main/proto/queue.proto | 83 ++++++++++--------- .../provider/AwsSqsMonolithQueueFactory.java | 4 +- .../provider/AwsSqsTbCoreQueueFactory.java | 4 +- .../InMemoryMonolithQueueFactory.java | 8 +- .../provider/KafkaMonolithQueueFactory.java | 14 ++-- .../provider/KafkaTbCoreQueueFactory.java | 14 ++-- .../provider/PubSubMonolithQueueFactory.java | 4 +- .../provider/PubSubTbCoreQueueFactory.java | 4 +- .../RabbitMqMonolithQueueFactory.java | 4 +- .../provider/RabbitMqTbCoreQueueFactory.java | 4 +- .../ServiceBusMonolithQueueFactory.java | 4 +- .../ServiceBusTbCoreQueueFactory.java | 4 +- .../queue/provider/TbCoreQueueFactory.java | 5 +- .../provider/TbCoreQueueProducerProvider.java | 9 +- .../provider/TbQueueProducerProvider.java | 2 +- .../TbRuleEngineProducerProvider.java | 2 +- .../TbTransportQueueProducerProvider.java | 2 +- .../TbVersionControlProducerProvider.java | 2 +- .../queue/settings/TbQueueCoreSettings.java | 2 +- 22 files changed, 134 insertions(+), 114 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 10d53f3500..644e3cc2c7 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -99,7 +99,7 @@ public class DefaultHousekeeperService implements HousekeeperService { processTask(msg); } catch (Throwable e) { log.error("Unexpected error during message processing [{}]", msg, e); - reprocessingService.submitForReprocessing(msg); + reprocessingService.submitForReprocessing(msg, e); } } consumer.commit(); @@ -131,23 +131,23 @@ public class DefaultHousekeeperService implements HousekeeperService { return null; }); future.get(taskProcessingTimeout, TimeUnit.SECONDS); - } catch (ExecutionException executionException) { - Throwable error = executionException.getCause(); + } catch (Throwable e) { + Throwable error = e; + if (e instanceof ExecutionException) { + error = error.getCause(); + } else if (e instanceof TimeoutException) { + error = new TimeoutException("Timeout after " + taskProcessingTimeout + " seconds"); + } log.error("[{}][{}][{}] {} task processing failed, submitting for reprocessing (attempt {}): {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType(), msg.getTask().getAttempt(), task, error); - reprocessingService.submitForReprocessing(queueMsg); - } catch (TimeoutException timeoutException) { - log.error("[{}][{}][{}] {} task processing timeout after {} seconds, submitting for reprocessing (attempt {}): {}", - task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), - task.getTaskType(), taskProcessingTimeout, msg.getTask().getAttempt(), task); - reprocessingService.submitForReprocessing(queueMsg); // fixme: we should schedule such task for later (separate queue?) + reprocessingService.submitForReprocessing(queueMsg, error); } } @Override public void submitTask(HousekeeperTask task) { - submitTask(UUID.randomUUID(), task); + submitTask(task.getEntityId().getId(), task); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 9d7114476b..2d2c3242f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -16,18 +16,23 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import javax.annotation.PreDestroy; @@ -42,35 +47,46 @@ import java.util.concurrent.TimeUnit; public class HousekeeperReprocessingService { private final DefaultHousekeeperService housekeeperService; + private final PartitionService partitionService; + private final TbCoreQueueFactory queueFactory; private final TbQueueProducerProvider producerProvider; - private final TbQueueConsumer> consumer; + private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); + private static final int startDelay = 15; // fixme - to 5 minutes + private static final int reprocessingDelay = 30; // seconds + private static final int maxReprocessingAttempts = 5; @Value("${queue.core.housekeeper.poll-interval-ms:500}") private int pollInterval; - private final long startTs = System.currentTimeMillis(); // fixme: some other tb-core might start earlier and submit for reprocessing private boolean stopped; // todo: stats public HousekeeperReprocessingService(@Lazy DefaultHousekeeperService housekeeperService, - TbCoreQueueFactory queueFactory, + PartitionService partitionService, TbCoreQueueFactory queueFactory, TbQueueProducerProvider producerProvider) { this.housekeeperService = housekeeperService; - this.consumer = queueFactory.createHousekeeperDelayedMsgConsumer(); + this.partitionService = partitionService; + this.queueFactory = queueFactory; this.producerProvider = producerProvider; } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void afterStartUp() { + @Scheduled(initialDelay = startDelay, fixedDelay = reprocessingDelay, timeUnit = TimeUnit.SECONDS) + public void startReprocessing() { + if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) { + return; + } + + var consumer = queueFactory.createHousekeeperReprocessingMsgConsumer(); consumer.subscribe(); consumerExecutor.submit(() -> { - while (!stopped && !consumer.isStopped()) { + log.info("Starting Housekeeper tasks reprocessing"); + long startTs = System.currentTimeMillis(); + while (!stopped) { try { List> msgs = consumer.poll(pollInterval); if (msgs.isEmpty() || msgs.stream().anyMatch(msg -> msg.getValue().getTask().getTs() >= startTs)) { // msg batch size should be 1. otherwise some tasks won't be reprocessed immediately - stop(); - return; + break; } for (TbProtoQueueMsg msg : msgs) { @@ -78,7 +94,7 @@ public class HousekeeperReprocessingService { housekeeperService.processTask(msg); } catch (Exception e) { log.error("Unexpected error during message reprocessing [{}]", msg, e); - submitForReprocessing(msg); + submitForReprocessing(msg, e); } } consumer.commit(); @@ -93,31 +109,33 @@ public class HousekeeperReprocessingService { } } } + consumer.unsubscribe(); + log.info("Stopped Housekeeper tasks reprocessing"); }); - log.info("Started Housekeeper tasks reprocessing"); } - public void submitForReprocessing(TbProtoQueueMsg queueMsg) { + // todo: dead letter queue if attempts count exceeds the configured maximum + public void submitForReprocessing(TbProtoQueueMsg queueMsg, Throwable error) { ToHousekeeperServiceMsg msg = queueMsg.getValue(); HousekeeperTaskProto task = msg.getTask(); int attempt = task.getAttempt() + 1; msg = msg.toBuilder() .setTask(task.toBuilder() .setAttempt(attempt) - .setTs(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)) // so that it is not reprocessed by anyone in the nearest hour + .addErrors(StringUtils.truncate(ExceptionUtils.getStackTrace(error), 1024)) + .setTs(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(reprocessingDelay)) .build()) .build(); - var producer = producerProvider.getHousekeeperDelayedMsgProducer(); + log.trace("Submitting for reprocessing: {}", msg); + var producer = producerProvider.getHousekeeperReprocessingMsgProducer(); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); producer.send(tpi, new TbProtoQueueMsg<>(queueMsg.getKey(), msg), null); } @PreDestroy private void stop() { - log.info("Stopped Housekeeper tasks reprocessing"); stopped = true; - consumer.unsubscribe(); consumerExecutor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java index 56edf01637..b95d7fc1e3 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java @@ -29,6 +29,7 @@ public class EventsDeletionTaskProcessor implements HousekeeperTaskProcessor> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index a1be25f29d..1a4f92fcd3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -217,12 +217,12 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 1ee8b404a5..310bc75fe4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -171,13 +171,13 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { - return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())); + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { - return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())); + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 5ee9b6f53b..b4c2220ac1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -373,22 +373,22 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) - .clientId("monolith-housekeeper-delayed-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) + .clientId("monolith-housekeeper-reprocessing-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) .admin(housekeeperAdmin) .build(); } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) - .clientId("monolith-housekeeper-delayed-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName("monolith-housekeeper-delayed-consumer")) + .topic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) + .clientId("monolith-housekeeper-reprocessing-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("monolith-housekeeper-reprocessing-consumer")) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(housekeeperAdmin) .statsService(consumerStatsService) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index c01ba32f70..635eb3e300 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -330,22 +330,22 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) - .clientId("tb-core-housekeeper-delayed-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) + .clientId("tb-core-housekeeper-reprocessing-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) .admin(housekeeperAdmin) .build(); } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName(coreSettings.getHousekeeperDelayedTopic())) - .clientId("tb-core-housekeeper-delayed-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName("tb-core-housekeeper-delayed-consumer")) + .topic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) + .clientId("tb-core-housekeeper-reprocessing-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(topicService.buildTopicName("tb-core-housekeeper-reprocessing-consumer")) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(housekeeperAdmin) .statsService(consumerStatsService) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index a63a9fc16b..e6a23b8068 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -239,12 +239,12 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 5f1f282e39..1b52a2ba42 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -210,12 +210,12 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 802b08773d..fa89956b64 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -236,12 +236,12 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index ceb6e03ca5..b1f3de672f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -209,12 +209,12 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 1e9c1cadaf..5f4be98ca0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -235,12 +235,12 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index e39df45b82..579fba272c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -210,12 +210,12 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperDelayedMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return null; } @Override - public TbQueueConsumer> createHousekeeperDelayedMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 279a469415..866eb931f2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -16,7 +16,6 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; @@ -137,8 +136,8 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory { TbQueueConsumer> createHousekeeperMsgConsumer(); - TbQueueProducer> createHousekeeperDelayedMsgProducer(); + TbQueueProducer> createHousekeeperReprocessingMsgProducer(); - TbQueueConsumer> createHousekeeperDelayedMsgConsumer(); + TbQueueConsumer> createHousekeeperReprocessingMsgConsumer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 4fa22cb7e3..e6bc8f0484 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -16,7 +16,6 @@ package org.thingsboard.server.queue.provider; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; @@ -44,7 +43,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; private TbQueueProducer> toHousekeeper; - private TbQueueProducer> toHousekeeperDelayed; + private TbQueueProducer> toHousekeeperReprocessing; public TbCoreQueueProducerProvider(TbCoreQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -60,7 +59,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toVersionControl = tbQueueProvider.createVersionControlMsgProducer(); this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); - this.toHousekeeperDelayed = tbQueueProvider.createHousekeeperDelayedMsgProducer(); + this.toHousekeeperReprocessing = tbQueueProvider.createHousekeeperReprocessingMsgProducer(); } @Override @@ -104,8 +103,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getHousekeeperDelayedMsgProducer() { - return toHousekeeperDelayed; + public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { + return toHousekeeperReprocessing; } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index 5cdf390f47..ff56d2e8be 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -82,6 +82,6 @@ public interface TbQueueProducerProvider { TbQueueProducer> getHousekeeperMsgProducer(); - TbQueueProducer> getHousekeeperDelayedMsgProducer(); + TbQueueProducer> getHousekeeperReprocessingMsgProducer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index c5ddf57fc0..ee6700906c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -96,7 +96,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Rule Engine!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index e8dd7431a6..869c588fc7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -92,7 +92,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 7ddd2109ba..3f4bd209e4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -88,7 +88,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getHousekeeperDelayedMsgProducer() { + public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java index 0b66c2fa75..2aa01193b0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java @@ -38,7 +38,7 @@ public class TbQueueCoreSettings { private String housekeeperTopic; @Value("${queue.core.housekeeper.reprocessing-topic:tb_housekeeper.reprocessing}") - private String housekeeperDelayedTopic; + private String housekeeperReprocessingTopic; @Value("${queue.core.partitions}") private int partitions; From b72af4ead902737e490ae93cdb5696e15ca84e9a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 21 Feb 2024 18:49:15 +0200 Subject: [PATCH 009/147] Housekeeper tasks encoded to json --- .../DefaultHousekeeperService.java | 26 +++++++++---------- .../HousekeeperReprocessingService.java | 18 ++++++++++--- .../src/main/resources/thingsboard.yml | 4 +++ .../data/notification/NotificationType.java | 1 + common/proto/src/main/proto/queue.proto | 2 +- .../queue/kafka/TbKafkaTopicConfigs.java | 5 ++++ .../provider/KafkaMonolithQueueFactory.java | 6 +++-- .../provider/KafkaTbCoreQueueFactory.java | 6 +++-- .../data/AlarmsUnassignHousekeeperTask.java | 8 +++--- .../data/EntitiesDeletionHousekeeperTask.java | 8 +++--- .../dao/housekeeper/data/HousekeeperTask.java | 21 ++++++++++++--- 11 files changed, 73 insertions(+), 32 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 644e3cc2c7..76bf40fc36 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.service.housekeeper; -import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.housekeeper.HousekeeperService; @@ -40,7 +40,6 @@ import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProce import javax.annotation.PreDestroy; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -95,8 +94,11 @@ public class DefaultHousekeeperService implements HousekeeperService { } for (TbProtoQueueMsg msg : msgs) { + log.trace("Processing task: {}", msg); try { processTask(msg); + } catch (InterruptedException e) { + return; } catch (Throwable e) { log.error("Unexpected error during message processing [{}]", msg, e); reprocessingService.submitForReprocessing(msg, e); @@ -121,16 +123,20 @@ public class DefaultHousekeeperService implements HousekeeperService { @SuppressWarnings("unchecked") protected void processTask(TbProtoQueueMsg queueMsg) throws Exception { ToHousekeeperServiceMsg msg = queueMsg.getValue(); - HousekeeperTask task = dataDecodingEncodingService.decode(msg.getTask().getValue().toByteArray()).get(); - HousekeeperTaskProcessor taskProcessor = getTaskProcessor(task.getTaskType()); + HousekeeperTask task = JacksonUtil.fromString(msg.getTask().getValue(), HousekeeperTask.class); + HousekeeperTaskProcessor taskProcessor = (HousekeeperTaskProcessor) taskProcessors.get(task.getTaskType()); + if (taskProcessor == null) { + throw new IllegalArgumentException("Unsupported task type " + task.getTaskType()); + } - log.info("[{}][{}][{}] Processing task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task); try { Future future = executor.submit(() -> { taskProcessor.process((T) task); return null; }); future.get(taskProcessingTimeout, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw e; } catch (Throwable e) { Throwable error = e; if (e instanceof ExecutionException) { @@ -152,21 +158,15 @@ public class DefaultHousekeeperService implements HousekeeperService { @Override public void submitTask(UUID key, HousekeeperTask task) { + log.trace("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); producer.send(tpi, new TbProtoQueueMsg<>(key, ToHousekeeperServiceMsg.newBuilder() .setTask(HousekeeperTaskProto.newBuilder() - .setValue(ByteString.copyFrom(dataDecodingEncodingService.encode(task))) + .setValue(JacksonUtil.toString(task)) .setTs(task.getTs()) .setAttempt(0) .build()) .build()), null); - log.trace("[{}][{}][{}] Submitted task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); - } - - @SuppressWarnings("unchecked") - private HousekeeperTaskProcessor getTaskProcessor(HousekeeperTaskType taskType) { - return Optional.ofNullable((HousekeeperTaskProcessor) taskProcessors.get(taskType)) - .orElseThrow(() -> new IllegalArgumentException("Unsupported task type " + taskType)); } @PreDestroy diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 2d2c3242f9..44b2728089 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -28,7 +28,8 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; -import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; @@ -36,7 +37,9 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import javax.annotation.PreDestroy; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -90,11 +93,15 @@ public class HousekeeperReprocessingService { } for (TbProtoQueueMsg msg : msgs) { + log.trace("Reprocessing task: {}", msg); try { housekeeperService.processTask(msg); - } catch (Exception e) { + } catch (InterruptedException e) { + return; + } catch (Throwable e) { log.error("Unexpected error during message reprocessing [{}]", msg, e); submitForReprocessing(msg, e); + // fixme: msgs are duplicated } } consumer.commit(); @@ -118,12 +125,15 @@ public class HousekeeperReprocessingService { public void submitForReprocessing(TbProtoQueueMsg queueMsg, Throwable error) { ToHousekeeperServiceMsg msg = queueMsg.getValue(); HousekeeperTaskProto task = msg.getTask(); + int attempt = task.getAttempt() + 1; + Set errors = new LinkedHashSet<>(task.getErrorsList()); + errors.add(StringUtils.truncate(ExceptionUtils.getStackTrace(error), 1024)); msg = msg.toBuilder() .setTask(task.toBuilder() .setAttempt(attempt) - .addErrors(StringUtils.truncate(ExceptionUtils.getStackTrace(error), 1024)) - .setTs(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(reprocessingDelay)) + .clearErrors().addAllErrors(errors) + .setTs(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis((long) (reprocessingDelay * 0.8))) .build()) .build(); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 7af241d41e..f59e3c86b7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1403,6 +1403,9 @@ queue: tb_housekeeper: - key: max.poll.records value: "1" + tb_housekeeper.reprocessing: + - key: max.poll.records + value: "1" other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms @@ -1425,6 +1428,7 @@ queue: # Kafka properties for Version Control topic version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" housekeeper: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1" + housekeeper-reprocessing: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java index cfc91d8fca..5a8fd87933 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java @@ -31,4 +31,5 @@ public enum NotificationType { RATE_LIMITS, EDGE_CONNECTION, EDGE_COMMUNICATION_FAILURE + } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 73cea54a47..8e757a3b14 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1424,7 +1424,7 @@ message ToHousekeeperServiceMsg { } message HousekeeperTaskProto { - bytes value = 1; + string value = 1; int64 ts = 2; // reprocessing diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index d3a5eb821d..915f61c8b4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -45,6 +45,8 @@ public class TbKafkaTopicConfigs { private String vcProperties; @Value("${queue.kafka.topic-properties.housekeeper:}") private String housekeeperProperties; + @Value("${queue.kafka.topic-properties.housekeeper-reprocessing:}") + private String housekeeperReprocessingProperties; @Getter private Map coreConfigs; @@ -66,6 +68,8 @@ public class TbKafkaTopicConfigs { private Map vcConfigs; @Getter private Map housekeeperConfigs; + @Getter + private Map housekeeperReprocessingConfigs; @PostConstruct private void init() { @@ -81,6 +85,7 @@ public class TbKafkaTopicConfigs { fwUpdatesConfigs = PropertyUtils.getProps(fwUpdatesProperties); vcConfigs = PropertyUtils.getProps(vcProperties); housekeeperConfigs = PropertyUtils.getProps(housekeeperProperties); + housekeeperReprocessingConfigs = PropertyUtils.getProps(housekeeperReprocessingProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index c8a11e1f32..95bf2d1195 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -84,6 +84,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin vcAdmin; private final TbQueueAdmin housekeeperAdmin; + private final TbQueueAdmin housekeeperReprocessingAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -118,6 +119,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); + this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); } @Override @@ -378,7 +380,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .settings(kafkaSettings) .clientId("monolith-housekeeper-reprocessing-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) - .admin(housekeeperAdmin) + .admin(housekeeperReprocessingAdmin) .build(); } @@ -390,7 +392,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .clientId("monolith-housekeeper-reprocessing-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("monolith-housekeeper-reprocessing-consumer")) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) - .admin(housekeeperAdmin) + .admin(housekeeperReprocessingAdmin) .statsService(consumerStatsService) .build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index f2812564e5..478963d300 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -82,6 +82,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin vcAdmin; private final TbQueueAdmin housekeeperAdmin; + private final TbQueueAdmin housekeeperReprocessingAdmin; public KafkaTbCoreQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -115,6 +116,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); + this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); } @Override @@ -335,7 +337,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .settings(kafkaSettings) .clientId("tb-core-housekeeper-reprocessing-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) - .admin(housekeeperAdmin) + .admin(housekeeperReprocessingAdmin) .build(); } @@ -347,7 +349,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .clientId("tb-core-housekeeper-reprocessing-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("tb-core-housekeeper-reprocessing-consumer")) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) - .admin(housekeeperAdmin) + .admin(housekeeperReprocessingAdmin) .statsService(consumerStatsService) .build(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java index aa1858f59f..aea09c093c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.dao.housekeeper.data; -import lombok.Getter; +import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.User; -@Getter +@Data +@EqualsAndHashCode(callSuper = true) public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { - private final String userTitle; + private String userTitle; protected AlarmsUnassignHousekeeperTask(User user) { super(user.getTenantId(), user.getId(), HousekeeperTaskType.UNASSIGN_ALARMS); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java index 8f4a8d38b0..7846feb229 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java @@ -15,14 +15,16 @@ */ package org.thingsboard.server.dao.housekeeper.data; -import lombok.Getter; +import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -@Getter +@Data +@EqualsAndHashCode(callSuper = true) public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { - private final EntityType entityType; + private EntityType entityType; protected EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { super(tenantId, tenantId, HousekeeperTaskType.DELETE_ENTITIES); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java index f9389e4566..6b63d43d43 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java @@ -15,7 +15,13 @@ */ package org.thingsboard.server.dao.housekeeper.data; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.AccessLevel; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.NonNull; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -24,13 +30,20 @@ import org.thingsboard.server.common.data.id.TenantId; import java.io.Serializable; +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY, defaultImpl = HousekeeperTask.class) +@JsonSubTypes({ + @Type(name = "DELETE_ENTITIES", value = EntitiesDeletionHousekeeperTask.class), + @Type(name = "UNASSIGN_ALARMS", value = AlarmsUnassignHousekeeperTask.class) +}) @Data +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class HousekeeperTask implements Serializable { - private final TenantId tenantId; - private final EntityId entityId; - private final HousekeeperTaskType taskType; - private final long ts; + private TenantId tenantId; + private EntityId entityId; + private HousekeeperTaskType taskType; + private long ts; protected HousekeeperTask(@NonNull TenantId tenantId, @NonNull EntityId entityId, @NonNull HousekeeperTaskType taskType) { this.tenantId = tenantId; From 7cc599b5b59ab0f65b78049e7c5b2db729618c9a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 22 Feb 2024 13:58:15 +0200 Subject: [PATCH 010/147] Housekeeper stats; Grafana dashboard --- .../DefaultHousekeeperService.java | 23 +- .../HousekeeperReprocessingService.java | 33 +- .../AlarmsUnassignTaskProcessor.java | 2 +- .../AttributesDeletionTaskProcessor.java | 2 +- .../EventsDeletionTaskProcessor.java | 3 +- .../TelemetryDeletionTaskProcessor.java | 2 +- .../stats/HousekeeperStatsService.java | 117 ++++++ .../src/main/resources/thingsboard.yml | 11 +- .../common/stats/DefaultStatsFactory.java | 20 +- .../server/common/stats/StatsFactory.java | 5 + .../server/common/stats/StatsType.java | 7 +- .../provisioning/dashboards/housekeeper.json | 390 ++++++++++++++++++ 12 files changed, 588 insertions(+), 27 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java create mode 100644 docker/monitoring/grafana/provisioning/dashboards/housekeeper.json diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 76bf40fc36..a324f279fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -33,9 +33,9 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.AfterStartUp; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; +import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; import javax.annotation.PreDestroy; import java.util.List; @@ -59,27 +59,28 @@ public class DefaultHousekeeperService implements HousekeeperService { private final TbQueueConsumer> consumer; private final TbQueueProducer> producer; private final HousekeeperReprocessingService reprocessingService; - private final DataDecodingEncodingService dataDecodingEncodingService; + private final HousekeeperStatsService statsService; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); private final ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); - @Value("${queue.core.housekeeper.poll-interval-ms:10000}") + @Value("${queue.core.housekeeper.task-processing-timeout-ms:120000}") + private int taskProcessingTimeout; + @Value("${queue.core.housekeeper.poll-interval-ms:500}") private int pollInterval; - private int taskProcessingTimeout = 120; private boolean stopped; public DefaultHousekeeperService(HousekeeperReprocessingService reprocessingService, TbCoreQueueFactory queueFactory, TbQueueProducerProvider producerProvider, - DataDecodingEncodingService dataDecodingEncodingService, + HousekeeperStatsService statsService, @Lazy List> taskProcessors) { this.consumer = queueFactory.createHousekeeperMsgConsumer(); this.producer = producerProvider.getHousekeeperMsgProducer(); this.reprocessingService = reprocessingService; + this.statsService = statsService; this.taskProcessors = taskProcessors.stream().collect(Collectors.toMap(HousekeeperTaskProcessor::getTaskType, p -> p)); - this.dataDecodingEncodingService = dataDecodingEncodingService; } @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) @@ -129,18 +130,23 @@ public class DefaultHousekeeperService implements HousekeeperService { throw new IllegalArgumentException("Unsupported task type " + task.getTaskType()); } + if (log.isDebugEnabled()) { + log.debug("[{}][{}][{}] {} task {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), + msg.getTask().getErrorsCount() == 0 ? "Processing" : "Reprocessing", task.getTaskType()); + } try { Future future = executor.submit(() -> { taskProcessor.process((T) task); return null; }); - future.get(taskProcessingTimeout, TimeUnit.SECONDS); + future.get(taskProcessingTimeout, TimeUnit.MILLISECONDS); + statsService.reportProcessed(task, msg); } catch (InterruptedException e) { throw e; } catch (Throwable e) { Throwable error = e; if (e instanceof ExecutionException) { - error = error.getCause(); + error = e.getCause(); } else if (e instanceof TimeoutException) { error = new TimeoutException("Timeout after " + taskProcessingTimeout + " seconds"); } @@ -148,6 +154,7 @@ public class DefaultHousekeeperService implements HousekeeperService { task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType(), msg.getTask().getAttempt(), task, error); reprocessingService.submitForReprocessing(queueMsg, error); + statsService.reportFailure(task, msg); } } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 44b2728089..de88b2dcea 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.StringUtils; @@ -28,20 +27,20 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; -import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; +import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @TbCoreComponent @@ -54,16 +53,19 @@ public class HousekeeperReprocessingService { private final TbCoreQueueFactory queueFactory; private final TbQueueProducerProvider producerProvider; - private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); - - private static final int startDelay = 15; // fixme - to 5 minutes - private static final int reprocessingDelay = 30; // seconds - private static final int maxReprocessingAttempts = 5; + @Value("${queue.core.housekeeper.reprocessing-start-delay-sec:15}") // fixme: to 5 minutes + private int startDelay; + @Value("${queue.core.housekeeper.task-reprocessing-delay-sec:30}") // fixme: to 30 minutes or 1 hour + private int reprocessingDelay; + @Value("${queue.core.housekeeper.max-reprocessing-attempts:10}") + private int maxReprocessingAttempts; @Value("${queue.core.housekeeper.poll-interval-ms:500}") private int pollInterval; + private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-scheduler")); + private boolean stopped; - // todo: stats public HousekeeperReprocessingService(@Lazy DefaultHousekeeperService housekeeperService, PartitionService partitionService, TbCoreQueueFactory queueFactory, @@ -74,7 +76,17 @@ public class HousekeeperReprocessingService { this.producerProvider = producerProvider; } - @Scheduled(initialDelay = startDelay, fixedDelay = reprocessingDelay, timeUnit = TimeUnit.SECONDS) + @PostConstruct + private void init() { + scheduler.scheduleWithFixedDelay(() -> { + try { + startReprocessing(); + } catch (Throwable e) { + log.error("Unexpected error during reprocessing", e); + } + }, startDelay, reprocessingDelay, TimeUnit.SECONDS); + } + public void startReprocessing() { if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) { return; @@ -146,6 +158,7 @@ public class HousekeeperReprocessingService { @PreDestroy private void stop() { stopped = true; + scheduler.shutdownNow(); consumerExecutor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java index 247466af65..68fd05deec 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java @@ -36,7 +36,7 @@ public class AlarmsUnassignTaskProcessor implements HousekeeperTaskProcessor alarms = alarmService.unassignDeletedUserAlarms(task.getTenantId(), (UserId) task.getEntityId(), task.getUserTitle(), task.getTs()); - log.trace("[{}][{}] Unassigned {} alarms", task.getTenantId(), task.getEntityId(), alarms.size()); + log.debug("[{}][{}] Unassigned {} alarms", task.getTenantId(), task.getEntityId(), alarms.size()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java index e3bac35e9b..0467e50aeb 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java @@ -32,7 +32,7 @@ public class AttributesDeletionTaskProcessor implements HousekeeperTaskProcessor @Override public void process(HousekeeperTask task) throws Exception { int deletedCount = attributesService.removeAllByEntityId(task.getTenantId(), task.getEntityId()); - log.trace("[{}][{}][{}] Deleted {} attributes", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), deletedCount); + log.debug("[{}][{}][{}] Deleted {} attributes", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), deletedCount); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java index b95d7fc1e3..481f9f1bbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java @@ -24,16 +24,17 @@ import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; @Component @RequiredArgsConstructor public class EventsDeletionTaskProcessor implements HousekeeperTaskProcessor { + private final EventService eventService; @Override public void process(HousekeeperTask task) throws Exception { eventService.removeEvents(task.getTenantId(), task.getEntityId(), null, 0L, System.currentTimeMillis()); - throw new RuntimeException("test error"); } @Override public HousekeeperTaskType getTaskType() { return HousekeeperTaskType.DELETE_EVENTS; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java index 7159c43f46..e7da740261 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -40,7 +40,7 @@ public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor< DeleteTsKvQuery deleteQuery = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false, true); timeseriesService.remove(task.getTenantId(), task.getEntityId(), List.of(deleteQuery)).get(); } - log.trace("[{}][{}][{}] Deleted {} telemetry keys", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), keys.size()); + log.debug("[{}][{}][{}] Deleted {} telemetry keys", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), keys.size()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java new file mode 100644 index 0000000000..4fcc13837d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -0,0 +1,117 @@ +/** + * 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.housekeeper.stats; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.stats.DefaultCounter; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class HousekeeperStatsService { + + private final Map stats = new EnumMap<>(HousekeeperTaskType.class); + + public HousekeeperStatsService(StatsFactory statsFactory) { + for (HousekeeperTaskType taskType : HousekeeperTaskType.values()) { + stats.put(taskType, new HousekeeperStats(taskType, statsFactory)); + } + } + + @Scheduled(initialDelay = 60, fixedDelay = 60, timeUnit = TimeUnit.SECONDS) + private void reportStats() { + String statsStr = stats.values().stream().map(stats -> { + String countersStr = stats.getCounters().stream() + .filter(counter -> counter.get() > 0) + .map(counter -> counter.getName() + " = " + counter.get()) + .collect(Collectors.joining(", ")); + if (countersStr.isEmpty()) { + return null; + } else { + return stats.getTaskType() + " {" + countersStr + "}"; + } + }).filter(Objects::nonNull).collect(Collectors.joining("; ")); + + if (!statsStr.isEmpty()) { + stats.values().forEach(HousekeeperStats::reset); + log.info("Housekeeper stats: {}", statsStr); + } + } + + public void reportProcessed(HousekeeperTask task, ToHousekeeperServiceMsg msg) { + HousekeeperStats stats = this.stats.get(task.getTaskType()); + if (msg.getTask().getErrorsCount() == 0) { + stats.getProcessedCounter().increment(); + } else { + stats.getReprocessedCounter().increment(); + } + } + + public void reportFailure(HousekeeperTask task, ToHousekeeperServiceMsg msg) { + HousekeeperStats stats = this.stats.get(task.getTaskType()); + if (msg.getTask().getErrorsCount() == 0) { + stats.getFailedProcessingCounter().increment(); + } else { + stats.getFailedReprocessingCounter().increment(); + } + } + + @Getter + static class HousekeeperStats { + private final HousekeeperTaskType taskType; + private final List counters = new ArrayList<>(); + + private final StatsCounter processedCounter; + private final StatsCounter failedProcessingCounter; + private final StatsCounter reprocessedCounter; + private final StatsCounter failedReprocessingCounter; + + public HousekeeperStats(HousekeeperTaskType taskType, StatsFactory statsFactory) { + this.taskType = taskType; + this.processedCounter = register("processed", statsFactory); + this.failedProcessingCounter = register("failedProcessing", statsFactory); + this.reprocessedCounter = register("reprocessed", statsFactory); + this.failedReprocessingCounter = register("failedReprocessing", statsFactory); + } + + private StatsCounter register(String statsName, StatsFactory statsFactory) { + StatsCounter counter = statsFactory.createStatsCounter(StatsType.HOUSEKEEPER.getName(), statsName, Map.of("taskType", taskType.name())); + counters.add(counter); + return counter; + } + + public void reset() { + counters.forEach(DefaultCounter::clear); + } + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f59e3c86b7..4ced0655f0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1585,9 +1585,14 @@ queue: # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" housekeeper: - topic: "tb_housekeeper" - reprocessing-topic: "tb_housekeeper.reprocessing" - poll-interval-ms: "1000" + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" + reprocessing-topic: "${TB_HOUSEKEEPER_REPROCESSING_TOPIC:tb_housekeeper.reprocessing}" + poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" + task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" + reprocessing-start-delay-sec: "${TB_HOUSEKEEPER_REPROCESSING_START_DELAY_SEC:15}" # fixme: to 5 minutes + task-reprocessing-delay-sec: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_SEC:30}" # fixme: to 30 minutes or 1 hour + max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:30}" + vc: # Default topic name for Kafka, RabbitMQ, etc. topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index a6ec01d089..459f2e0a68 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -25,6 +25,10 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.StringUtils; import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @Service @@ -62,10 +66,24 @@ public class DefaultStatsFactory implements StatsFactory { @Override public StatsCounter createStatsCounter(String key, String statsName) { + return createStatsCounter(key, statsName, Collections.emptyMap()); + } + + @Override + public StatsCounter createStatsCounter(String key, String statsName, Map tags) { + String[] tagsArr = new String[]{STATS_NAME_TAG, statsName}; + if (!tags.isEmpty()) { + List tagsList = new ArrayList<>(List.of(tagsArr)); + tags.forEach((name, value) -> { + tagsList.add(name); + tagsList.add(value); + }); + tagsArr = tagsList.toArray(String[]::new); + } return new StatsCounter( new AtomicInteger(0), metricsEnabled ? - meterRegistry.counter(key, STATS_NAME_TAG, statsName) + meterRegistry.counter(key, tagsArr) : STUB_COUNTER, statsName ); diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java index 8e6989016b..90a4191ee2 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java @@ -17,9 +17,14 @@ package org.thingsboard.server.common.stats; import io.micrometer.core.instrument.Timer; +import java.util.Map; + public interface StatsFactory { + StatsCounter createStatsCounter(String key, String statsName); + StatsCounter createStatsCounter(String key, String statsName, Map tags); + DefaultCounter createDefaultCounter(String key, String... tags); T createGauge(String key, T number, String... tags); diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java index 318f0418de..6c854ac8d6 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java @@ -16,7 +16,12 @@ package org.thingsboard.server.common.stats; public enum StatsType { - RULE_ENGINE("ruleEngine"), CORE("core"), TRANSPORT("transport"), JS_INVOKE("jsInvoke"), RATE_EXECUTOR("rateExecutor"); + RULE_ENGINE("ruleEngine"), + CORE("core"), + TRANSPORT("transport"), + JS_INVOKE("jsInvoke"), + RATE_EXECUTOR("rateExecutor"), + HOUSEKEEPER("housekeeper"); private String name; diff --git a/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json b/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json new file mode 100644 index 0000000000..8c68cc9428 --- /dev/null +++ b/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json @@ -0,0 +1,390 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "liveNow": false, + "panels": [ + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "9BonzvTSz" + }, + "exemplar": true, + "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"processed\"}[1m]))", + "interval": "", + "legendFormat": "{{taskType}}", + "refId": "A" + } + ], + "title": "Processed", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "9BonzvTSz" + }, + "exemplar": true, + "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"failedProcessing\"}[1m]))", + "interval": "", + "legendFormat": "{{taskType}}", + "refId": "A" + } + ], + "title": "Processing failures", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "9BonzvTSz" + }, + "exemplar": true, + "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"reprocessed\"}[1m]))", + "interval": "", + "legendFormat": "{{taskType}}", + "refId": "A" + } + ], + "title": "Reprocessed", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 10 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "9BonzvTSz" + }, + "exemplar": true, + "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"failedReprocessing\"}[1m]))", + "interval": "", + "legendFormat": "{{taskType}}", + "refId": "A" + } + ], + "title": "Reprocessing failures", + "type": "timeseries" + } + ], + "refresh": "10s", + "schemaVersion": 35, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Housekeeper", + "uid": "JFJb7voIz", + "version": 5, + "weekStart": "" +} \ No newline at end of file From 85f54200195d4b8e8dca78aecb4b77492c15aa14 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 22 Feb 2024 14:03:16 +0200 Subject: [PATCH 011/147] Revert proto formatting --- common/proto/src/main/proto/queue.proto | 78 ++++++++++++------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 8e757a3b14..3ac8c90a1d 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -24,36 +24,36 @@ option java_outer_classname = "TransportProtos"; * Common data structures */ enum EntityTypeProto { - UNSPECIFIED = 0; - TENANT = 1; - CUSTOMER = 2; - USER = 3; - DASHBOARD = 4; - ASSET = 5; - DEVICE = 6; - ALARM = 7; + UNSPECIFIED = 0; + TENANT = 1; + CUSTOMER = 2; + USER = 3; + DASHBOARD = 4; + ASSET = 5; + DEVICE = 6; + ALARM = 7; // next 3 reserved for PE; - RULE_CHAIN = 11; - RULE_NODE = 12; + RULE_CHAIN = 11; + RULE_NODE = 12; // next 2 reserved for PE; - ENTITY_VIEW = 15; - WIDGETS_BUNDLE = 16; - WIDGET_TYPE = 17; + ENTITY_VIEW = 15; + WIDGETS_BUNDLE = 16; + WIDGET_TYPE = 17; // next 2 reserved for PE; - TENANT_PROFILE = 20; - DEVICE_PROFILE = 21; - ASSET_PROFILE = 22; - API_USAGE_STATE = 23; - TB_RESOURCE = 24; - OTA_PACKAGE = 25; - EDGE = 26; - RPC = 27; - QUEUE = 28; - NOTIFICATION_TARGET = 29; + TENANT_PROFILE = 20; + DEVICE_PROFILE = 21; + ASSET_PROFILE = 22; + API_USAGE_STATE = 23; + TB_RESOURCE = 24; + OTA_PACKAGE = 25; + EDGE = 26; + RPC = 27; + QUEUE = 28; + NOTIFICATION_TARGET = 29; NOTIFICATION_TEMPLATE = 30; - NOTIFICATION_REQUEST = 31; - NOTIFICATION = 32; - NOTIFICATION_RULE = 33; + NOTIFICATION_REQUEST = 31; + NOTIFICATION = 32; + NOTIFICATION_RULE = 33; } /** @@ -133,9 +133,9 @@ message KeyValueProto { } enum AttributeScopeProto { - CLIENT_SCOPE = 0; - SERVER_SCOPE = 1; - SHARED_SCOPE = 2; + CLIENT_SCOPE = 0; + SERVER_SCOPE = 1; + SHARED_SCOPE = 2; } message AttributeKey { @@ -259,7 +259,7 @@ message GetOrCreateDeviceFromGatewayResponseMsg { } enum TransportApiRequestErrorCode { - UNKNOWN_TRANSPORT_API_ERROR = 0; + UNKNOWN_TRANSPORT_API_ERROR = 0; ENTITY_LIMIT = 1; } @@ -918,15 +918,15 @@ message FromDeviceRPCResponseProto { } enum ComponentLifecycleEvent { - CREATED = 0; - STARTED = 1; - ACTIVATED = 2; - SUSPENDED = 3; - UPDATED = 4; - STOPPED = 5; - DELETED = 6; - FAILED = 7; - DEACTIVATED = 8; + CREATED = 0; + STARTED = 1; + ACTIVATED = 2; + SUSPENDED = 3; + UPDATED = 4; + STOPPED = 5; + DELETED = 6; + FAILED = 7; + DEACTIVATED = 8; } message ComponentLifecycleMsgProto { From 886c448d61afc621da9b91c32139cf14c3e5725f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 22 Feb 2024 14:57:40 +0200 Subject: [PATCH 012/147] Clean up tenant service --- .../DefaultHousekeeperService.java | 18 +-- .../HousekeeperReprocessingService.java | 10 +- .../stats/HousekeeperStatsService.java | 8 +- ...mponentLifecycleEventTriggerProcessor.java | 2 +- .../server/common/data/EntityType.java | 3 +- .../dao/device/DeviceProfileServiceImpl.java | 6 - .../dao/housekeeper/CleanUpService.java | 8 +- .../data/AlarmsUnassignHousekeeperTask.java | 3 + .../data/EntitiesDeletionHousekeeperTask.java | 3 + .../validator/AssetProfileDataValidator.java | 1 + .../validator/DashboardDataValidator.java | 2 + .../validator/DeviceDataValidator.java | 2 + .../validator/DeviceProfileDataValidator.java | 1 + .../validator/ResourceDataValidator.java | 1 + .../validator/RuleChainDataValidator.java | 1 + .../settings/AdminSettingsServiceImpl.java | 39 ++---- .../server/dao/tenant/TenantServiceImpl.java | 113 +++--------------- .../usagerecord/ApiUsageStateServiceImpl.java | 2 +- 18 files changed, 63 insertions(+), 160 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index a324f279fb..c9054c30fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -97,12 +97,12 @@ public class DefaultHousekeeperService implements HousekeeperService { for (TbProtoQueueMsg msg : msgs) { log.trace("Processing task: {}", msg); try { - processTask(msg); + processTask(msg.getValue()); } catch (InterruptedException e) { return; } catch (Throwable e) { log.error("Unexpected error during message processing [{}]", msg, e); - reprocessingService.submitForReprocessing(msg, e); + reprocessingService.submitForReprocessing(msg.getValue(), e); } } consumer.commit(); @@ -122,8 +122,7 @@ public class DefaultHousekeeperService implements HousekeeperService { } @SuppressWarnings("unchecked") - protected void processTask(TbProtoQueueMsg queueMsg) throws Exception { - ToHousekeeperServiceMsg msg = queueMsg.getValue(); + protected void processTask(ToHousekeeperServiceMsg msg) throws Exception { HousekeeperTask task = JacksonUtil.fromString(msg.getTask().getValue(), HousekeeperTask.class); HousekeeperTaskProcessor taskProcessor = (HousekeeperTaskProcessor) taskProcessors.get(task.getTaskType()); if (taskProcessor == null) { @@ -131,8 +130,9 @@ public class DefaultHousekeeperService implements HousekeeperService { } if (log.isDebugEnabled()) { - log.debug("[{}][{}][{}] {} task {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), - msg.getTask().getErrorsCount() == 0 ? "Processing" : "Reprocessing", task.getTaskType()); + log.debug("[{}] {} task {}", task.getTenantId(), + msg.getTask().getErrorsCount() == 0 ? "Processing" : "Reprocessing", + msg.getTask().getValue()); } try { Future future = executor.submit(() -> { @@ -140,7 +140,7 @@ public class DefaultHousekeeperService implements HousekeeperService { return null; }); future.get(taskProcessingTimeout, TimeUnit.MILLISECONDS); - statsService.reportProcessed(task, msg); + statsService.reportProcessed(task.getTaskType(), msg); } catch (InterruptedException e) { throw e; } catch (Throwable e) { @@ -153,8 +153,8 @@ public class DefaultHousekeeperService implements HousekeeperService { log.error("[{}][{}][{}] {} task processing failed, submitting for reprocessing (attempt {}): {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType(), msg.getTask().getAttempt(), task, error); - reprocessingService.submitForReprocessing(queueMsg, error); - statsService.reportFailure(task, msg); + reprocessingService.submitForReprocessing(msg, error); + statsService.reportFailure(task.getTaskType(), msg); } } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index de88b2dcea..4d361cfba4 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -38,6 +38,7 @@ import javax.annotation.PreDestroy; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -107,12 +108,12 @@ public class HousekeeperReprocessingService { for (TbProtoQueueMsg msg : msgs) { log.trace("Reprocessing task: {}", msg); try { - housekeeperService.processTask(msg); + housekeeperService.processTask(msg.getValue()); } catch (InterruptedException e) { return; } catch (Throwable e) { log.error("Unexpected error during message reprocessing [{}]", msg, e); - submitForReprocessing(msg, e); + submitForReprocessing(msg.getValue(), e); // fixme: msgs are duplicated } } @@ -134,8 +135,7 @@ public class HousekeeperReprocessingService { } // todo: dead letter queue if attempts count exceeds the configured maximum - public void submitForReprocessing(TbProtoQueueMsg queueMsg, Throwable error) { - ToHousekeeperServiceMsg msg = queueMsg.getValue(); + public void submitForReprocessing(ToHousekeeperServiceMsg msg, Throwable error) { HousekeeperTaskProto task = msg.getTask(); int attempt = task.getAttempt() + 1; @@ -152,7 +152,7 @@ public class HousekeeperReprocessingService { log.trace("Submitting for reprocessing: {}", msg); var producer = producerProvider.getHousekeeperReprocessingMsgProducer(); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - producer.send(tpi, new TbProtoQueueMsg<>(queueMsg.getKey(), msg), null); + producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); // reprocessing topic has single partition, so we don't care about the msg key } @PreDestroy diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java index 4fcc13837d..ab4f699ed1 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -67,8 +67,8 @@ public class HousekeeperStatsService { } } - public void reportProcessed(HousekeeperTask task, ToHousekeeperServiceMsg msg) { - HousekeeperStats stats = this.stats.get(task.getTaskType()); + public void reportProcessed(HousekeeperTaskType taskType, ToHousekeeperServiceMsg msg) { + HousekeeperStats stats = this.stats.get(taskType); if (msg.getTask().getErrorsCount() == 0) { stats.getProcessedCounter().increment(); } else { @@ -76,8 +76,8 @@ public class HousekeeperStatsService { } } - public void reportFailure(HousekeeperTask task, ToHousekeeperServiceMsg msg) { - HousekeeperStats stats = this.stats.get(task.getTaskType()); + public void reportFailure(HousekeeperTaskType taskType, ToHousekeeperServiceMsg msg) { + HousekeeperStats stats = this.stats.get(taskType); if (msg.getTask().getErrorsCount() == 0) { stats.getFailedProcessingCounter().increment(); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java index 4c92e9b92b..fc573e0bc4 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java @@ -47,7 +47,7 @@ public class RuleEngineComponentLifecycleEventTriggerProcessor implements Notifi return false; } } - if (!partitionService.resolve(ServiceType.TB_RULE_ENGINE, trigger.getTenantId(), trigger.getComponentId()).isMyPartition()) { + if (!partitionService.isMyPartition(ServiceType.TB_RULE_ENGINE, trigger.getTenantId(), trigger.getComponentId())) { return false; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 6316ab1281..fb4fe1011e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -58,8 +58,7 @@ public enum EntityType { NOTIFICATION_TEMPLATE (30), NOTIFICATION_REQUEST (31), NOTIFICATION (32), - NOTIFICATION_RULE (33), - ADMIN_SETTINGS(34); + NOTIFICATION_RULE (33); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index ceebe8f7b2..c9806a03c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.device; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; @@ -47,7 +46,6 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -91,10 +89,6 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService deviceProfileValidator; - @Lazy - @Autowired - private QueueService queueService; - @Autowired private ImageService imageService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 1097ec5ece..5bdcd341b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -37,13 +37,11 @@ public class CleanUpService { private final HousekeeperService housekeeperService; private final RelationService relationService; - @TransactionalEventListener(fallbackExecution = true) // todo: consider moving this to HousekeeperService + @TransactionalEventListener(fallbackExecution = true) public void handleEntityDeletionEvent(DeleteEntityEvent event) { TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); - log.trace("[{}] DeleteEntityEvent handler: {}", tenantId, event); - - log.info("[{}][{}][{}] Handling DeleteEntityEvent", tenantId, entityId.getEntityType(), entityId.getId()); + log.trace("[{}] Handling entity deletion event: {}", tenantId, event); cleanUpRelatedData(tenantId, entityId); if (entityId.getEntityType() == EntityType.USER) { housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); @@ -60,7 +58,7 @@ public class CleanUpService { } public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { - UUID tasksKey = UUID.randomUUID(); // so that all tasks are processed synchronously from one partition + UUID tasksKey = UUID.randomUUID(); // so that all tasks are pushed to single partition to be processed synchronously for (EntityType entityType : entityTypes) { housekeeperService.submitTask(tasksKey, HousekeeperTask.deleteEntities(tenantId, entityType)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java index aea09c093c..8761229e89 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.dao.housekeeper.data; +import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.User; @Data @EqualsAndHashCode(callSuper = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { private String userTitle; diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java index 7846feb229..e20f3ffe0c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.dao.housekeeper.data; +import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; @Data @EqualsAndHashCode(callSuper = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { private EntityType entityType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java index 19a04fd867..7543c247e0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java @@ -42,6 +42,7 @@ public class AssetProfileDataValidator extends DataValidator { @Lazy private AssetProfileService assetProfileService; @Autowired + @Lazy private TenantService tenantService; @Lazy @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java index 178905f7f6..829da4d5c7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; @@ -28,6 +29,7 @@ import org.thingsboard.server.dao.tenant.TenantService; public class DashboardDataValidator extends DataValidator { @Autowired + @Lazy private TenantService tenantService; @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java index e2047dc448..a8f1bdd972 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; @@ -39,6 +40,7 @@ public class DeviceDataValidator extends AbstractHasOtaPackageValidator private DeviceDao deviceDao; @Autowired + @Lazy private TenantService tenantService; @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 23f3a6bbdb..a06fd5edaa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -86,6 +86,7 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { private WidgetTypeDao widgetTypeDao; @Autowired + @Lazy private TenantService tenantService; @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 222aaa3eeb..2f758b1b9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -53,6 +53,7 @@ public class RuleChainDataValidator extends DataValidator { private RuleChainService ruleChainService; @Autowired + @Lazy private TenantService tenantService; @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index 1100d40d22..095e51082a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -21,21 +21,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AdminSettingsId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; -import java.util.Optional; - @Service @Slf4j -public class AdminSettingsServiceImpl implements AdminSettingsService, EntityDaoService { - +public class AdminSettingsServiceImpl implements AdminSettingsService { + @Autowired private AdminSettingsDao adminSettingsDao; @@ -46,7 +40,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService, EntityDao public AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId) { log.trace("Executing findAdminSettingsById [{}]", adminSettingsId); Validator.validateId(adminSettingsId, "Incorrect adminSettingsId " + adminSettingsId); - return adminSettingsDao.findById(tenantId, adminSettingsId.getId()); + return adminSettingsDao.findById(tenantId, adminSettingsId.getId()); } @Override @@ -65,15 +59,15 @@ public class AdminSettingsServiceImpl implements AdminSettingsService, EntityDao public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { log.trace("Executing saveAdminSettings [{}]", adminSettings); adminSettingsValidator.validate(adminSettings, data -> tenantId); - if (adminSettings.getKey().equals("mail")){ + if (adminSettings.getKey().equals("mail")) { AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); if (mailSettings != null) { JsonNode newJsonValue = adminSettings.getJsonValue(); JsonNode oldJsonValue = mailSettings.getJsonValue(); - if (!newJsonValue.has("password") && oldJsonValue.has("password")){ - ((ObjectNode) newJsonValue).put("password", oldJsonValue.get("password").asText()); + if (!newJsonValue.has("password") && oldJsonValue.has("password")) { + ((ObjectNode) newJsonValue).put("password", oldJsonValue.get("password").asText()); } - if (!newJsonValue.has("refreshToken") && oldJsonValue.has("refreshToken")){ + if (!newJsonValue.has("refreshToken") && oldJsonValue.has("refreshToken")) { ((ObjectNode) newJsonValue).put("refreshToken", oldJsonValue.get("refreshToken").asText()); } dropTokenIfProviderInfoChanged(newJsonValue, oldJsonValue); @@ -98,12 +92,12 @@ public class AdminSettingsServiceImpl implements AdminSettingsService, EntityDao } private void dropTokenIfProviderInfoChanged(JsonNode newJsonValue, JsonNode oldJsonValue) { - if (newJsonValue.has("enableOauth2") && newJsonValue.get("enableOauth2").asBoolean()){ + if (newJsonValue.has("enableOauth2") && newJsonValue.get("enableOauth2").asBoolean()) { if (!newJsonValue.get("providerId").equals(oldJsonValue.get("providerId")) || !newJsonValue.get("clientId").equals(oldJsonValue.get("clientId")) || !newJsonValue.get("clientSecret").equals(oldJsonValue.get("clientSecret")) || !newJsonValue.get("redirectUri").equals(oldJsonValue.get("redirectUri")) || - (newJsonValue.has("providerTenantId") && !newJsonValue.get("providerTenantId").equals(oldJsonValue.get("providerTenantId")))){ + (newJsonValue.has("providerTenantId") && !newJsonValue.get("providerTenantId").equals(oldJsonValue.get("providerTenantId")))) { ((ObjectNode) newJsonValue).put("tokenGenerated", false); ((ObjectNode) newJsonValue).remove("refreshToken"); ((ObjectNode) newJsonValue).remove("refreshTokenExpires"); @@ -111,19 +105,4 @@ public class AdminSettingsServiceImpl implements AdminSettingsService, EntityDao } } - @Override - public void deleteByTenantId(TenantId tenantId) { - deleteAdminSettingsByTenantId(tenantId); - } - - @Override - public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return Optional.empty(); - } - - @Override - public EntityType getEntityType() { - return EntityType.ADMIN_SETTINGS; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index b82cfa8613..6ab9361b19 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -16,9 +16,8 @@ package org.thingsboard.server.dao.tenant; import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; @@ -34,32 +33,17 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.notification.NotificationRequestService; -import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationSettingsService; -import org.thingsboard.server.dao.notification.NotificationTargetService; -import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.ota.OtaPackageService; -import org.thingsboard.server.dao.queue.QueueService; -import org.thingsboard.server.dao.resource.ResourceService; -import org.thingsboard.server.dao.rpc.RpcService; -import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.List; import java.util.Optional; @@ -68,89 +52,22 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TenantDaoService") @Slf4j +@RequiredArgsConstructor public class TenantServiceImpl extends AbstractCachedEntityService implements TenantService { private static final String DEFAULT_TENANT_REGION = "Global"; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - @Autowired - private TenantDao tenantDao; - - @Autowired - private TenantProfileService tenantProfileService; - - @Autowired - @Lazy - private UserService userService; - - @Autowired - private CustomerService customerService; - - @Autowired - private AssetService assetService; - - @Autowired - private AssetProfileService assetProfileService; - - @Autowired - private DeviceService deviceService; - - @Autowired - private DeviceProfileService deviceProfileService; - - @Lazy - @Autowired - private ApiUsageStateService apiUsageStateService; - - @Autowired - private WidgetsBundleService widgetsBundleService; - - @Autowired - private WidgetTypeService widgetTypeService; - - @Autowired - private DashboardService dashboardService; - - @Autowired - private RuleChainService ruleChainService; - - @Autowired - private ResourceService resourceService; - - @Autowired - @Lazy - private OtaPackageService otaPackageService; - - @Autowired - private RpcService rpcService; - - @Autowired - private DataValidator tenantValidator; - - @Lazy - @Autowired - private QueueService queueService; - - @Autowired - private AdminSettingsService adminSettingsService; - - @Autowired - private NotificationSettingsService notificationSettingsService; - - @Autowired - private NotificationRequestService notificationRequestService; - - @Autowired - private NotificationRuleService notificationRuleService; - - @Autowired - private NotificationTemplateService notificationTemplateService; - - @Autowired - private NotificationTargetService notificationTargetService; - - @Autowired - protected TbTransactionalCache existsTenantCache; + private final TenantDao tenantDao; + private final TenantProfileService tenantProfileService; + private final UserService userService; + private final AssetProfileService assetProfileService; + private final DeviceProfileService deviceProfileService; + private final ApiUsageStateService apiUsageStateService; + private final AdminSettingsService adminSettingsService; + private final NotificationSettingsService notificationSettingsService; + private final DataValidator tenantValidator; + private final TbTransactionalCache existsTenantCache; @TransactionalEventListener(classes = TenantEvictEvent.class) @Override @@ -218,16 +135,18 @@ public class TenantServiceImpl extends AbstractCachedEntityService apiUsageStateValidator; public ApiUsageStateServiceImpl(ApiUsageStateDao apiUsageStateDao, TenantProfileDao tenantProfileDao, - TenantService tenantService, @Lazy TimeseriesService tsService, + @Lazy TenantService tenantService, @Lazy TimeseriesService tsService, DataValidator apiUsageStateValidator) { this.apiUsageStateDao = apiUsageStateDao; this.tenantProfileDao = tenantProfileDao; From 570533d528b989de9d1121be2183a2a49392c91c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 26 Feb 2024 17:17:17 +0200 Subject: [PATCH 013/147] UI: Add ar_AR locale --- .../assets/locale/locale.constant-ar_AR.json | 8263 +++++++++++++++++ .../assets/locale/locale.constant-en_US.json | 1 + 2 files changed, 8264 insertions(+) create mode 100644 ui-ngx/src/assets/locale/locale.constant-ar_AR.json diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AR.json b/ui-ngx/src/assets/locale/locale.constant-ar_AR.json new file mode 100644 index 0000000000..104eb18abd --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AR.json @@ -0,0 +1,8263 @@ +{ + "access": { + "unauthorized": "غير مصرح لك", + "unauthorized-access": "وصول غير مصرح", + "unauthorized-access-text": "يجب عليك تسجيل الدخول للوصول إلى هذا المورد!", + "access-forbidden": "الوصول ممنوع", + "access-forbidden-text": "ليس لديك حق الوصول إلى هذا المكان!
جرب تسجيل الدخول بمستخدم مختلف إذا كنت لا تزال ترغب في الوصول إلى هذا المكان.", + "refresh-token-expired": "انتهت صلاحية الجلسة", + "refresh-token-failed": "غير قادر على تحديث الجلسة", + "permission-denied": "رفض الإذن", + "permission-denied-text": "ليس لديك إذن لأداء هذه العملية!" + }, + "account": { + "account": "الحساب", + "notification-settings": "إعدادات الإشعارات" + }, + "action": { + "activate": "تفعيل", + "suspend": "تعليق", + "save": "حفظ", + "saveAs": "حفظ باسم", + "move": "نقل", + "cancel": "إلغاء", + "ok": "موافق", + "delete": "حذف", + "add": "إضافة", + "yes": "نعم", + "no": "لا", + "update": "تحديث", + "remove": "إزالة", + "search": "بحث", + "clear-search": "مسح البحث", + "assign": "تعيين", + "unassign": "إلغاء التعيين", + "share": "مشاركة", + "make-private": "جعل خاص", + "make-public": "جعل عام", + "apply": "تطبيق", + "apply-changes": "تطبيق التغييرات", + "edit-mode": "وضع التحرير", + "enter-edit-mode": "الدخول إلى وضع التحرير", + "decline-changes": "رفض التغييرات", + "open": "فتح", + "decline": "رفض", + "close": "إغلاق", + "back": "رجوع", + "run": "تشغيل", + "sign-in": "تسجيل الدخول", + "edit": "تعديل", + "view": "عرض", + "create": "إنشاء", + "drag": "سحب", + "refresh": "تحديث", + "undo": "تراجع", + "copy": "نسخ", + "paste": "لصق", + "copy-reference": "نسخ المرجع", + "paste-reference": "لصق المرجع", + "import": "استيراد", + "export": "تصدير", + "share-via": "مشاركة عبر {{provider}}", + "select": "اختيار", + "continue": "استمرار", + "discard-changes": "تجاهل التغييرات", + "download": "تنزيل", + "next": "التالي", + "next-with-label": "التالي: {{label}}", + "read-more": "اقرأ المزيد", + "hide": "إخفاء", + "done": "تم", + "print": "طباعة", + "restore": "استعادة", + "confirm": "تأكيد", + "more": "المزيد", + "less": "أقل", + "skip": "تخطي", + "send": "إرسال", + "reset": "إعادة تعيين", + "show-more": "عرض المزيد", + "dont-show-again": "لا تظهر مرة أخرى", + "see-documentation": "مشاهدة الوثائق", + "clear": "مسح", + "upload": "تحميل", + "delete-anyway": "الحذف على أي حال", + "delete-selected": "حذف المحدد" + }, + "aggregation": { + "aggregation": "تجميع", + "function": "وظيفة تجميع البيانات", + "limit": "الحد الأقصى للقيم", + "group-interval": "فترة التجميع", + "min": "الحد الأدنى", + "max": "الحد الأقصى", + "avg": "متوسط", + "sum": "مجموع", + "count": "عدد", + "none": "لا شيء" + }, + "admin": { + "settings": "الإعدادات", + "general": "عام", + "general-settings": "الإعدادات العامة", + "home-settings": "إعدادات الصفحة الرئيسية", + "home": "الصفحة الرئيسية", + "outgoing-mail": "خادم البريد الصادر", + "outgoing-mail-settings": "إعدادات خادم البريد الصادر", + "system-settings": "إعدادات النظام", + "test-mail-sent": "تم إرسال بريد الاختبار بنجاح!", + "base-url": "عنوان URL الأساسي", + "base-url-required": "عنوان URL الأساسي مطلوب.", + "prohibit-different-url": "منع استخدام اسم المضيف من رؤوس طلب العميل", + "prohibit-different-url-hint": "يجب تفعيل هذا الإعداد لبيئات الإنتاج. قد يسبب مشاكل أمنية عند تعطيله", + "device-connectivity": { + "device-connectivity": "اتصال الجهاز", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "إذا كانت حقول العنوان أو المنفذ فارغة، سيتم استخدام قيمة البروتوكول الافتراضية.", + "host": "المضيف", + "port": "المنفذ", + "port-pattern": "يجب أن يكون المنفذ عددًا صحيحًا موجبًا.", + "port-range": "يجب أن يكون المنفذ في نطاق من 1 إلى 65535." + }, + "mail-from": "البريد من", + "mail-from-required": "البريد من مطلوب.", + "smtp-protocol": "بروتوكول SMTP", + "smtp-host": "مضيف SMTP", + "smtp-host-required": "مضيف SMTP مطلوب.", + "smtp-port": "منفذ SMTP", + "smtp-port-required": "يجب توفير منفذ smtp.", + "smtp-port-invalid": "هذا لا يبدو منفذ smtp صالحًا.", + "timeout-msec": "المهلة (ميلي ثانية)", + "timeout-required": "المهلة مطلوبة.", + "timeout-invalid": "هذا لا يبدو مهلة صالحة.", + "enable-tls": "تفعيل TLS", + "tls-version": "إصدار TLS", + "enable-proxy": "تفعيل الوكيل", + "proxy-host": "مضيف الوكيل", + "proxy-host-required": "مضيف الوكيل مطلوب.", + "proxy-port": "منفذ الوكيل", + "proxy-port-required": "منفذ الوكيل مطلوب.", + "proxy-port-range": "يجب أن يكون منفذ الوكيل في نطاق من 1 إلى 65535.", + "proxy-user": "مستخدم الوكيل", + "proxy-password": "كلمة مرور الوكيل", + "change-password": "تغيير كلمة المرور", + "send-test-mail": "إرسال بريد اختبار", + "use-system-mail-settings": "استخدام إعدادات خادم البريد النظام", + "mail-templates": "قوالب البريد", + "mail-template-settings": "إعدادات قوالب البريد", + "use-system-mail-template-settings": "استخدام قوالب البريد النظام", + "mail-template": { + "mail-template": "قالب البريد", + "test": "رسالة بريد اختبارية", + "activation": "رسالة تفعيل الحساب", + "account-activated": "رسالة تم تفعيل الحساب", + "account-lockout": "رسالة قفل الحساب", + "reset-password": "رسالة إعادة تعيين كلمة المرور", + "password-was-reset": "تم إعادة تعيين كلمة المرور", + "user-activated": "رسالة تم تفعيل المستخدم", + "user-registered": "رسالة تسجيل المستخدم", + "api-usage-state-enabled": "تم تفعيل حالة استخدام API", + "api-usage-state-warning": "تحذير حالة استخدام API", + "api-usage-state-disabled": "تم تعطيل حالة استخدام API", + "two-fa-verification": "رسالة التحقق 2FA" + }, + "mail-subject": "موضوع البريد", + "mail-body": "نص البريد", + "sms-provider": "مزود الرسائل القصيرة", + "sms-provider-settings": "إعدادات مزود الرسائل القصيرة", + "use-system-sms-settings": "استخدام إعدادات مزود الرسائل القصيرة النظام", + "sms-provider-type": "نوع مزود الرسائل القصيرة", + "sms-provider-type-required": "نوع مزود الرسائل القصيرة مطلوب.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "معرف مفتاح الوصول AWS", + "aws-access-key-id-required": "معرف مفتاح الوصول AWS مطلوب", + "aws-secret-access-key": "مفتاح الوصول السري AWS", + "aws-secret-access-key-required": "مفتاح الوصول السري AWS مطلوب", + "aws-region": "منطقة AWS", + "aws-region-required": "منطقة AWS مطلوبة", + "number-from": "رقم الهاتف من", + "number-from-required": "رقم الهاتف من مطلوب.", + "number-to": "رقم الهاتف إلى", + "number-to-required": "رقم الهاتف إلى مطلوب.", + "phone-number-hint": "رقم الهاتف بتنسيق E.164، مثل +19995550123", + "phone-number-hint-twilio": "رقم الهاتف بتنسيق E.164/معرّف رقم الهاتف/معرّف خدمة الرسائل، مثل +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "رقم الهاتف غير صالح. يجب أن يكون بتنسيق E.164، مثل +19995550123.", + "phone-number-pattern-twilio": "رقم الهاتف غير صالح. يجب أن يكون بتنسيق E.164/معرّف رقم الهاتف/معرّف خدمة الرسائل، مثل +19995550123/PNXXX/MGXXX.", + "sms-message": "رسالة الرسائل القصيرة", + "sms-message-required": "رسالة الرسائل القصيرة مطلوبة.", + "sms-message-max-length": "لا يمكن أن تكون رسالة الرسائل القصيرة أطول من 1600 حرف", + "twilio-account-sid": "معرّف حساب Twilio", + "twilio-account-sid-required": "معرّف حساب Twilio مطلوب", + "twilio-account-token": "رمز حساب Twilio", + "twilio-account-token-required": "رمز حساب Twilio مطلوب", + "send-test-sms": "إرسال رسالة قصيرة اختبارية", + "test-sms-sent": "تم إرسال الرسالة القصيرة الاختبارية بنجاح!", + "security-settings": "إعدادات الأمان", + "password-policy": "سياسة كلمة المرور", + "minimum-password-length": "الحد الأدنى لطول كلمة المرور", + "minimum-password-length-required": "الحد الأدنى لطول كلمة المرور مطلوب", + "minimum-password-length-range": "يجب أن يكون طول كلمة المرور الأدنى في نطاق من 6 إلى 50", + "maximum-password-length": "الحد الأقصى لطول كلمة المرور", + "maximum-password-length-min": "يجب أن يكون الحد الأقصى لطول كلمة المرور 6 على الأقل", + "maximum-password-length-less-min": "يجب أن يكون الحد الأقصى لطول كلمة المرور أكبر من الحد الأدنى للطول", + "minimum-uppercase-letters": "الحد الأدنى لعدد الأحرف الكبيرة", + "minimum-uppercase-letters-range": "لا يمكن أن يكون الحد الأدنى لعدد الأحرف الكبيرة سالبًا", + "minimum-lowercase-letters": "الحد الأدنى لعدد الأحرف الصغيرة", + "minimum-lowercase-letters-range": "لا يمكن أن يكون الحد الأدنى لعدد الأحرف الصغيرة سالبًا", + "minimum-digits": "الحد الأدنى لعدد الأرقام", + "minimum-digits-range": "لا يمكن أن يكون الحد الأدنى لعدد الأرقام سالبًا", + "minimum-special-characters": "الحد الأدنى لعدد الأحرف الخاصة", + "minimum-special-characters-range": "لا يمكن أن يكون الحد الأدنى لعدد الأحرف الخاصة سالبًا", + "password-expiration-period-days": "فترة انتهاء صلاحية كلمة المرور بالأيام", + "password-expiration-period-days-range": "لا يمكن أن تكون فترة انتهاء صلاحية كلمة المرور بالأيام سالبة", + "password-reuse-frequency-days": "تكرار استخدام كلمة المرور بالأيام", + "password-reuse-frequency-days-range": "لا يمكن أن يكون تكرار استخدام كلمة المرور بالأيام سالبًا", + "allow-whitespace": "السماح بالمسافات", + "force-reset-password-if-no-valid": "إجبار على إعادة تعيين كلمة المرور إذا لم تكن صالحة", + "force-reset-password-if-no-valid-hint": "يرجى الحذر عند تفعيل هذه الميزة: سيتطلب من المستخدمين الذين لا تكون كلمات مرورهم صالحة إعادة تعيين كلمات المرور الخاصة بهم عبر البريد الإلكتروني.", + "general-policy": "السياسة العامة", + "max-failed-login-attempts": "الحد الأقصى لعدد محاولات تسجيل الدخول الفاشلة، قبل قفل الحساب", + "minimum-max-failed-login-attempts-range": "لا يمكن أن يكون الحد الأقصى لعدد محاولات تسجيل الدخول الفاشلة سالبًا", + "user-lockout-notification-email": "في حالة قفل حساب المستخدم، إرسال إشعار إلى البريد الإلكتروني", + "domain-name": "اسم النطاق", + "domain-name-unique": "يجب أن يكون اسم النطاق والبروتوكول فريدين.", + "domain-name-max-length": "يجب أن يكون اسم النطاق أقل من 256", + "error-verification-url": "لا ينبغي أن يحتوي اسم النطاق على الرموز '/' و ':'. مثال: thingsboard.io", + "connection-settings": "إعدادات الاتصال", + "oauth2": { + "access-token-uri": "عنوان URI لرمز الوصول", + "access-token-uri-required": "عنوان URI لرمز الوصول مطلوب.", + "activate-user": "تفعيل المستخدم", + "add-domain": "إضافة نطاق", + "delete-domain": "حذف نطاق", + "add-provider": "إضافة مزود", + "delete-provider": "حذف مزود", + "allow-user-creation": "السماح بإنشاء المستخدم", + "always-fullscreen": "دائمًا بملء الشاشة", + "authorization-uri": "عنوان URI للتفويض", + "authorization-uri-required": "عنوان URI للتفويض مطلوب.", + "client-authentication-method": "طريقة توثيق العميل", + "client-id": "معرف العميل", + "client-id-required": "معرف العميل مطلوب.", + "client-id-max-length": "يجب أن يكون معرف العميل أقل من 256", + "client-secret": "سر العميل", + "client-secret-required": "سر العميل مطلوب.", + "client-secret-max-length": "يجب أن يكون سر العميل أقل من 2049", + "custom-setting": "إعدادات مخصصة", + "customer-name-pattern": "نمط اسم العميل", + "customer-name-pattern-max-length": "يجب أن يكون نمط اسم العميل أقل من 256", + "parent-customer-name-pattern": "نمط اسم العميل الأصل", + "user-groups-name-pattern": "نمط اسم مجموعات المستخدمين", + "default-dashboard-name": "اسم لوحة التحكم الافتراضي", + "default-dashboard-name-max-length": "يجب أن يكون اسم لوحة التحكم الافتراضي أقل من 256", + "delete-domain-text": "كن حذرًا، بعد التأكيد سيصبح النطاق وجميع بيانات المزود غير متوفرة.", + "delete-domain-title": "هل أنت متأكد أنك تريد حذف إعدادات النطاق '{{domainName}}'؟", + "delete-registration-text": "كن حذرًا، بعد التأكيد ستصبح بيانات المزود غير متوفرة.", + "delete-registration-title": "هل أنت متأكد أنك تريد حذف المزود '{{name}}'؟", + "email-attribute-key": "مفتاح سمة البريد الإلكتروني", + "email-attribute-key-required": "مفتاح سمة البريد الإلكتروني مطلوب.", + "email-attribute-key-max-length": "يجب أن يكون مفتاح سمة البريد الإلكتروني أقل من 32", + "first-name-attribute-key": "مفتاح سمة الاسم الأول", + "first-name-attribute-key-max-length": "يجب أن يكون مفتاح سمة الاسم الأول أقل من 32", + "general": "عام", + "jwk-set-uri": "عنوان URI لمجموعة مفاتيح الويب JSON", + "last-name-attribute-key": "مفتاح سمة الاسم الأخير", + "last-name-attribute-key-max-length": "يجب أن يكون مفتاح سمة الاسم الأخير أقل من 32", + "login-button-icon": "أيقونة زر تسجيل الدخول", + "login-button-label": "علامة مزود تسجيل الدخول", + "login-button-label-placeholder": "تسجيل الدخول مع $(علامة مزود تسجيل الدخول)", + "login-button-label-required": "العلامة مطلوبة.", + "login-provider": "مزود تسجيل الدخول", + "mapper": "المعين", + "new-domain": "نطاق جديد", + "oauth2": "OAuth2", + "password-max-length": "يجب أن تكون كلمة المرور أقل من 256", + "redirect-uri-template": "قالب عنوان URI لإعادة التوجيه", + "copy-redirect-uri": "نسخ عنوان URI لإعادة التوجيه", + "registration-id": "معرّف التسجيل", + "registration-id-required": "معرّف التسجيل مطلوب.", + "registration-id-unique": "يجب أن يكون معرّف التسجيل فريدًا للنظام.", + "scope": "نطاق", + "scope-required": "النطاق مطلوب.", + "tenant-name-pattern": "نمط اسم المستأجر", + "tenant-name-pattern-required": "نمط اسم المستأجر مطلوب.", + "tenant-name-pattern-max-length": "يجب أن يكون نمط اسم المستأجر أقل من 256", + "tenant-name-strategy": "استراتيجية اسم المستأجر", + "type": "نوع المعين", + "uri-pattern-error": "تنسيق URI غير صالح.", + "url": "URL", + "url-pattern": "تنسيق URL غير صالح.", + "url-required": "URL مطلوب.", + "url-max-length": "يجب أن يكون URL أقل من 256", + "user-info-uri": "عنوان URI لمعلومات المستخدم", + "user-info-uri-required": "عنوان URI لمعلومات المستخدم مطلوب.", + "username-max-length": "يجب أن يكون اسم المستخدم أقل من 256", + "user-name-attribute-name": "مفتاح سمة اسم المستخدم", + "user-name-attribute-name-required": "مفتاح سمة اسم المستخدم مطلوب", + "protocol": "البروتوكول", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "تفعيل إعدادات OAuth2", + "domains": "النطاقات", + "mobile-apps": "تطبيقات الجوال", + "no-mobile-apps": "لا توجد تطبيقات مُعدة", + "mobile-package": "حزمة التطبيق", + "mobile-package-placeholder": "مثال: my.example.app", + "mobile-package-hint": "لنظام Android: معرّف التطبيق الفريد الخاص بك. لنظام iOS: معرّف حزمة المنتج.", + "mobile-package-unique": "يجب أن تكون حزمة التطبيق فريدة.", + "mobile-app-secret": "سر التطبيق", + "invalid-mobile-app-secret": "يجب أن يحتوي سر التطبيق على أحرف أبجدية رقمية فقط ويجب أن يكون بين 16 و2048 حرفًا.", + "copy-mobile-app-secret": "نسخ سر التطبيق", + "add-mobile-app": "إضافة تطبيق", + "delete-mobile-app": "حذف معلومات التطبيق", + "providers": "المزودون", + "platform-web": "ويب", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "جميع المنصات", + "smtp-provider": "مزود SMTP", + "allowed-platforms": "المنصات المسموح بها", + "authentication": "المصادقة", + "basic": "أساسي", + "provider": "المزود", + "redirect-url": "عنوان URL لإعادة التوجيه", + "domain-name": "اسم النطاق", + "redirect-url-template": "قالب عنوان URL لإعادة التوجيه", + "microsoft-tenant-id": "معرّف الدليل (المستأجر)", + "microsoft-tenant-id-required": "معرّف الدليل (المستأجر) مطلوب", + "token-uri": "عنوان URI للرمز", + "token-uri-required": "عنوان URI للرمز مطلوب", + "redirect-uri": "عنوان URI لإعادة التوجيه", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "مخصص", + "generate-access-token": "توليد رمز الوصول", + "update-access-token": "تحديث رمز الوصول", + "access-token-status": "حالة رمز الوصول:", + "token-status-generated": "تم التوليد", + "token-status-not-generated": "لم يتم التوليد" + }, + "smpp-provider": { + "smpp-version": "إصدار SMPP", + "smpp-host": "مضيف SMPP", + "smpp-host-required": "مضيف SMPP مطلوب", + "smpp-port": "منفذ SMPP", + "smpp-port-required": "منفذ SMPP مطلوب", + "system-id": "معرّف النظام", + "system-id-required": "معرّف النظام مطلوب", + "password": "كلمة المرور", + "password-required": "كلمة المرور مطلوبة", + "type-settings": "إعدادات النوع", + "source-settings": "إعدادات المصدر", + "destination-settings": "إعدادات الوجهة", + "additional-settings": "إعدادات إضافية", + "system-type": "نوع النظام", + "bind-type": "نوع الربط", + "service-type": "نوع الخدمة", + "source-address": "عنوان المصدر", + "source-ton": "TON المصدر", + "source-npi": "NPI المصدر", + "destination-ton": "TON الوجهة (نوع الرقم)", + "destination-npi": "NPI الوجهة (تعريف خطة الأرقام)", + "address-range": "نطاق العنوان", + "coding-scheme": "مخطط الترميز", + "bind-type-tx": "مرسل", + "bind-type-rx": "مستقبل", + "bind-type-trx": "جهاز إرسال واستقبال", + "ton-unknown": "غير معروف", + "ton-international": "دولي", + "ton-national": "وطني", + "ton-network-specific": "خاص بالشبكة", + "ton-subscriber-number": "رقم المشترك", + "ton-alphanumeric": "أبجدية رقمية", + "ton-abbreviated": "مختصر", + "npi-unknown": "0 - غير معروف", + "npi-isdn": "1 - ISDN/خطة ترقيم الهاتف (E163/E164)", + "npi-data-numbering-plan": "3 - خطة ترقيم البيانات (X.121)", + "npi-telex-numbering-plan": "4 - خطة ترقيم البرق (F.69)", + "npi-land-mobile": "6 - الأرض المتنقلة (E.212)", + "npi-national-numbering-plan": "8 - خطة الترقيم الوطنية", + "npi-private-numbering-plan": "9 - خطة الترقيم الخاصة", + "npi-ermes-numbering-plan": "10 - خطة ترقيم ERMES (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - الإنترنت (IP)", + "npi-wap-client-id": "18 - معرّف عميل WAP (لتحديد بواسطة منتدى WAP)", + "scheme-smsc": "0 - أبجدية SMSC الافتراضية (ASCII للرمز القصير والطويل وإلى GSM للمجاني)", + "scheme-ia5": "1 - IA5 (ASCII للرمز القصير والطويل، اللاتينية 9 للمجاني (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - أوكتيت غير محدد (ثنائي 8 بت)", + "scheme-latin-1": "3 - اللاتينية 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - أوكتيت غير محدد (ثنائي 8 بت)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - السيريلية (ISO-8859-5)", + "scheme-latin-hebrew": "7 - اللاتينية/العبرية (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - ترميز الرموز التعبيرية", + "scheme-music-codes": "10 - رموز الموسيقى (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - الكانجي الموسع JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - مجموعة أحرف الرسومات الكورية (KS C 5601/KS X 1001)" + }, + "queue-select-name": "اختيار اسم الطابور", + "queue-name": "الاسم", + "queue-name-required": "اسم الطابور مطلوب!", + "queues": "الطوابير", + "queue-partitions": "الأقسام", + "queue-submit-strategy": "استراتيجية الإرسال", + "queue-processing-strategy": "استراتيجية المعالجة", + "queue-configuration": "تكوين الطابور", + "repository-settings": "إعدادات المستودع", + "repository": "المستودع", + "repository-url": "عنوان URL للمستودع", + "repository-url-required": "عنوان URL للمستودع مطلوب.", + "default-branch": "اسم الفرع الافتراضي", + "repository-read-only": "للقراءة فقط", + "show-merge-commits": "إظهار التزامات الدمج", + "authentication-settings": "إعدادات المصادقة", + "auth-method": "طريقة المصادقة", + "auth-method-username-password": "كلمة المرور / رمز الوصول", + "auth-method-username-password-hint": "يجب على مستخدمي GitHub استخدام الرموز الرموز مع أذونات الكتابة إلى المستودع.", + "auth-method-private-key": "المفتاح الخاص", + "password-access-token": "كلمة المرور / رمز الوصول", + "change-password-access-token": "تغيير كلمة المرور / رمز الوصول", + "private-key": "المفتاح الخاص", + "drop-private-key-file-or": "اسحب وأفلت ملف المفتاح الخاص أو", + "passphrase": "العبارة السرية", + "enter-passphrase": "أدخل العبارة السرية", + "change-passphrase": "تغيير العبارة السرية", + "check-access": "فحص الوصول", + "check-repository-access-success": "تم التحقق من وصول المستودع بنجاح!", + "delete-repository-settings-title": "هل أنت متأكد أنك تريد حذف إعدادات المستودع؟", + "delete-repository-settings-text": "كن حذرًا، بعد التأكيد سيتم إزالة إعدادات المستودع ولن تكون ميزة التحكم في الإصدار متوفرة.", + "auto-commit-settings": "إعدادات الالتزام التلقائي", + "auto-commit": "الالتزام التلقائي", + "auto-commit-entities": "الكيانات الملتزمة تلقائيًا", + "no-auto-commit-entities-prompt": "لا توجد كيانات مُعدة للالتزام التلقائي", + "delete-auto-commit-settings-title": "هل أنت متأكد أنك تريد حذف إعدادات الالتزام التلقائي؟", + "delete-auto-commit-settings-text": "كن حذرًا، بعد التأكيد سيتم إزالة إعدادات الالتزام التلقائي وسيتم تعطيل الالتزام التلقائي لجميع الكيانات.", + "2fa": { + "2fa": "المصادقة الثنائية", + "available-providers": "المزودون المتاحون", + "issuer-name": "اسم الجهة المصدرة", + "issuer-name-required": "اسم الجهة المصدرة مطلوب.", + "max-verification-failures-before-user-lockout": "الحد الأقصى لفشل التحقق قبل قفل المستخدم", + "max-verification-failures-before-user-lockout-pattern": "يجب أن يكون الحد الأقصى لفشل التحقق عددًا صحيحًا موجبًا.", + "number-of-checking-attempts": "عدد محاولات التحقق", + "number-of-checking-attempts-pattern": "يجب أن يكون عدد محاولات التحقق عددًا صحيحًا موجبًا.", + "number-of-checking-attempts-required": "عدد محاولات التحقق مطلوب.", + "number-of-codes": "عدد الرموز", + "number-of-codes-pattern": "يجب أن يكون عدد الرموز عددًا صحيحًا موجبًا.", + "number-of-codes-required": "عدد الرموز مطلوب.", + "provider": "المزود", + "retry-verification-code-period": "فترة إعادة المحاولة لرمز التحقق (ثانية)", + "retry-verification-code-period-pattern": "الحد الأدنى للوقت هو 5 ثوانٍ", + "retry-verification-code-period-required": "فترة إعادة المحاولة لرمز التحقق مطلوبة.", + "total-allowed-time-for-verification": "الوقت الإجمالي المسموح به للتحقق (ثانية)", + "total-allowed-time-for-verification-pattern": "الحد الأدنى للوقت الإجمالي المسموح به هو 60 ثانية", + "total-allowed-time-for-verification-required": "الوقت الإجمالي المسموح به مطلوب.", + "use-system-two-factor-auth-settings": "استخدام إعدادات المصادقة الثنائية للنظام", + "verification-code-check-rate-limit": "حد معدل التحقق من رمز التحقق", + "verification-code-lifetime": "مدة صلاحية رمز التحقق (ثانية)", + "verification-code-lifetime-pattern": "يجب أن تكون مدة صلاحية رمز التحقق عددًا صحيحًا موجبًا.", + "verification-code-lifetime-required": "مدة صلاحية رمز التحقق مطلوبة.", + "verification-message-template": "قالب رسالة التحقق", + "verification-limitations": "قيود التحقق", + "verification-message-template-pattern": "يجب أن تحتوي رسالة التحقق على النمط: ${code}", + "verification-message-template-required": "قالب رسالة التحقق مطلوب.", + "within-time": "خلال الوقت (ثانية)", + "within-time-pattern": "يجب أن يكون الوقت عددًا صحيحًا موجبًا.", + "within-time-required": "الوقت مطلوب." + }, + "jwt": { + "security-settings": "إعدادات أمان JWT", + "issuer-name": "اسم الجهة المصدرة", + "issuer-name-required": "اسم الجهة المصدرة مطلوب.", + "signings-key": "مفتاح التوقيع", + "signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 256 بت من البيانات.", + "signings-key-required": "مفتاح التوقيع مطلوب.", + "signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 256 بت من البيانات.", + "signings-key-base64": "يجب أن يكون مفتاح التوقيع بتنسيق base64.", + "expiration-time": "وقت انتهاء صلاحية الرمز (ثانية)", + "expiration-time-required": "وقت انتهاء صلاحية الرمز مطلوب.", + "expiration-time-pattern": "يجب أن يكون وقت انتهاء صلاحية الرمز عددًا صحيحًا موجبًا.", + "expiration-time-min": "الحد الأدنى للوقت هو 60 ثانية (1 دقيقة).", + "refresh-expiration-time": "وقت انتهاء صلاحية رمز التحديث (ثانية)", + "refresh-expiration-time-required": "وقت انتهاء صلاحية رمز التحديث مطلوب.", + "refresh-expiration-time-pattern": "يجب أن يكون وقت انتهاء صلاحية رمز التحديث عددًا صحيحًا موجبًا.", + "refresh-expiration-time-min": "الحد الأدنى للوقت هو 900 ثانية (15 دقيقة).", + "refresh-expiration-time-less-token": "يجب أن يكون وقت رمز التحديث أكبر من وقت الرمز.", + "generate-key": "توليد المفتاح", + "info-header": "سيتعين على جميع المستخدمين إعادة تسجيل الدخول", + "info-message": "تغيير مفتاح التوقيع JWT سيؤدي إلى إبطال جميع الرموز الصادرة. سيحتاج جميع المستخدمين إلى إعادة تسجيل الدخول. سيؤثر هذا أيضًا على البرامج النصية التي تستخدم واجهة برمجة التطبيقات REST/Websockets." + }, + "resources": "الموارد", + "notifications": "الإشعارات", + "notifications-settings": "إعدادات الإشعارات", + "slack-api-token": "رمز API لـ Slack", + "slack": "Slack", + "slack-settings": "إعدادات Slack" + }, + "alarm": { + "alarm": "إنذار", + "alarms": "الإنذارات", + "all-alarms": "جميع الإنذارات", + "select-alarm": "اختر إنذار", + "no-alarms-matching": "لم يتم العثور على إنذارات مطابقة لـ '{{entity}}'.", + "alarm-required": "الإنذار مطلوب", + "alarm-filter": "فلتر الإنذار", + "filter": "فلتر", + "alarm-status": "حالة الإنذار", + "alarm-status-list": "قائمة حالات الإنذار", + "any-status": "أي حالة", + "search-status": { + "ANY": "أي", + "ACTIVE": "نشط", + "CLEARED": "مُزال", + "ACK": "مُقر", + "UNACK": "غير مُقر" + }, + "display-status": { + "ACTIVE_UNACK": "نشط غير مُقر", + "ACTIVE_ACK": "نشط مُقر", + "CLEARED_UNACK": "مُزال غير مُقر", + "CLEARED_ACK": "مُزال مُقر" + }, + "no-alarms-prompt": "لم يتم العثور على إنذارات", + "created-time": "وقت الإنشاء", + "type": "النوع", + "severity": "الخطورة", + "originator": "المُبتدئ", + "originator-type": "نوع المُبتدئ", + "details": "التفاصيل", + "originator-label": "تسمية المُبتدئ", + "assign": "تعيين", + "assignments": "التعيينات", + "assignee": "المُعين", + "assignee-id": "معرّف المُعين", + "assignee-first-name": "اسم المُعين الأول", + "assignee-last-name": "اسم عائلة المُعين", + "assignee-email": "بريد المُعين الإلكتروني", + "unassigned": "غير مُعين", + "assignee-not-set": "الكل", + "status": "الحالة", + "alarm-details": "تفاصيل الإنذار", + "start-time": "وقت البدء", + "assign-time": "وقت التعيين", + "end-time": "وقت الانتهاء", + "ack-time": "وقت الاعتراف", + "clear-time": "وقت الإزالة", + "duration": "المدة", + "alarm-severity-list": "قائمة خطورة الإنذار", + "any-severity": "أي خطورة", + "severity-critical": "حرج", + "severity-major": "رئيسي", + "severity-minor": "ثانوي", + "severity-warning": "تحذير", + "severity-indeterminate": "غير محدد", + "acknowledge": "اعتراف", + "clear": "إزالة", + "delete": "حذف", + "search": "بحث عن إنذارات", + "selected-alarms": "{ count, plural, =1 {1 إنذار} other {# إنذارات} } محددة", + "no-data": "لا توجد بيانات لعرضها", + "polling-interval": "فترة استطلاع الإنذارات (ثانية)", + "polling-interval-required": "فترة استطلاع الإنذارات مطلوبة.", + "min-polling-interval-message": "الحد الأدنى المسموح به هو 1 ثانية لفترة الاستطلاع.", + "aknowledge-alarms-title": "الاعتراف بـ { count, plural, =1 {1 إنذار} other {# إنذارات} }", + "aknowledge-alarms-text": "هل أنت متأكد أنك تريد الاعتراف بـ { count, plural, =1 {1 إنذار} other {# إنذارات} }؟", + "aknowledge-alarm-title": "اعتراف بالإنذار", + "aknowledge-alarm-text": "هل أنت متأكد أنك تريد الاعتراف بالإنذار؟", + "selected-alarms-are-acknowledged": "تم الاعتراف بالإنذارات المحددة بالفعل", + "clear-alarms-title": "إزالة { count, plural, =1 {1 إنذار} other {# إنذارات} }", + "clear-alarms-text": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {1 إنذار} other {# إنذارات} }؟", + "clear-alarm-title": "إزالة الإنذار", + "clear-alarm-text": "هل أنت متأكد أنك تريد إزالة الإنذار؟", + "delete-alarms-title": "حذف { count, plural, =1 {1 إنذار} other {# إنذارات} }", + "delete-alarms-text": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 إنذار} other {# إنذارات} }؟", + "selected-alarms-are-cleared": "تم إزالة الإنذارات المحددة بالفعل", + "alarm-status-filter": "فلتر حالة الإنذار", + "alarm-filter-title": "فلتر الإنذار", + "assigned": "مُعين", + "filter-title": "فلتر", + "max-count-load": "الحد الأقصى لعدد الإنذارات للتحميل (0 - غير محدود)", + "max-count-load-required": "الحد الأقصى لعدد الإنذارات للتحميل مطلوب.", + "max-count-load-error-min": "القيمة الدنيا هي 0.", + "fetch-size": "حجم الجلب", + "fetch-size-required": "حجم الجلب مطلوب.", + "fetch-size-error-min": "القيمة الدنيا هي 10.", + "alarm-type-list": "قائمة نوع الإنذار", + "any-type": "أي نوع", + "assigned-to-current-user": "مُعين للمستخدم الحالي", + "assigned-to-me": "مُعين لي", + "search-propagated-alarms": "البحث عن الإنذارات المنتشرة", + "comments": "تعليقات الإنذار", + "show-more": "إظهار المزيد", + "additional-info": "معلومات إضافية", + "alarm-type": "نوع الإنذار", + "enter-alarm-type": "أدخل نوع الإنذار", + "no-alarm-types-matching": "لم يتم العثور على أنواع إنذار مطابقة لـ '{{entitySubtype}}'.", + "alarm-type-list-empty": "لم يتم تحديد أنواع الإنذار." + }, + "alarm-activity": { + "add": "إضافة تعليق...", + "alarm-comment": "تعليق الإنذار", + "comments": "التعليقات", + "delete-alarm-comment": "هل تريد حذف هذا التعليق؟", + "refresh": "تحديث", + "oldest-first": "الأقدم أولاً", + "newest-first": "الأحدث أولاً", + "activity": "النشاط", + "export": "تصدير إلى CSV", + "author": "المؤلف", + "created-date": "تاريخ الإنشاء", + "edited-date": "تاريخ التحرير", + "text": "النص", + "system": "النظام" + }, + "alias": { + "add": "إضافة الاسم المستعار", + "edit": "تحرير الاسم المستعار", + "name": "اسم الاسم المستعار", + "name-required": "اسم الاسم المستعار مطلوب", + "duplicate-alias": "الاسم المستعار بنفس الاسم موجود بالفعل.", + "filter-type-single-entity": "كيان واحد", + "filter-type-entity-group": "مجموعة كيانات", + "filter-type-entity-list": "قائمة الكيانات", + "filter-type-entity-name": "اسم الكيان", + "filter-type-entity-type": "نوع الكيان", + "filter-type-entity-group-list": "قائمة مجموعة الكيانات", + "filter-type-entity-group-name": "اسم مجموعة الكيانات", + "filter-type-entities-by-group-name": "الكيانات حسب اسم المجموعة", + "filter-type-state-entity": "كيان من حالة لوحة التحكم", + "filter-type-state-entity-description": "الكيان المأخوذ من معلمات حالة لوحة التحكم", + "filter-type-state-entity-owner": "مالك الكيان من حالة لوحة التحكم", + "filter-type-state-entity-owner-description": "مالك الكيان المأخوذ من معلمات حالة لوحة التحكم", + "filter-type-asset-type": "نوع الأصل", + "filter-type-asset-type-description": "الأصول من النوع '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "الأصول من النوع '{{assetTypes}}' وباسم يبدأ بـ '{{prefix}}'", + "filter-type-device-type": "نوع الجهاز", + "filter-type-device-type-description": "الأجهزة من النوع '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "الأجهزة من الأنواع '{{deviceTypes}}' وباسم يبدأ بـ '{{prefix}}'", + "filter-type-entity-view-type": "نوع عرض الكيان", + "filter-type-entity-view-type-description": "عروض الكيان من النوع '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "عروض الكيان من النوع '{{entityViewTypes}}' وباسم يبدأ بـ '{{prefix}}'", + "filter-type-edge-type": "نوع الحافة", + "filter-type-edge-type-description": "الحواف من النوع '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "الحواف من النوع '{{edgeTypes}}' وباسم يبدأ بـ '{{prefix}}'", + "filter-type-relations-query": "استعلام العلاقات", + "filter-type-relations-query-description": "{{entities}} التي لها علاقة {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "استعلام بحث الحافة", + "filter-type-edge-search-query-description": "الحواف بأنواع {{edgeTypes}} التي لها علاقة {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-scheduler-event": "أحداث المُجدول", + "filter-type-scheduler-event-type-description": "أحداث المُجدول بالنوع '{{eventType}}'", + "filter-type-scheduler-event-originator-description": "أحداث المُجدول للمُبتدئ محدد", + "filter-type-scheduler-event-type-originator-description": "أحداث المُجدول للمُبتدئ محدد بـ '{{eventType}}'", + "filter-type-asset-search-query": "استعلام بحث الأصل", + "filter-type-asset-search-query-description": "الأصول بأنواع {{assetTypes}} التي لها علاقة {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "استعلام بحث الجهاز", + "filter-type-device-search-query-description": "الأجهزة بأنواع {{deviceTypes}} التي لها علاقة {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "استعلام بحث عرض الكيان", + "filter-type-entity-view-search-query-description": "عروض الكيان بأنواع {{entityViewTypes}} التي لها علاقة {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "حالة استخدام API", + "entity-filter": "فلتر الكيان", + "resolve-multiple": "حل ككيانات متعددة", + "filter-type": "نوع الفلتر", + "filter-type-required": "نوع الفلتر مطلوب.", + "entity-filter-no-entity-matched": "لم يتم العثور على كيانات تطابق الفلتر المحدد.", + "no-entity-filter-specified": "لم يتم تحديد فلتر الكيان", + "root-state-entity": "استخدام كيان حالة لوحة التحكم كجذر", + "group-state-entity": "استخدام كيان حالة لوحة التحكم كمجموعة كيانات", + "group-state-entity-owner": "استخدام كيان حالة لوحة التحكم كمالك مجموعة كيانات", + "last-level-relation": "جلب العلاقة المستوى الأخير فقط", + "root-entity": "الكيان الجذر", + "state-entity-parameter-name": "اسم معلمة كيان الحالة", + "default-state-entity": "كيان الحالة الافتراضي", + "default-state-entity-group": "مجموعة كيان الحالة الافتراضية", + "default-entity-parameter-name": "افتراضيًا", + "max-relation-level": "الحد الأقصى لمستوى العلاقة", + "unlimited-level": "مستوى غير محدود", + "state-entity": "كيان حالة لوحة التحكم", + "entities-of-group-state-entity": "الكيانات من مجموعة كيان حالة لوحة التحكم", + "all-entities": "جميع الكيانات", + "any-relation": "أي", + "originator": "المُبتدئ", + "originator-state-entity": "استخدام كيان حالة لوحة التحكم كمُبتدئ", + "scheduler-event-type": "نوع حدث المُجدول" + }, + "asset": { + "all": "الكل", + "all-assets": "جميع الأصول", + "groups": "المجموعات", + "shared": "مشترك", + "asset": "الأصل", + "assets": "الأصول", + "management": "إدارة الأصول", + "view-assets": "عرض الأصول", + "add": "إضافة أصل", + "asset-type-max-length": "يجب أن يكون نوع الأصل أقل من 256", + "assign-to-customer": "تعيين للعميل", + "assign-asset-to-customer": "تعيين الأصل/الأصول للعميل", + "assign-asset-to-customer-text": "الرجاء تحديد الأصول لتعيينها للعميل", + "no-assets-text": "لم يتم العثور على أصول", + "assign-to-customer-text": "الرجاء تحديد العميل لتعيين الأصل/الأصول له", + "public": "عام", + "assignedToCustomer": "مُعين للعميل", + "make-public": "جعل الأصل عام", + "make-private": "جعل الأصل خاص", + "unassign-from-customer": "إلغاء التعيين من العميل", + "delete": "حذف الأصل", + "asset-public": "الأصل عام", + "asset-type": "نوع الأصل", + "asset-type-required": "نوع الأصل مطلوب.", + "select-asset-type": "اختر نوع الأصل", + "enter-asset-type": "أدخل ملف الأصل", + "any-asset": "أي أصل", + "no-asset-types-matching": "لم يتم العثور على أنواع أصول مطابقة لـ '{{entitySubtype}}'.", + "asset-type-list-empty": "لم يتم تحديد أنواع الأصول.", + "asset-types": "أنواع الأصول", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "label-max-length": "يجب أن تكون التسمية أقل من 256", + "description": "الوصف", + "type": "النوع", + "type-required": "النوع مطلوب.", + "details": "التفاصيل", + "events": "الأحداث", + "add-asset-text": "إضافة أصل جديد", + "asset-details": "تفاصيل الأصل", + "assign-assets": "تعيين الأصول", + "assign-assets-text": "تعيين { count, plural, =1 {1 أصل} other {# أصول} } للعميل", + "assign-asset-to-edge-title": "تعيين الأصل/الأصول للحافة", + "assign-asset-to-edge-text": "الرجاء تحديد الأصول لتعيينها للحافة", + "delete-assets": "حذف الأصول", + "unassign-assets": "إلغاء تعيين الأصول", + "unassign-assets-action-title": "إلغاء تعيين { count, plural, =1 {1 أصل} other {# أصول} } من العميل", + "assign-new-asset": "تعيين أصل جديد", + "delete-asset-title": "هل أنت متأكد أنك تريد حذف الأصل '{{assetName}}'؟", + "delete-asset-text": "كن حذرًا، بعد التأكيد سيتم حذف الأصل وجميع البيانات ذات الصلة ولن تكون قابلة للاسترداد.", + "delete-assets-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 أصل} other {# أصول} }؟", + "delete-assets-action-title": "حذف { count, plural, =1 {1 أصل} other {# أصول} }", + "delete-assets-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع الأصول المحددة وجميع البيانات ذات الصلة ولن تكون قابلة للاسترداد.", + "make-public-asset-title": "هل أنت متأكد أنك تريد جعل الأصل '{{assetName}}' عامًا؟", + "make-public-asset-text": "بعد التأكيد، سيصبح الأصل وجميع بياناته عامة ويمكن الوصول إليها من قبل الآخرين.", + "make-private-asset-title": "هل أنت متأكد أنك تريد جعل الأصل '{{assetName}}' خاصًا؟", + "make-private-asset-text": "بعد التأكيد، سيصبح الأصل وجميع بياناته خاصة ولن يمكن الوصول إليها من قبل الآخرين.", + "unassign-asset-title": "هل أنت متأكد أنك تريد إلغاء تعيين الأصل '{{assetName}}'؟", + "unassign-asset-text": "بعد التأكيد، سيتم إلغاء تعيين الأصل ولن يكون متاحًا للعميل.", + "unassign-asset": "إلغاء تعيين الأصل", + "unassign-assets-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {1 أصل} other {# أصول} }؟", + "unassign-assets-text": "بعد التأكيد، سيتم إلغاء تعيين جميع الأصول المحددة ولن تكون متاحة للعميل.", + "copyId": "نسخ معرّف الأصل", + "idCopiedMessage": "تم نسخ معرّف الأصل إلى الحافظة", + "select-asset": "اختر أصل", + "no-assets-matching": "لم يتم العثور على أصول مطابقة لـ '{{entity}}'.", + "asset-required": "الأصل مطلوب", + "name-starts-with": "تعبير اسم الأصل", + "help-text": "استخدم '%' حسب الحاجة: '%asset_name_contains%', '%asset_name_ends', 'asset_starts_with'.", + "search": "بحث عن أصول", + "select-group-to-add": "اختر المجموعة الهدف لإضافة الأصول المحددة", + "select-group-to-move": "اختر المجموعة الهدف لنقل الأصول المحددة", + "remove-assets-from-group": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {1 أصل} other {# أصول} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة الأصول", + "list-of-groups": "{ count, plural, =1 {مجموعة أصل واحدة} other {قائمة من # مجموعات الأصول} }", + "group-name-starts-with": "مجموعات الأصول التي تبدأ أسماؤها بـ '{{prefix}}'", + "import": "استيراد الأصول", + "asset-file": "ملف الأصل", + "label": "التسمية", + "assign-asset-to-edge": "تعيين الأصل/الأصول للحافة", + "unassign-asset-from-edge": "إلغاء تعيين الأصل", + "unassign-asset-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين الأصل '{{assetName}}'؟", + "unassign-asset-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين الأصل ولن يكون متاحًا للحافة.", + "unassign-assets-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {1 أصل} other {# أصول} }؟", + "unassign-assets-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين جميع الأصول المحددة ولن تكون متاحة للحافة.", + "selected-assets": "{ count, plural, =1 {1 أصل} other {# أصول} } محددة" + }, + "attribute": { + "attributes": "السمات", + "latest-telemetry": "أحدث قياسات", + "no-latest-telemetry": "لا توجد أحدث قياسات", + "attributes-scope": "نطاق سمات الكيان", + "scope-telemetry": "قياسات", + "scope-latest-telemetry": "أحدث قياسات", + "scope-client": "سمات العميل", + "scope-server": "سمات الخادم", + "scope-shared": "السمات المشتركة", + "add": "إضافة سمة", + "add-attribute-prompt": "الرجاء إضافة سمة", + "key": "المفتاح", + "key-max-length": "يجب أن يكون المفتاح أقل من 256", + "last-update-time": "آخر وقت تحديث", + "key-required": "مفتاح السمة مطلوب.", + "value": "القيمة", + "value-required": "قيمة السمة مطلوبة.", + "telemetry-key-required": "مفتاح القياسات مطلوب", + "telemetry-value-required": "قيمة القياسات مطلوبة", + "delete-attributes-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 سمة} other {# سمات} }؟", + "delete-attributes-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع السمات المحددة.", + "delete-attributes": "حذف السمات", + "enter-attribute-value": "أدخل قيمة السمة", + "show-on-widget": "إظهار على القطعة", + "widget-mode": "وضع القطعة", + "next-widget": "القطعة التالية", + "prev-widget": "القطعة السابقة", + "add-to-dashboard": "إضافة إلى لوحة التحكم", + "add-widget-to-dashboard": "إضافة قطعة إلى لوحة التحكم", + "selected-attributes": "{ count, plural, =1 {1 سمة} other {# سمات} } محددة", + "selected-telemetry": "{ count, plural, =1 {1 وحدة قياسات} other {# وحدات قياسات} } محددة", + "no-attributes-text": "لم يتم العثور على سمات", + "no-telemetry-text": "لم يتم العثور على قياسات", + "copy-key": "نسخ المفتاح", + "add-telemetry": "إضافة قياسات", + "copy-value": "نسخ القيمة", + "delete-timeseries": { + "start-time": "وقت البداية", + "ends-on": "ينتهي في", + "strategy": "الاستراتيجية", + "delete-strategy": "استراتيجية الحذف", + "all-data": "حذف جميع البيانات", + "all-data-except-latest-value": "حذف جميع البيانات باستثناء آخر قيمة", + "latest-value": "حذف آخر قيمة", + "all-data-for-time-period": "حذف جميع البيانات لفترة زمنية", + "rewrite-latest-value": "إعادة كتابة آخر قيمة" + } + }, + "api-usage": { + "api-features": "ميزات API", + "api-usage": "استخدام API", + "alarm": "إنذار", + "alarms-created": "الإنذارات المُنشأة", + "alarms-created-daily-activity": "نشاط يومي لإنشاء الإنذارات", + "alarms-created-hourly-activity": "نشاط ساعي لإنشاء الإنذارات", + "alarms-created-monthly-activity": "نشاط شهري لإنشاء الإنذارات", + "data-points": "نقاط البيانات", + "data-points-storage-days": "أيام تخزين نقاط البيانات", + "device-api": "API الجهاز", + "email": "البريد الإلكتروني", + "email-messages": "رسائل البريد الإلكتروني", + "email-messages-daily-activity": "نشاط يومي لرسائل البريد الإلكتروني", + "email-messages-monthly-activity": "نشاط شهري لرسائل البريد الإلكتروني", + "exceptions": "استثناءات", + "executions": "تنفيذات", + "scripts": "السكربتات", + "scripts-hourly-activity": "نشاط ساعي للسكربتات", + "scripts-daily-activity": "نشاط يومي للسكربتات", + "scripts-monthly-activity": "نشاط شهري للسكربتات", + "javascript": "جافاسكريبت", + "javascript-executions": "تنفيذات جافاسكريبت", + "tbel": "TBEL", + "tbel-executions": "تنفيذات TBEL", + "latest-error": "آخر خطأ", + "messages": "الرسائل", + "notifications": "الإشعارات", + "notifications-email-sms": "الإشعارات (البريد الإلكتروني/SMS)", + "notifications-hourly-activity": "نشاط ساعي للإشعارات", + "permanent-failures": "${entityName} فشل دائم", + "permanent-timeouts": "${entityName} مهلة دائمة", + "processing-failures": "${entityName} فشل المعالجة", + "processing-failures-and-timeouts": "فشل المعالجة والمهل", + "processing-timeouts": "${entityName} مهل المعالجة", + "queue-stats": "إحصائيات الطابور", + "rule-chain": "سلسلة القواعد", + "rule-engine": "محرك القواعد", + "rule-engine-daily-activity": "نشاط يومي لمحرك القواعد", + "rule-engine-executions": "تنفيذات محرك القواعد", + "rule-engine-hourly-activity": "نشاط ساعي لمحرك القواعد", + "rule-engine-monthly-activity": "نشاط شهري لمحرك القواعد", + "rule-engine-statistics": "إحصائيات محرك القواعد", + "rule-node": "عقدة القاعدة", + "sms": "رسائل قصيرة", + "sms-messages": "الرسائل القصيرة", + "sms-messages-daily-activity": "نشاط يومي للرسائل القصيرة", + "sms-messages-monthly-activity": "نشاط شهري للرسائل القصيرة", + "successful": "${entityName} ناجح", + "telemetry": "القياسات", + "telemetry-persistence": "استمرارية القياسات", + "telemetry-persistence-daily-activity": "نشاط يومي لاستمرارية القياسات", + "telemetry-persistence-hourly-activity": "نشاط ساعي لاستمرارية القياسات", + "telemetry-persistence-monthly-activity": "نشاط شهري لاستمرارية القياسات", + "transport": "النقل", + "transport-daily-activity": "نشاط يومي للنقل", + "transport-data-points": "نقاط بيانات النقل", + "transport-hourly-activity": "نشاط ساعي للنقل", + "transport-messages": "رسائل النقل", + "transport-monthly-activity": "نشاط شهري للنقل", + "view-details": "عرض التفاصيل", + "view-statistics": "عرض الإحصائيات" + }, + "api-limit": { + "cassandra-queries": "استعلامات Cassandra", + "entity-version-creation": "إنشاء إصدار الكيان", + "entity-version-load": "تحميل إصدار الكيان", + "integration-messages": "رسائل التكامل", + "integration-messages-per-device": "رسائل التكامل لكل جهاز", + "notification-requests": "طلبات الإشعار", + "notification-requests-per-rule": "طلبات الإشعار لكل قاعدة", + "reports-generation": "توليد التقارير", + "rest-api-requests": "طلبات REST API", + "rest-api-requests-per-customer": "طلبات REST API لكل عميل", + "transport-messages": "رسائل النقل", + "transport-messages-per-device": "رسائل النقل لكل جهاز", + "ws-updates-per-session": "تحديثات WS لكل جلسة" + }, + "audit-log": { + "audit": "تدقيق", + "audit-logs": "سجلات التدقيق", + "timestamp": "الطابع الزمني", + "entity-type": "نوع الكيان", + "entity-name": "اسم الكيان", + "user": "المستخدم", + "type": "النوع", + "status": "الحالة", + "details": "التفاصيل", + "type-added": "مُضاف", + "type-deleted": "مُحذف", + "type-updated": "مُحدث", + "type-attributes-updated": "تم تحديث السمات", + "type-attributes-deleted": "تم حذف السمات", + "type-rpc-call": "استدعاء RPC", + "type-credentials-updated": "تم تحديث الاعتمادات", + "type-assigned-to-customer": "مُعين للعميل", + "type-unassigned-from-customer": "غير مُعين من العميل", + "type-assigned-to-edge": "مُعين للحافة", + "type-unassigned-from-edge": "غير مُعين من الحافة", + "type-activated": "مُفعل", + "type-suspended": "مُعلق", + "type-credentials-read": "تم قراءة الاعتمادات", + "type-attributes-read": "تم قراءة السمات", + "type-added-to-entity-group": "مُضاف إلى المجموعة", + "type-removed-from-entity-group": "مُزال من المجموعة", + "type-relation-add-or-update": "تم تحديث العلاقة", + "type-relation-delete": "تم حذف العلاقة", + "type-relations-delete": "تم حذف جميع العلاقات", + "type-alarm-ack": "مُقر", + "type-alarm-clear": "مُزال", + "type-alarm-assign": "مُعين", + "type-alarm-unassign": "غير مُعين", + "type-added-comment": "تم إضافة تعليق", + "type-updated-comment": "تم تحديث التعليق", + "type-deleted-comment": "تم حذف التعليق", + "type-rest-api-rule-engine-call": "استدعاء API REST لمحرك القواعد", + "type-made-public": "أصبح عامًا", + "type-made-private": "أصبح خاصًا", + "type-login": "تسجيل الدخول", + "type-logout": "تسجيل الخروج", + "type-lockout": "قفل", + "status-success": "نجاح", + "status-failure": "فشل", + "audit-log-details": "تفاصيل سجل التدقيق", + "no-audit-logs-prompt": "لم يتم العثور على سجلات", + "action-data": "بيانات العمل", + "failure-details": "تفاصيل الفشل", + "search": "بحث في سجلات التدقيق", + "clear-search": "مسح البحث", + "type-assigned-from-tenant": "مُعين من المستأجر", + "type-assigned-to-tenant": "مُعين للمستأجر", + "type-provision-success": "تم توفير الجهاز", + "type-provision-failure": "فشل في توفير الجهاز", + "type-timeseries-updated": "تم تحديث القياسات", + "type-timeseries-deleted": "تم حذف القياسات", + "type-owner-changed": "تغير المالك", + "type-sms-sent": "تم إرسال رسالة قصيرة" + }, + "confirm-on-exit": { + "message": "لديك تغييرات غير محفوظة. هل أنت متأكد أنك تريد مغادرة هذه الصفحة؟", + "html-message": "لديك تغييرات غير محفوظة.
هل أنت متأكد أنك تريد مغادرة هذه الصفحة؟", + "title": "تغييرات غير محفوظة" + }, + "contact": { + "country": "البلد", + "city": "المدينة", + "state": "الولاية / المقاطعة", + "postal-code": "الرمز البريدي / الرمز البريدي", + "postal-code-invalid": "تنسيق الرمز البريدي / الرمز البريدي غير صالح.", + "address": "العنوان", + "address2": "العنوان 2", + "phone": "الهاتف", + "email": "البريد الإلكتروني", + "no-address": "لا يوجد عنوان", + "state-max-length": "يجب أن تكون الولاية أقل من 256", + "phone-max-length": "يجب أن يكون رقم الهاتف أقل من 256", + "city-max-length": "يجب أن تكون المدينة المحددة أقل من 256" + }, + "common": { + "username": "اسم المستخدم", + "password": "كلمة المرور", + "enter-username": "أدخل اسم المستخدم", + "enter-password": "أدخل كلمة المرور", + "enter-search": "أدخل للبحث", + "created-time": "وقت الإنشاء", + "loading": "جار التحميل...", + "proceed": "تقدم", + "open-details-page": "افتح صفحة التفاصيل", + "not-found": "غير موجود", + "documentation": "التوثيق" + }, + "converter": { + "converter": "محول البيانات", + "converters": "محولات البيانات", + "select-converter": "اختر محول البيانات", + "no-converters-matching": "لم يتم العثور على محولات بيانات تطابق '{{entity}}'.", + "no-converters-found": "لم يتم العثور على محولات بيانات.", + "converter-required": "محول البيانات مطلوب", + "delete": "حذف المحول", + "management": "إدارة محولات البيانات", + "add-converter-text": "أضف محول بيانات جديد", + "no-converters-text": "لم يتم العثور على محولات بيانات", + "selected-converters": "{ count, plural, =1 {1 محول بيانات} other {# محولات بيانات} } مختار", + "delete-converter-title": "هل أنت متأكد من أنك تريد حذف محول البيانات '{{converterName}}'؟", + "delete-converter-text": "كن حذرًا، بعد التأكيد سيصبح محول البيانات وجميع البيانات ذات الصلة غير قابلين للاسترداد.", + "delete-converters-title": "هل أنت متأكد من أنك تريد حذف { count, plural, =1 {1 محول بيانات} other {# محولات بيانات} }؟", + "delete-converters-action-title": "حذف { count, plural, =1 {1 محول بيانات} other {# محولات بيانات} }", + "delete-converters-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع محولات البيانات المختارة وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد.", + "events": "الأحداث", + "add": "أضف محول بيانات", + "search": "ابحث في محولات البيانات", + "converter-details": "تفاصيل محول البيانات", + "details": "التفاصيل", + "copyId": "نسخ معرف المحول", + "idCopiedMessage": "تم نسخ معرف المحول إلى الحافظة", + "debug-mode": "وضع التصحيح", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "description": "الوصف", + "decoder": "فك التشفير", + "encoder": "التشفير", + "test-decoder-fuction": "اختبر وظيفة فك التشفير", + "test-encoder-fuction": "اختبر وظيفة التشفير", + "test-with-this-message": "{{test}} مع هذه الرسالة", + "decoder-input-params": "معاملات إدخال فك التشفير", + "encoder-input-params": "معاملات إدخال التشفير", + "payload": "الحمولة", + "payload-content-type": "نوع محتوى الحمولة", + "payload-content": "محتوى الحمولة", + "message": "الرسالة", + "message-type": "نوع الرسالة", + "message-type-required": "نوع الرسالة مطلوب", + "test": "اختبار", + "metadata": "البيانات الوصفية", + "metadata-required": "لا يمكن أن تكون إدخالات البيانات الوصفية فارغة.", + "integration-metadata": "بيانات وصفية للتكامل", + "integration-metadata-required": "لا يمكن أن تكون إدخالات البيانات الوصفية للتكامل فارغة.", + "output": "المخرجات", + "import": "استيراد المحول", + "export": "تصدير المحول", + "export-failed-error": "غير قادر على تصدير المحول: {{error}}", + "create-new-converter": "إنشاء محول جديد", + "converter-file": "ملف المحول", + "invalid-converter-file-error": "غير قادر على استيراد المحول: بنية بيانات المحول غير صالحة.", + "type": "النوع", + "type-required": "النوع مطلوب.", + "type-uplink": "ربط تصاعدي", + "type-downlink": "ربط تنازلي", + "update-only-keys-list": "تحديث قائمة المفاتيح فقط", + "update-only-keys-list-hint": "سيتم حفظ القيم المرتبطة بالمفاتيح المقدمة في قاعدة البيانات فقط إذا كانت مختلفة عن القيم المقابلة في الرسالة المحولة السابقة. هذه الوظيفة تنطبق على كل من السمات والقياسات في مخرجات المحول.", + "add-key": "أضف مفتاح" + }, + "content-type": { + "json": "Json", + "text": "نص", + "binary": "ثنائي (Base64)" + }, + "color": { + "color": "اللون", + "color-picker": "اختيار اللون", + "primary-colors": "الألوان الأساسية", + "accent-colors": "الألوان الفرعية", + "no-color-selected": "لم يتم اختيار لون" + }, + "customer": { + "all": "الكل", + "all-customers": "جميع العملاء", + "groups": "المجموعات", + "shared": "مشترك", + "hierarchy": "التسلسل الهرمي", + "customer": "العميل", + "customers": "العملاء", + "management": "إدارة العملاء", + "dashboard": "لوحة تحكم العميل", + "dashboards": "لوحات تحكم العميل", + "devices": "أجهزة العميل", + "entity-views": "عرض كيانات العميل", + "assets": "أصول العميل", + "public-dashboards": "لوحات التحكم العامة", + "public-devices": "الأجهزة العامة", + "public-assets": "الأصول العامة", + "public-entity-views": "عرض الكيانات العامة", + "add": "إضافة عميل", + "delete": "حذف العميل", + "manage-customer-user-groups": "إدارة مجموعات مستخدمي العملاء", + "manage-customer-groups": "إدارة مجموعات العملاء", + "manage-customer-device-groups": "إدارة مجموعات أجهزة العملاء", + "manage-customer-asset-groups": "إدارة مجموعات أصول العملاء", + "manage-customer-entity-view-groups": "إدارة مجموعات عرض كيانات العملاء", + "manage-customer-edge-groups": "إدارة مجموعات حواف العملاء", + "manage-customer-dashboard-groups": "إدارة مجموعات لوحات تحكم العملاء", + "manage-customer-users": "إدارة مستخدمي العملاء", + "manage-customers": "إدارة العملاء", + "manage-customer-devices": "إدارة أجهزة العملاء", + "manage-customer-entity-views": "إدارة عرض كيانات العملاء", + "manage-customer-dashboards": "إدارة لوحات تحكم العملاء", + "manage-public-devices": "إدارة الأجهزة العامة", + "manage-public-dashboards": "إدارة لوحات التحكم العامة", + "manage-customer-assets": "إدارة أصول العملاء", + "manage-customer-edges": "إدارة حواف العملاء", + "manage-public-assets": "إدارة الأصول العامة", + "add-customer-text": "أضف عميل جديد", + "no-customers-text": "لم يتم العثور على عملاء", + "customer-details": "تفاصيل العميل", + "delete-customer-title": "هل أنت متأكد من أنك تريد حذف العميل '{{customerTitle}}'؟", + "delete-customer-text": "كن حذرًا، بعد التأكيد سيصبح العميل وجميع البيانات ذات الصلة غير قابلين للاسترداد.", + "delete-customers-title": "هل أنت متأكد من أنك تريد حذف { count, plural, =1 {1 عميل} other {# عملاء} }؟", + "delete-customers-action-title": "حذف { count, plural, =1 {1 عميل} other {# عملاء} }", + "delete-customers-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع العملاء المختارين وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد.", + "manage-user-groups": "إدارة مجموعات المستخدمين", + "manage-asset-groups": "إدارة مجموعات الأصول", + "manage-device-groups": "إدارة مجموعات الأجهزة", + "manage-dashboard-groups": "إدارة مجموعات لوحات التحكم", + "manage-entity-view-groups": "إدارة مجموعات عرض الكيانات", + "manage-edge-groups": "إدارة مجموعات الحواف", + "manage-users": "إدارة المستخدمين", + "manage-assets": "إدارة الأصول", + "manage-devices": "إدارة الأجهزة", + "manage-dashboards": "إدارة لوحات التحكم", + "manage-entity-views": "إدارة عرض الكيانات", + "title": "العنوان", + "title-required": "العنوان مطلوب.", + "title-max-length": "يجب أن يكون العنوان أقل من 256", + "description": "الوصف", + "details": "التفاصيل", + "events": "الأحداث", + "copyId": "نسخ معرف العميل", + "idCopiedMessage": "تم نسخ معرف العميل إلى الحافظة", + "select-customer": "اختر عميل", + "no-customers-matching": "لم يتم العثور على عملاء يطابقون '{{entity}}'.", + "customer-required": "العميل مطلوب", + "select-group-to-add": "اختر المجموعة المستهدفة لإضافة العملاء المختارين", + "select-group-to-move": "اختر المجموعة المستهدفة لنقل العملاء المختارين", + "remove-customers-from-group": "هل أنت متأكد من أنك تريد إزالة { count, plural, =1 {1 عميل} other {# عملاء} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة العملاء", + "list-of-groups": "{ count, plural, =1 {مجموعة عملاء واحدة} other {قائمة بـ # مجموعات العملاء} }", + "group-name-starts-with": "مجموعات العملاء التي تبدأ أسماؤها بـ '{{prefix}}'", + "select-default-customer": "اختر العميل الافتراضي", + "default-customer": "العميل الافتراضي", + "default-customer-required": "العميل الافتراضي مطلوب لتصحيح لوحة التحكم على مستوى المستأجر", + "allow-white-labeling": "السماح بالتسمية البيضاء", + "search": "بحث في العملاء", + "selected-customers": "{ count, plural, =1 {1 عميل} other {# عملاء} } مختار", + "edges": "حواف العميل", + "manage-edges": "إدارة الحواف" + }, + "customers-hierarchy": { + "customers-hierarchy": "تسلسل العملاء الهرمي", + "open-nav-tree": "فتح شجرة التنقل", + "return-to-top-level": "العودة إلى المستوى الأعلى" + }, + "custom-menu": { + "custom-menu": "قائمة مخصصة", + "custom-menu-hint": "حدد قائمة مخصصة JSON أدناه. هذا JSON يحتوي على قائمة بعناصر القائمة المخصصة." + }, + "custom-translation": { + "custom-translation": "ترجمة مخصصة", + "translation-map": "خريطة الترجمة", + "key": "مفتاح الترجمة", + "import": "استيراد الترجمة", + "export": "تصدير الترجمة", + "export-data": "تصدير بيانات الترجمة", + "import-data": "استيراد بيانات الترجمة", + "translation-file": "ملف الترجمة", + "invalid-translation-file-error": "غير قادر على استيراد ملف الترجمة: بنية بيانات الترجمة غير صالحة.", + "custom-translation-hint": "حدد ترجمة مخصصة JSON أدناه. سيتم استبدال هذا JSON بالترجمة الافتراضية. انقر على 'تنزيل ملف اللغة' للحصول على الترجمة الحالية. يمكنك أيضًا استخدام الملف المحمل كمرجع لمفتاح قيم الترجمة المتاحة.", + "download-locale-file": "تنزيل ملف اللغة" + }, + "date": { + "last-update-n-ago": "آخر تحديث منذ N", + "last-update-n-ago-text": "آخر تحديث {{ agoText }}", + "custom-date": "تاريخ مخصص", + "format": "التنسيق", + "preview": "معاينة" + }, + "datetime": { + "date-from": "التاريخ من", + "time-from": "الوقت من", + "date-to": "التاريخ إلى", + "time-to": "الوقت إلى" + }, + "dashboard": { + "all": "الكل", + "all-dashboards": "جميع لوحات المعلومات", + "groups": "المجموعات", + "shared": "مشترك", + "dashboard": "لوحة المعلومات", + "dashboards": "لوحات المعلومات", + "management": "إدارة لوحات المعلومات", + "view-dashboards": "عرض لوحات المعلومات", + "add": "إضافة لوحة معلومات", + "assign-dashboard-to-customer": "تعيين لوحة(لوحات) المعلومات للعميل", + "assign-dashboard-to-customer-text": "يرجى اختيار لوحات المعلومات لتعيينها للعميل", + "assign-to-customer-text": "يرجى اختيار العميل لتعيين لوحة(لوحات) المعلومات له", + "assign-to-customer": "تعيين للعميل", + "unassign-from-customer": "إلغاء التعيين من العميل", + "make-public": "جعل لوحة المعلومات عامة", + "make-private": "جعل لوحة المعلومات خاصة", + "manage-assigned-customers": "إدارة العملاء المعينين", + "assigned-customers": "العملاء المعينون", + "assign-to-customers": "تعيين لوحة(لوحات) المعلومات للعملاء", + "assign-to-customers-text": "يرجى اختيار العملاء لتعيين لوحة(لوحات) المعلومات لهم", + "unassign-from-customers": "إلغاء تعيين لوحة(لوحات) المعلومات من العملاء", + "unassign-from-customers-text": "يرجى اختيار العملاء لإلغاء تعيين لوحة(لوحات) المعلومات منهم", + "no-dashboards-text": "لم يتم العثور على لوحات معلومات", + "no-widgets": "لا توجد أدوات مُعدة", + "add-widget": "إضافة أداة جديدة", + "add-widget-button-text": "إضافة أداة", + "title": "العنوان", + "image": "صورة لوحة المعلومات", + "mobile-app-settings": "إعدادات التطبيق المحمول", + "mobile-order": "ترتيب لوحة المعلومات في التطبيق المحمول", + "mobile-hide": "إخفاء لوحة المعلومات في التطبيق المحمول", + "update-image": "تحديث صورة لوحة المعلومات", + "take-screenshot": "التقاط لقطة شاشة", + "select-widget-title": "اختر عنوان الأداة", + "select-widget-value": "{{title}}: اختر أداة", + "select-widget-subtitle": "قائمة بأنواع الأدوات المتاحة", + "delete": "حذف لوحة المعلومات", + "title-required": "العنوان مطلوب.", + "title-max-length": "يجب أن يكون العنوان أقل من 256", + "description": "الوصف", + "details": "التفاصيل", + "dashboard-details": "تفاصيل لوحة المعلومات", + "add-dashboard-text": "إضافة لوحة معلومات جديدة", + "assign-dashboards": "تعيين لوحات المعلومات", + "assign-new-dashboard": "تعيين لوحة معلومات جديدة", + "assign-dashboards-text": "تعيين { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} } للعملاء", + "unassign-dashboards-action-text": "إلغاء تعيين { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} } من العملاء", + "delete-dashboards": "حذف لوحات المعلومات", + "unassign-dashboards": "إلغاء تعيين لوحات المعلومات", + "unassign-dashboards-action-title": "إلغاء تعيين { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} } من العميل", + "delete-dashboard-title": "هل أنت متأكد أنك تريد حذف لوحة المعلومات '{{dashboardTitle}}'؟", + "delete-dashboard-text": "كن حذرًا، بعد التأكيد ستصبح لوحة المعلومات وجميع البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-dashboards-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} }؟", + "delete-dashboards-action-title": "حذف { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} }", + "delete-dashboards-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع لوحات المعلومات المختارة وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد.", + "unassign-dashboard-title": "هل أنت متأكد أنك تريد إلغاء تعيين لوحة المعلومات '{{dashboardTitle}}'؟", + "unassign-dashboard-text": "بعد التأكيد سيتم إلغاء تعيين لوحة المعلومات ولن تكون متاحة للعميل.", + "unassign-dashboard": "إلغاء تعيين لوحة المعلومات", + "unassign-dashboards-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} }؟", + "unassign-dashboards-text": "بعد التأكيد سيتم إلغاء تعيين جميع لوحات المعلومات المختارة ولن تكون متاحة للعميل.", + "public-dashboard-title": "لوحة المعلومات الآن عامة", + "public-dashboard-text": "لوحة المعلومات {{dashboardTitle}} الخاصة بك أصبحت الآن عامة ويمكن الوصول إليها عبر الرابط العام التالي الرابط:", + "public-dashboard-notice": "ملاحظة: لا تنسى جعل الأجهزة ذات الصلة عامة للوصول إلى بياناتها.", + "public-dashboard-link": "رابط لوحة المعلومات العامة", + "public-dashboard-link-text": "يمكن الوصول إلى لوحة المعلومات العامة {{dashboardTitle}} عبر الرابط العام التالي الرابط:", + "public-dashboard-link-notice": "ملاحظة: لا تنسى جعل الأجهزة والأصول وعروض الكيانات العامة ذات الصلة عامة للوصول إلى بياناتها.", + "make-private-dashboard-title": "هل أنت متأكد أنك تريد جعل لوحة المعلومات '{{dashboardTitle}}' خاصة؟", + "make-private-dashboard-text": "بعد التأكيد ستصبح لوحة المعلومات خاصة ولن تكون متاحة للآخرين.", + "make-private-dashboard": "جعل لوحة المعلومات خاصة", + "socialshare-text": "'{{dashboardTitle}}' بدعم من ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' بدعم من ThingsBoard", + "select-dashboard": "اختر لوحة المعلومات", + "no-dashboards-matching": "لم يتم العثور على لوحات معلومات تطابق '{{entity}}'.", + "dashboard-required": "لوحة المعلومات مطلوبة.", + "select-existing": "اختر لوحة المعلومات الحالية", + "create-new": "إنشاء لوحة معلومات جديدة", + "new-dashboard-title": "عنوان لوحة المعلومات الجديدة", + "open-dashboard": "فتح لوحة المعلومات", + "set-background": "تعيين الخلفية", + "background-color": "لون الخلفية", + "background-image": "صورة الخلفية", + "background-size-mode": "وضع حجم الخلفية", + "no-image": "لم يتم اختيار صورة", + "empty-image": "لا توجد صورة", + "drop-image": "أسقط صورة أو انقر لاختيار ملف للتحميل.", + "maximum-upload-file-size": "الحد الأقصى لحجم الملف المرفوع: {{ size }}", + "cannot-upload-file": "لا يمكن تحميل الملف", + "settings": "الإعدادات", + "layout-settings": "إعدادات التخطيط", + "columns-count": "عدد الأعمدة", + "columns-count-required": "عدد الأعمدة مطلوب.", + "min-columns-count-message": "الحد الأدنى المسموح به هو 10 أعمدة.", + "max-columns-count-message": "الحد الأقصى المسموح به هو 1000 عمود.", + "widgets-margins": "هامش بين الأدوات", + "margin-required": "قيمة الهامش مطلوبة.", + "min-margin-message": "0 هو الحد الأدنى المسموح به كقيمة هامش.", + "max-margin-message": "50 هو الحد الأقصى المسموح به كقيمة هامش.", + "horizontal-margin": "الهامش الأفقي", + "horizontal-margin-required": "قيمة الهامش الأفقي مطلوبة.", + "min-horizontal-margin-message": "0 هو الحد الأدنى المسموح به كقيمة للهامش الأفقي.", + "max-horizontal-margin-message": "50 هو الحد الأقصى المسموح به كقيمة للهامش الأفقي.", + "vertical-margin": "الهامش العمودي", + "vertical-margin-required": "قيمة الهامش العمودي مطلوبة.", + "min-vertical-margin-message": "0 هو الحد الأدنى المسموح به كقيمة للهامش العمودي.", + "max-vertical-margin-message": "50 هو الحد الأقصى المسموح به كقيمة للهامش العمودي.", + "apply-outer-margin": "تطبيق الهامش على جوانب التخطيط", + "autofill-height": "تعبئة ارتفاع التخطيط تلقائيًا", + "mobile-layout": "إعدادات التخطيط للجوال", + "mobile-row-height": "ارتفاع الصف في التخطيط للجوال، بكسل", + "mobile-row-height-required": "قيمة ارتفاع الصف للجوال مطلوبة.", + "min-mobile-row-height-message": "5 بكسل هو الحد الأدنى المسموح به كقيمة لارتفاع الصف للجوال.", + "max-mobile-row-height-message": "200 بكسل هو الحد الأقصى المسموح به كقيمة لارتفاع الصف للجوال.", + "title-settings": "إعدادات العنوان", + "display-title": "عرض عنوان لوحة المعلومات", + "title-color": "لون العنوان", + "toolbar-settings": "إعدادات شريط الأدوات", + "hide-toolbar": "إخفاء شريط الأدوات", + "toolbar-always-open": "الإبقاء على شريط الأدوات مفتوحًا", + "display-dashboards-selection": "عرض اختيار لوحات المعلومات", + "display-entities-selection": "عرض اختيار الكيانات", + "display-filters": "عرض الفلاتر", + "display-dashboard-timewindow": "عرض نافذة الوقت للوحة المعلومات", + "display-dashboard-export": "عرض تصدير لوحة المعلومات", + "display-update-dashboard-image": "عرض تحديث صورة لوحة المعلومات", + "dashboard-logo-settings": "إعدادات شعار لوحة المعلومات", + "display-dashboard-logo": "عرض الشعار في وضع ملء الشاشة للوحة المعلومات", + "dashboard-logo-image": "صورة شعار لوحة المعلومات", + "advanced-settings": "الإعدادات المتقدمة", + "dashboard-css": "CSS للوحة المعلومات", + "import": "استيراد لوحة المعلومات", + "export": "تصدير لوحة المعلومات", + "export-failed-error": "غير قادر على تصدير لوحة المعلومات: {{error}}", + "export-pdf": "تصدير كـ PDF", + "export-png": "تصدير كـ PNG", + "export-jpg": "تصدير كـ JPEG", + "export-json-config": "تصدير تكوين JSON", + "download-dashboard-progress": "جار توليد {{reportType}} للوحة المعلومات...", + "create-new-dashboard": "إنشاء لوحة معلومات جديدة", + "dashboard-file": "ملف لوحة المعلومات", + "invalid-dashboard-file-error": "غير قادر على استيراد لوحة المعلومات: بنية بيانات لوحة المعلومات غير صالحة.", + "dashboard-import-missing-aliases-title": "تكوين الأسماء المستعارة المستخدمة بواسطة لوحة المعلومات المستوردة", + "create-new-widget": "إنشاء أداة جديدة", + "import-widget": "استيراد أداة", + "widget-file": "ملف الأداة", + "invalid-widget-file-error": "غير قادر على استيراد الأداة: بنية بيانات الأداة غير صالحة.", + "widget-import-missing-aliases-title": "تكوين الأسماء المستعارة المستخدمة بواسطة الأداة المستوردة", + "open-toolbar": "فتح شريط أدوات لوحة المعلومات", + "close-toolbar": "إغلاق شريط أدوات لوحة المعلومات", + "configuration-error": "خطأ في التكوين", + "alias-resolution-error-title": "خطأ في تكوين الأسماء المستعارة للوحة المعلومات", + "invalid-aliases-config": "غير قادر على العثور على أي أجهزة تطابق فلتر الأسماء المستعارة.
يرجى الاتصال بمدير النظام لحل هذه المشكلة.", + "select-devices": "اختر الأجهزة", + "assignedToCustomer": "معين للعميل", + "assignedToCustomers": "معين للعملاء", + "public": "عام", + "copyId": "نسخ معرف لوحة المعلومات", + "idCopiedMessage": "تم نسخ معرف لوحة المعلومات إلى الحافظة", + "public-link": "الرابط العام", + "copy-public-link": "نسخ الرابط العام", + "public-link-copied-message": "تم نسخ الرابط العام للوحة المعلومات إلى الحافظة", + "manage-states": "إدارة حالات لوحة المعلومات", + "states": "حالات لوحة المعلومات", + "states-short": "الحالات", + "search-states": "بحث في حالات لوحة المعلومات", + "selected-states": "{ count, plural, =1 {حالة لوحة معلومات واحدة} other {# حالات لوحات المعلومات} } مختارة", + "edit-state": "تعديل حالة لوحة المعلومات", + "delete-state": "حذف حالة لوحة المعلومات", + "add-state": "إضافة حالة لوحة المعلومات", + "no-states-text": "لم يتم العثور على حالات", + "state": "حالة لوحة المعلومات", + "state-name": "الاسم", + "state-name-required": "اسم حالة لوحة المعلومات مطلوب.", + "state-id": "معرف الحالة", + "state-id-required": "معرف حالة لوحة المعلومات مطلوب.", + "state-id-exists": "حالة لوحة المعلومات بنفس المعرف موجودة بالفعل.", + "is-root-state": "حالة الجذر", + "delete-state-title": "حذف حالة لوحة المعلومات", + "delete-state-text": "هل أنت متأكد أنك تريد حذف حالة لوحة المعلومات بالاسم '{{stateName}}'؟", + "show-details": "عرض التفاصيل", + "hide-details": "إخفاء التفاصيل", + "select-state": "اختر الحالة المستهدفة", + "state-controller": "تحكم حالة لوحة المعلومات", + "state-controller-default": "ثابت (مُهمل)", + "search": "بحث في لوحات المعلومات", + "selected-dashboards": "{ count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} } مختارة", + "home-dashboard": "لوحة المعلومات الرئيسية", + "home-dashboard-hide-toolbar": "إخفاء شريط أدوات لوحة المعلومات الرئيسية", + "select-group-to-add": "اختر المجموعة المستهدفة لإضافة لوحات المعلومات المختارة", + "select-group-to-move": "اختر المجموعة المستهدفة لنقل لوحات المعلومات المختارة", + "remove-dashboards-from-group": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة لوحات المعلومات", + "list-of-groups": "{ count, plural, =1 {مجموعة لوحة معلومات واحدة} other {قائمة بـ # مجموعات لوحات المعلومات} }", + "group-name-starts-with": "مجموعات لوحات المعلومات التي تبدأ أسماؤها بـ '{{prefix}}'", + "unassign-dashboard-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين لوحة المعلومات ولن تكون متاحة بواسطة الحافة.", + "unassign-dashboards-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {لوحة معلومات واحدة} other {# لوحات المعلومات} }؟", + "unassign-dashboards-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين جميع لوحات المعلومات المختارة ولن تكون متاحة بواسطة الحافة.", + "assign-dashboard-to-edge": "تعيين لوحة(لوحات) المعلومات للحافة", + "assign-dashboard-to-edge-text": "يرجى اختيار لوحات المعلومات لتعيينها للحافة", + "non-existent-dashboard-state-error": "حالة لوحة المعلومات بمعرف \"{{ stateId }}\" غير موجودة", + "edit-mode": "وضع التحرير", + "duplicate-state-action": "تكرار الحالة" + }, + "datakey": { + "settings": "الإعدادات", + "general": "عام", + "advanced": "متقدم", + "key": "المفتاح", + "label": "التسمية", + "color": "اللون", + "units": "رمز خاص للعرض بجانب القيمة", + "decimals": "عدد الأرقام بعد النقطة العائمة", + "data-generation-func": "وظيفة توليد البيانات", + "use-data-post-processing-func": "استخدم وظيفة معالجة البيانات بعد الإدخال", + "configuration": "تكوين مفتاح البيانات", + "timeseries": "السلاسل الزمنية", + "attributes": "السمات", + "entity-field": "حقل الكيان", + "alarm": "حقول التنبيه", + "timeseries-required": "سلاسل البيانات الزمنية للكيان مطلوبة.", + "timeseries-or-attributes-required": "سلاسل البيانات الزمنية للكيان/السمات مطلوبة.", + "alarm-fields-timeseries-or-attributes-required": "حقول التنبيه أو سلاسل البيانات الزمنية للكيان/السمات مطلوبة.", + "maximum-timeseries-or-attributes": "الحد الأقصى { count, plural, =1 {سلسلة بيانات زمنية/سمة واحدة مسموح بها.} other {# من سلاسل البيانات الزمنية/السمات مسموح بها} }", + "alarm-fields-required": "حقول التنبيه مطلوبة.", + "function-types": "أنواع الوظائف", + "function-type": "نوع الوظيفة", + "function-types-required": "أنواع الوظائف مطلوبة.", + "data-keys": "مفاتيح البيانات", + "data-key": "مفتاح البيانات", + "data-keys-required": "مفاتيح البيانات مطلوبة.", + "data-key-required": "مفتاح البيانات مطلوب.", + "alarm-keys": "مفاتيح بيانات التنبيه", + "alarm-key": "مفتاح بيانات التنبيه", + "alarm-key-functions": "وظائف مفتاح التنبيه", + "alarm-key-function": "وظيفة مفتاح التنبيه", + "latest-keys": "آخر مفاتيح البيانات", + "latest-key": "آخر مفتاح بيانات", + "latest-key-functions": "وظائف آخر مفتاح", + "latest-key-function": "وظيفة آخر مفتاح", + "timeseries-keys": "مفاتيح بيانات السلاسل الزمنية", + "timeseries-key": "مفتاح بيانات السلسلة الزمنية", + "timeseries-key-functions": "وظائف مفتاح السلسلة الزمنية", + "timeseries-key-function": "وظيفة مفتاح السلسلة الزمنية", + "maximum-function-types": "الحد الأقصى { count, plural, =1 {نوع وظيفة واحد مسموح به.} other {# من أنواع الوظائف مسموح بها} }", + "time-description": "طابع زمني للقيمة الحالية؛", + "value-description": "القيمة الحالية؛", + "prev-value-description": "نتيجة آخر استدعاء للوظيفة؛", + "time-prev-description": "طابع زمني للقيمة السابقة؛", + "prev-orig-value-description": "القيمة الأصلية السابقة؛", + "aggregation": "تجميع", + "aggregation-type-hint-common": "لأسباب تتعلق بالأداء، يتوفر حساب القيم المجمعة فقط لفترات زمنية ثابتة مثل \"اليوم الحالي\"، \"الشهر الحالي\"، إلخ، وغير متاحة لفترات النافذة الزمنية المتحركة مثل 'آخر 30 دقيقة' أو 'آخر 24 ساعة'.", + "aggregation-type-none-hint": "أخذ آخر قيمة.", + "aggregation-type-min-hint": "العثور على القيمة الدنيا بين نقاط البيانات ضمن نافذة زمنية محددة.", + "aggregation-type-max-hint": "العثور على القيمة العظمى بين نقاط البيانات ضمن نافذة زمنية محددة.", + "aggregation-type-avg-hint": "حساب قيمة متوسطة بين نقاط البيانات ضمن نافذة زمنية محددة.", + "aggregation-type-sum-hint": "جمع جميع قيم نقاط البيانات ضمن نافذة زمنية محددة.", + "aggregation-type-count-hint": "إجمالي عدد نقاط البيانات ضمن نافذة زمنية محددة.", + "delta-calculation": "حساب الفارق", + "enable-delta-calculation": "تمكين حساب الفارق", + "enable-delta-calculation-hint": "عند التمكين، يتم حساب قيمة مفتاح البيانات بناءً على القيم المجمعة لنافذة زمنية محددة وفترة مقارنة محددة. لأسباب تتعلق بالأداء، يتوفر حساب الفارق فقط لنوافذ الزمن التاريخية وليس للقيم الزمنية الفعلية. على سبيل المثال، يمكنك حساب الفارق بين استهلاك الطاقة للأمس مقارنةً باليوم الذي سبقه.", + "delta-calculation-result": "نتيجة حساب الفارق", + "delta-calculation-result-previous-value": "القيمة السابقة", + "delta-calculation-result-delta-absolute": "الفارق (مطلق)", + "delta-calculation-result-delta-percent": "الفارق (نسبة مئوية)", + "source": "المصدر", + "latest": "آخر", + "latest-value": "آخر قيمة", + "delta": "الفارق", + "percent": "نسبة مئوية", + "absolute": "مطلق" + }, + "datasource": { + "type": "نوع مصدر البيانات", + "name": "الاسم", + "label": "التسمية", + "add-datasource-prompt": "يرجى إضافة مصدر بيانات" + }, + "details": { + "details": "تفاصيل", + "edit-mode": "وضع التحرير", + "edit-json": "تحرير JSON", + "toggle-edit-mode": "تبديل وضع التحرير" + }, + "device": { + "all": "الكل", + "all-devices": "جميع الأجهزة", + "groups": "المجموعات", + "shared": "مشترك", + "device": "الجهاز", + "device-required": "الجهاز مطلوب.", + "devices": "الأجهزة", + "management": "إدارة الأجهزة", + "view-devices": "عرض الأجهزة", + "device-alias": "اسم مستعار للجهاز", + "device-type-max-length": "يجب أن يكون نوع الجهاز أقل من 256", + "aliases": "أسماء مستعارة للجهاز", + "no-alias-matching": "لم يتم العثور على '{{alias}}'.", + "no-aliases-found": "لم يتم العثور على أسماء مستعارة.", + "no-key-matching": "لم يتم العثور على '{{key}}'.", + "no-keys-found": "لم يتم العثور على مفاتيح.", + "create-new-alias": "إنشاء جديد!", + "create-new-key": "إنشاء جديد!", + "duplicate-alias-error": "تم العثور على اسم مستعار مكرر '{{alias}}'.
يجب أن تكون أسماء الأجهزة المستعارة فريدة ضمن لوحة المعلومات.", + "configure-alias": "تكوين الاسم المستعار '{{alias}}'", + "no-devices-matching": "لم يتم العثور على أجهزة تطابق '{{entity}}'.", + "alias": "الاسم المستعار", + "alias-required": "الاسم المستعار للجهاز مطلوب.", + "remove-alias": "إزالة اسم مستعار للجهاز", + "add-alias": "إضافة اسم مستعار للجهاز", + "name-starts-with": "تعبير اسم الجهاز", + "help-text": "استخدم '%' وفقًا للحاجة: '%device_name_contains%', '%device_name_ends', 'device_starts_with'.", + "device-list": "قائمة الأجهزة", + "use-device-name-filter": "استخدم الفلتر", + "device-list-empty": "لم يتم اختيار أجهزة.", + "device-name-filter-required": "فلتر اسم الجهاز مطلوب.", + "device-name-filter-no-device-matched": "لم يتم العثور على أجهزة تبدأ بـ '{{device}}'.", + "add": "إضافة جهاز", + "assign-to-customer": "تعيين للعميل", + "assign-device-to-customer": "تعيين جهاز(أجهزة) للعميل", + "assign-device-to-customer-text": "يرجى اختيار الأجهزة لتعيينها للعميل", + "make-public": "جعل الجهاز عام", + "make-private": "جعل الجهاز خاص", + "no-devices-text": "لم يتم العثور على أجهزة", + "assign-to-customer-text": "يرجى اختيار العميل لتعيين الجهاز(الأجهزة) له", + "device-details": "تفاصيل الجهاز", + "add-device-text": "إضافة جهاز جديد", + "credentials": "بيانات الاعتماد", + "manage-credentials": "إدارة بيانات الاعتماد", + "delete": "حذف الجهاز", + "assign-devices": "تعيين الأجهزة", + "assign-devices-text": "تعيين { count, plural, =1 {جهاز واحد} other {# أجهزة} } للعميل", + "delete-devices": "حذف الأجهزة", + "unassign-from-customer": "إلغاء التعيين من العميل", + "unassign-devices": "إلغاء تعيين الأجهزة", + "unassign-devices-action-title": "إلغاء تعيين { count, plural, =1 {جهاز واحد} other {# أجهزة} } من العميل", + "unassign-device-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين الجهاز '{{deviceName}}'؟", + "unassign-device-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين الجهاز ولن يكون متاحًا بواسطة الحافة.", + "unassign-devices-from-edge": "إلغاء تعيين الأجهزة من الحافة", + "assign-new-device": "تعيين جهاز جديد", + "make-public-device-title": "هل أنت متأكد أنك تريد جعل الجهاز '{{deviceName}}' عام؟", + "make-public-device-text": "بعد التأكيد سيصبح الجهاز وجميع بياناته عامة ومتاحة للآخرين.", + "make-private-device-title": "هل أنت متأكد أنك تريد جعل الجهاز '{{deviceName}}' خاص؟", + "make-private-device-text": "بعد التأكيد سيصبح الجهاز وجميع بياناته خاصة ولن تكون متاحة للآخرين.", + "view-credentials": "عرض بيانات الاعتماد", + "delete-device-title": "هل أنت متأكد أنك تريد حذف الجهاز '{{deviceName}}'؟", + "delete-device-text": "كن حذرًا، بعد التأكيد سيتم حذف الجهاز وجميع البيانات ذات الصلة ولن يمكن استردادها.", + "delete-devices-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {جهاز واحد} other {# أجهزة} }؟", + "delete-devices-action-title": "حذف { count, plural, =1 {جهاز واحد} other {# أجهزة} }", + "delete-devices-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع الأجهزة المختارة وجميع البيانات ذات الصلة ولن يمكن استردادها.", + "unassign-device-title": "هل أنت متأكد أنك تريد إلغاء تعيين الجهاز '{{deviceName}}'؟", + "unassign-device-text": "بعد التأكيد سيتم إلغاء تعيين الجهاز ولن يكون متاحًا للعميل.", + "unassign-device": "إلغاء تعيين الجهاز", + "unassign-devices-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {جهاز واحد} other {# أجهزة} }؟", + "unassign-devices-text": "بعد التأكيد سيتم إلغاء تعيين جميع الأجهزة المختارة ولن تكون متاحة للعميل.", + "device-credentials": "بيانات اعتماد الجهاز", + "loading-device-credentials": "جار تحميل بيانات اعتماد الجهاز...", + "credentials-type": "نوع بيانات الاعتماد", + "access-token": "رمز الوصول", + "access-token-required": "رمز الوصول مطلوب.", + "access-token-invalid": "يجب أن يكون طول رمز الوصول من 1 إلى 32 حرفًا.", + "certificate-pem-format": "شهادة بتنسيق PEM", + "certificate-pem-format-required": "الشهادة مطلوبة.", + "copy-access-token": "نسخ رمز الوصول", + "copy-certificate": "نسخ الشهادة", + "copy-client-id": "نسخ معرف العميل", + "copy-user-name": "نسخ اسم المستخدم", + "copy-password": "نسخ كلمة المرور", + "generate-client-id": "توليد معرف العميل", + "generate-user-name": "توليد اسم المستخدم", + "generate-password": "توليد كلمة المرور", + "generate-access-token": "توليد رمز الوصول", + "lwm2m-security-config": { + "identity": "هوية العميل", + "identity-required": "هوية العميل مطلوبة.", + "identity-tooltip": "معرف PSK هو معرف PSK تعسفي يصل إلى 128 بايت، كما هو موصوف في المعيار [RFC7925].\nيجب تحويل معرف PSK أولاً إلى سلسلة أحرف ثم ترميزها إلى أوكتيتات باستخدام UTF-8.", + "client-key": "مفتاح العميل", + "client-key-required": "مفتاح العميل مطلوب.", + "client-key-tooltip-prk": "يجب أن يكون المفتاح العام لـ RPK أو المعرف وفقًا للمعيار [RFC7250] ومرمزًا إلى تنسيق Base64!", + "client-key-tooltip-psk": "يجب أن يكون مفتاح PSK وفقًا للمعيار [RFC4279] وبتنسيق HexDec: 32، 64، 128 حرفًا!", + "endpoint": "اسم نهاية العميل", + "endpoint-required": "اسم نهاية العميل مطلوب.", + "client-public-key": "المفتاح العام للعميل", + "client-public-key-hint": "إذا كان المفتاح العام للعميل فارغًا، سيتم استخدام الشهادة الموثوق بها", + "client-public-key-tooltip": "يجب أن يكون المفتاح العام لـ X509 بتنسيق X509v3 المشفر بـ DER ويدعم خوارزمية EC حصريًا ثم يتم ترميزه إلى تنسيق Base64!", + "mode": "وضع تكوين الأمان", + "client-tab": "تكوين أمان العميل", + "client-certificate": "شهادة العميل", + "bootstrap-tab": "عميل الإقلاع", + "bootstrap-server": "خادم الإقلاع", + "lwm2m-server": "خادم LwM2M", + "client-publicKey-or-id": "المفتاح العام للعميل أو المعرف", + "client-publicKey-or-id-required": "المفتاح العام للعميل أو المعرف مطلوب.", + "client-publicKey-or-id-tooltip-psk": "معرف PSK هو معرف PSK تعسفي يصل إلى 128 بايت، كما هو موصوف في المعيار [RFC7925].\nيجب تحويل معرف PSK أولاً إلى سلسلة أحرف ثم ترميزها إلى أوكتيتات باستخدام UTF-8.", + "client-publicKey-or-id-tooltip-rpk": "يجب أن يكون المفتاح العام لـ RPK أو المعرف وفقًا للمعيار [RFC7250] ومرمزًا إلى تنسيق Base64!", + "client-publicKey-or-id-tooltip-x509": "يجب أن يكون المفتاح العام لـ X509 بتنسيق X509v3 المشفر بـ DER ويدعم خوارزمية EC حصريًا ثم يتم ترميزه إلى تنسيق Base64", + "client-secret-key": "المفتاح السري للعميل", + "client-secret-key-required": "المفتاح السري للعميل مطلوب.", + "client-secret-key-tooltip-psk": "يجب أن يكون مفتاح PSK وفقًا للمعيار [RFC4279] وبتنسيق HexDec: 32، 64، 128 حرفًا!", + "client-secret-key-tooltip-prk": "يجب أن يكون المفتاح السري لـ RPK بتنسيق PKCS_8 (ترميز DER، المعيار [RFC5958]) ثم يتم ترميزه إلى تنسيق Base64!", + "client-secret-key-tooltip-x509": "يجب أن يكون المفتاح السري لـ X509 بتنسيق PKCS_8 (ترميز DER، المعيار [RFC5958]) ثم يتم ترميزه إلى تنسيق Base64!" + }, + "client-id": "معرّف العميل", + "client-id-pattern": "يحتوي على حرف غير صالح.", + "user-name": "اسم المستخدم", + "user-name-required": "اسم المستخدم مطلوب.", + "client-id-or-user-name-necessary": "معرّف العميل و/أو اسم المستخدم ضروريان", + "password": "كلمة المرور", + "secret": "السر", + "secret-required": "السر مطلوب.", + "device-type": "ملف تعريف الجهاز", + "device-type-required": "نوع الجهاز مطلوب.", + "select-device-type": "اختر نوع الجهاز", + "enter-device-type": "أدخل ملف تعريف الجهاز", + "any-device": "أي جهاز", + "no-device-types-matching": "لم يتم العثور على ملفات تعريف جهاز تطابق '{{entitySubtype}}'.", + "device-type-list-empty": "لم يتم اختيار ملفات تعريف جهاز!", + "device-profile-type-list-empty": "يجب اختيار ملف تعريف جهاز واحد على الأقل.", + "device-types": "أنواع الأجهزة", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "label-max-length": "يجب أن تكون التسمية أقل من 256", + "description": "الوصف", + "label": "التسمية", + "events": "الأحداث", + "details": "التفاصيل", + "copyId": "نسخ معرّف الجهاز", + "copyAccessToken": "نسخ رمز الوصول", + "copy-mqtt-authentication": "نسخ بيانات اعتماد MQTT", + "idCopiedMessage": "تم نسخ معرّف الجهاز إلى الحافظة", + "accessTokenCopiedMessage": "تم نسخ رمز وصول الجهاز إلى الحافظة", + "mqtt-authentication-copied-message": "تم نسخ بيانات اعتماد MQTT الخاصة بالجهاز إلى الحافظة", + "assignedToCustomer": "معيّن للعميل", + "unable-delete-device-alias-title": "غير قادر على حذف الاسم المستعار للجهاز", + "unable-delete-device-alias-text": "لا يمكن حذف الاسم المستعار '{{deviceAlias}}' لأنه يستخدم من قبل الأدوات التالية:
{{widgetsList}}", + "is-gateway": "هو بوابة", + "overwrite-activity-time": "استبدال وقت النشاط للجهاز المتصل", + "device-filter": "فلتر الجهاز", + "device-filter-title": "فلتر الجهاز", + "filter-title": "الفلتر", + "device-state": "حالة الجهاز", + "state": "الحالة", + "any": "أي", + "active": "نشط", + "inactive": "غير نشط", + "public": "عام", + "device-public": "الجهاز عام", + "select-device": "اختر جهاز", + "select-group-to-add": "اختر المجموعة المستهدفة لإضافة الأجهزة المختارة", + "select-group-to-move": "اختر المجموعة المستهدفة لنقل الأجهزة المختارة", + "remove-devices-from-group": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {جهاز واحد} other {# أجهزة} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة الأجهزة", + "list-of-groups": "{ count, plural, =1 {مجموعة جهاز واحدة} other {قائمة # مجموعات الأجهزة} }", + "group-name-starts-with": "مجموعات الأجهزة التي تبدأ أسماؤها بـ '{{prefix}}'", + "import": "استيراد جهاز", + "device-file": "ملف الجهاز", + "search": "بحث في الأجهزة", + "selected-devices": "{ count, plural, =1 {جهاز واحد} other {# أجهزة} } مختارة", + "device-configuration": "تكوين الجهاز", + "transport-configuration": "تكوين النقل", + "wizard": { + "device-details": "تفاصيل الجهاز" + }, + "unassign-devices-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {جهاز واحد} other {# أجهزة} }؟", + "unassign-devices-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين جميع الأجهزة المختارة ولن تكون متاحة بواسطة الحافة.", + "time": "الوقت", + "connectivity": { + "check-connectivity": "تحقق من الاتصال", + "device-created-check-connectivity": "تم إنشاء الجهاز. دعنا نتحقق من الاتصال!", + "loading-check-connectivity-command": "جار تحميل أوامر التحقق من الاتصال...", + "use-following-instructions": "استخدم التعليمات التالية لإرسال بيانات القياس نيابة عن الجهاز باستخدام الشل", + "execute-following-command": "نفذ الأمر التالي", + "install-curl-windows": "ابتداءً من Windows 10 b17063، يتوفر cURL بشكل افتراضي", + "install-curl-macos": "ابتداءً من Mac OS X 10.2 6C115 (Jaguar)، يتوفر cURL بشكل افتراضي", + "install-mqtt-windows": "استخدم التعليمات لتنزيل، تثبيت، إعداد وتشغيل mosquitto_pub", + "install-coap-client": "استخدم التعليمات لتنزيل، تثبيت، إعداد وتشغيل coap-client", + "install-necessary-client-tools": "قم بتثبيت أدوات العميل الضرورية", + "mqtts-x509-command": "استخدم التوثيق التالي لربط الجهاز عبر MQTT بتفويض X509", + "coaps-x509-command": "استخدم التوثيق التالي لربط الجهاز عبر CoAP فوق DTLS بتفويض X509", + "snmp-command": "استخدم التوثيق التالي لربط الجهاز من خلال SNMP.", + "sparkplug-command": "استخدم التوثيق التالي لربط الجهاز من خلال MQTT Sparkplug.", + "lwm2m-command": "استخدم التوثيق التالي لربط الجهاز من خلال LWM2M." + } + }, + "asset-profile": { + "asset-profile": "ملف تعريف الأصل", + "asset-profiles": "ملفات تعريف الأصول", + "all-asset-profiles": "الكل", + "add": "إضافة ملف تعريف أصل", + "edit": "تعديل ملف تعريف الأصل", + "asset-profile-details": "تفاصيل ملف تعريف الأصل", + "no-asset-profiles-text": "لم يتم العثور على ملفات تعريف أصول", + "search": "بحث في ملفات تعريف الأصول", + "selected-asset-profiles": "{ count, plural, =1 {ملف تعريف أصل واحد} other {# من ملفات تعريف الأصول} } محدد", + "no-asset-profiles-matching": "لم يتم العثور على ملف تعريف أصل يطابق '{{entity}}'.", + "asset-profile-required": "ملف تعريف الأصل مطلوب", + "idCopiedMessage": "تم نسخ معرف ملف تعريف الأصل إلى الحافظة", + "set-default": "جعل ملف تعريف الأصل افتراضي", + "delete": "حذف ملف تعريف الأصل", + "copyId": "نسخ معرف ملف تعريف الأصل", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "new-device-profile-name": "اسم ملف تعريف الأصل", + "new-device-profile-name-required": "اسم ملف تعريف الأصل مطلوب.", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "image": "صورة ملف تعريف الأصل", + "description": "الوصف", + "default": "افتراضي", + "default-rule-chain": "سلسلة القواعد الافتراضية", + "default-edge-rule-chain": "سلسلة القواعد الافتراضية للحافة", + "default-edge-rule-chain-hint": "تستخدم على الحافة كسلسلة قواعد لمعالجة البيانات الواردة للأصول من هذا الملف تعريف الأصل", + "mobile-dashboard": "لوحة التحكم المحمولة", + "mobile-dashboard-hint": "تستخدم من قبل التطبيق المحمول كلوحة تفاصيل الأصل", + "select-queue-hint": "اختر من قائمة منسدلة.", + "delete-asset-profile-title": "هل أنت متأكد أنك تريد حذف ملف تعريف الأصل '{{assetProfileName}}'؟", + "delete-asset-profile-text": "كن حذرًا، بعد التأكيد سيتم حذف ملف تعريف الأصل وجميع البيانات ذات الصلة ولن يمكن استردادها.", + "delete-asset-profiles-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {ملف تعريف أصل واحد} other {# من ملفات تعريف الأصول} }؟", + "delete-asset-profiles-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع ملفات تعريف الأصول المحددة وجميع البيانات ذات الصلة ولن يمكن استردادها.", + "set-default-asset-profile-title": "هل أنت متأكد أنك تريد جعل ملف تعريف الأصل '{{assetProfileName}}' افتراضي؟", + "set-default-asset-profile-text": "بعد التأكيد سيتم تمييز ملف تعريف الأصل كافتراضي وسيتم استخدامه للأصول الجديدة بدون تحديد ملف تعريف.", + "no-asset-profiles-found": "لم يتم العثور على ملفات تعريف أصول.", + "create-new-asset-profile": "إنشاء جديد!", + "create-asset-profile": "إنشاء ملف تعريف أصل جديد", + "import": "استيراد ملف تعريف الأصل", + "export": "تصدير ملف تعريف الأصل", + "export-failed-error": "غير قادر على تصدير ملف تعريف الأصل: {{error}}", + "asset-profile-file": "ملف ملف تعريف الأصل", + "invalid-asset-profile-file-error": "غير قادر على استيراد ملف تعريف الأصل: بنية بيانات ملف تعريف الأصل غير صالحة." + }, + "device-profile": { + "device-profile": "ملف تعريف الجهاز", + "device-profiles": "ملفات تعريف الأجهزة", + "all-device-profiles": "الكل", + "add": "إضافة ملف تعريف جهاز", + "edit": "تعديل ملف تعريف الجهاز", + "device-profile-details": "تفاصيل ملف تعريف الجهاز", + "no-device-profiles-text": "لم يتم العثور على ملفات تعريف للأجهزة", + "search": "بحث في ملفات تعريف الأجهزة", + "selected-device-profiles": "{ count, plural, =1 {ملف تعريف جهاز واحد} other {# من ملفات تعريف الأجهزة} } محددة", + "no-device-profiles-matching": "لم يتم العثور على ملف تعريف جهاز يطابق '{{entity}}'.", + "device-profile-required": "ملف تعريف الجهاز مطلوب", + "idCopiedMessage": "تم نسخ معرف ملف تعريف الجهاز إلى الحافظة", + "set-default": "جعل ملف تعريف الجهاز افتراضي", + "delete": "حذف ملف تعريف الجهاز", + "copyId": "نسخ معرف ملف تعريف الجهاز", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "type": "نوع الملف التعريفي", + "type-required": "نوع الملف التعريفي مطلوب.", + "type-default": "افتراضي", + "image": "صورة ملف تعريف الجهاز", + "transport-type": "نوع النقل", + "transport-type-required": "نوع النقل مطلوب.", + "transport-type-default": "افتراضي", + "transport-type-default-hint": "يدعم نقل MQTT و HTTP و CoAP الأساسي", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "يمكن تفعيل إعدادات نقل MQTT المتقدمة", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "يمكن تفعيل إعدادات نقل CoAP المتقدمة", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "نوع نقل LWM2M", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "تحديد تكوين نقل SNMP", + "transport-type-http": "HTTP", + "description": "الوصف", + "default": "افتراضي", + "profile-configuration": "تكوين الملف التعريفي", + "transport-configuration": "تكوين النقل", + "default-rule-chain": "سلسلة القواعد الافتراضية", + "default-edge-rule-chain": "سلسلة قواعد الحافة الافتراضية", + "default-edge-rule-chain-hint": "تستخدم على الحافة كسلسلة قواعد لمعالجة البيانات الواردة للأجهزة من هذا الملف التعريفي", + "mobile-dashboard": "لوحة التحكم المحمولة", + "mobile-dashboard-hint": "تستخدم من قبل التطبيق المحمول كلوحة تفاصيل الجهاز", + "select-queue-hint": "اختر من قائمة منسدلة.", + "delete-device-profile-title": "هل أنت متأكد أنك تريد حذف ملف تعريف الجهاز '{{deviceProfileName}}'؟", + "delete-device-profile-text": "كن حذرًا، بعد التأكيد سيتم حذف ملف تعريف الجهاز وجميع البيانات ذات الصلة بما في ذلك التحديثات OTA المرتبطة ولن يمكن استردادها.", + "delete-device-profiles-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {ملف تعريف جهاز واحد} other {# من ملفات تعريف الأجهزة} }؟", + "delete-device-profiles-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع ملفات تعريف الأجهزة المحددة وجميع البيانات ذات الصلة بما في ذلك التحديثات OTA المرتبطة ولن يمكن استردادها.", + "set-default-device-profile-title": "هل أنت متأكد أنك تريد جعل ملف تعريف الجهاز '{{deviceProfileName}}' افتراضي؟", + "set-default-device-profile-text": "بعد التأكيد سيتم تمييز ملف تعريف الجهاز كافتراضي وسيتم استخدامه للأجهزة الجديدة بدون تحديد ملف تعريف.", + "no-device-profiles-found": "لم يتم العثور على ملفات تعريف للأجهزة.", + "create-new-device-profile": "إنشاء جديد!", + "mqtt-device-topic-filters": "فلاتر موضوع جهاز MQTT", + "mqtt-device-topic-filters-unique": "يجب أن تكون فلاتر موضوع جهاز MQTT فريدة.", + "mqtt-device-topic-filters-spark-plug": "عقدة MQTT Sparkplug B Edge of Network (EoN).", + "mqtt-device-topic-filters-spark-plug-hint": "السماح بالاتصالات من عقد EoN مع بيانات Sparkplug B وتنسيق الموضوع.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "مقاييس SparkPlug لتخزينها كسمات.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "أسماء مقاييس SparkPlug التي سيتم تخزينها كسمات الجهاز. سيتم تخزين جميع المقاييس الأخرى كبيانات تليمترية للجهاز.", + "mqtt-device-payload-type": "حمولة جهاز MQTT", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "تمكين التوافق مع تنسيقات الحمولة الأخرى.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "عند التمكين، ستستخدم المنصة تنسيق حمولة Protobuf بشكل افتراضي. إذا فشل التحليل، ستحاول المنصة استخدام تنسيق حمولة JSON. مفيد للتوافق العكسي أثناء تحديثات البرنامج الثابت. على سبيل المثال، يستخدم الإصدار الأولي من البرنامج الثابت Json، بينما يستخدم الإصدار الجديد Protobuf. خلال عملية تحديث البرنامج الثابت لأسطول الأجهزة، يلزم دعم كل من Protobuf و JSON في نفس الوقت. يقدم وضع التوافق تدهورًا طفيفًا في الأداء، لذا يُنصح بتعطيل هذا الوضع بمجرد تحديث جميع الأجهزة.", + "mqtt-use-json-format-for-default-downlink-topics": "استخدم تنسيق Json لمواضيع الارتباط الهابط الافتراضية", + "mqtt-use-json-format-for-default-downlink-topics-hint": "عند التمكين، ستستخدم المنصة تنسيق حمولة Json لدفع السمات و RPC عبر المواضيع التالية: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. لا يؤثر هذا الإعداد على اشتراكات السمات و rpc التي تم إرسالها باستخدام مواضيع جديدة (v2): v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. حيث $request_id هو معرف الطلب الصحيح.", + "mqtt-send-ack-on-validation-exception": "إرسال PUBACK عند فشل التحقق من صحة رسالة PUBLISH", + "mqtt-send-ack-on-validation-exception-hint": "بشكل افتراضي، ستغلق المنصة جلسة MQTT عند فشل التحقق من الرسالة. عند التمكين، سترسل المنصة إقرارًا بالنشر بدلاً من إغلاق الجلسة.", + "snmp-add-mapping": "إضافة تعيين SNMP", + "snmp-mapping-not-configured": "لم يتم تكوين تعيين ل OID إلى timeseries/telemetry", + "snmp-timseries-or-attribute-name": "اسم timeseries/attribute للتعيين", + "snmp-timseries-or-attribute-type": "نوع timeseries/attribute للتعيين", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "نوع الحمولة مطلوب.", + "coap-device-type": "نوع جهاز CoAP", + "coap-device-payload-type": "حمولة جهاز CoAP", + "coap-device-type-required": "نوع جهاز CoAP مطلوب.", + "coap-device-type-default": "افتراضي", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "يدعم البدلات المستوى الواحد [+] والمستويات المتعددة [#].", + "telemetry-topic-filter": "فلتر موضوع التليمتري", + "telemetry-topic-filter-required": "فلتر موضوع التليمتري مطلوب.", + "attributes-topic-filter": "فلتر موضوع نشر السمات", + "attributes-subscribe-topic-filter": "فلتر موضوع اشتراك السمات", + "attributes-topic-filter-required": "فلتر موضوع نشر السمات مطلوب.", + "attributes-subscribe-topic-filter-required": "موضوع اشتراك السمات مطلوب", + "telemetry-proto-schema": "مخطط بروتو التليمتري", + "telemetry-proto-schema-required": "مخطط بروتو التليمتري مطلوب.", + "attributes-proto-schema": "مخطط بروتو السمات", + "attributes-proto-schema-required": "مخطط بروتو السمات مطلوب.", + "rpc-response-proto-schema": "مخطط بروتو استجابة RPC", + "rpc-response-proto-schema-required": "مخطط بروتو استجابة RPC مطلوب.", + "rpc-response-topic-filter": "فلتر موضوع استجابة RPC", + "rpc-response-topic-filter-required": "فلتر موضوع استجابة RPC مطلوب.", + "rpc-request-proto-schema": "مخطط بروتو طلب RPC", + "rpc-request-proto-schema-required": "مخطط بروتو طلب RPC مطلوب.", + "rpc-request-proto-schema-hint": "يجب أن يحتوي رسالة طلب RPC دائمًا على الحقول: string method = 1; int32 requestId = 2; و params = 3 من أي نوع بيانات.", + "not-valid-pattern-topic-filter": "فلتر موضوع النمط غير صالح", + "not-valid-single-character": "استخدام غير صالح لحرف البدل لمستوى واحد", + "not-valid-multi-character": "استخدام غير صالح لحرف البدل لمستويات متعددة", + "single-level-wildcards-hint": "[+] مناسب لأي مستوى فلتر موضوع. مثال: v1/devices/+/telemetry أو +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] يمكن أن يحل محل فلتر الموضوع نفسه ويجب أن يكون آخر رمز في الموضوع. مثال: # أو v1/devices/me/#.", + "alarm-rules": "قواعد الإنذار", + "alarm-rules-with-count": "قواعد الإنذار ({{count}})", + "no-alarm-rules": "لم يتم تكوين قواعد إنذار", + "add-alarm-rule": "إضافة قاعدة إنذار", + "edit-alarm-rule": "تعديل قاعدة إنذار", + "alarm-type": "نوع الإنذار", + "alarm-type-required": "نوع الإنذار مطلوب.", + "alarm-type-unique": "يجب أن يكون نوع الإنذار فريدًا ضمن قواعد إنذار ملف تعريف الجهاز.", + "alarm-type-max-length": "يجب أن يكون نوع الإنذار أقل من 256", + "create-alarm-pattern": "إنشاء إنذار {{alarmType}}", + "create-alarm-rules": "إنشاء قواعد إنذار", + "no-create-alarm-rules": "لم يتم تكوين شروط إنشاء", + "add-create-alarm-rule-prompt": "يرجى إضافة قاعدة إنشاء إنذار", + "clear-alarm-rule": "مسح قاعدة الإنذار", + "no-clear-alarm-rule": "لم يتم تكوين شرط مسح", + "add-create-alarm-rule": "إضافة شرط إنشاء", + "add-clear-alarm-rule": "إضافة شرط مسح", + "select-alarm-severity": "اختر شدة الإنذار", + "alarm-severity-required": "شدة الإنذار مطلوبة.", + "condition-duration": "مدة الشرط", + "condition-duration-value": "قيمة المدة", + "condition-duration-time-unit": "وحدة الزمن", + "condition-duration-value-range": "يجب أن تكون قيمة المدة في نطاق من 1 إلى 2147483647.", + "condition-duration-value-pattern": "يجب أن تكون قيمة المدة أعدادًا صحيحة.", + "condition-duration-value-required": "قيمة المدة مطلوبة.", + "condition-duration-time-unit-required": "وحدة الزمن مطلوبة.", + "advanced-settings": "الإعدادات المتقدمة", + "alarm-rule-additional-info": "معلومات إضافية", + "edit-alarm-rule-additional-info": "تعديل المعلومات الإضافية", + "alarm-rule-additional-info-placeholder": "يرجى تقديم تعليقاتك وتعديلاتك هنا لعرضها ضمن تفاصيل الإنذار تحت المعلومات الإضافية", + "alarm-rule-additional-info-hint": "تلميح: استخدم ${اسم المفتاح} لاستبدال قيم السمات أو مفاتيح البيانات التي يتم استخدامها في شرط قاعدة الإنذار.", + "alarm-rule-mobile-dashboard": "لوحة التحكم المحمولة", + "alarm-rule-mobile-dashboard-hint": "تستخدم من قبل التطبيق المحمول كلوحة تفاصيل الإنذار", + "alarm-rule-no-mobile-dashboard": "لم يتم تحديد لوحة تحكم", + "propagate-alarm": "نشر الإنذار إلى الكيانات ذات الصلة", + "alarm-rule-relation-types-list": "أنواع العلاقات لنشر الإنذار", + "alarm-rule-relation-types-list-hint": "إذا لم يتم تحديد أنواع علاقات النشر، سيتم نشر الإنذارات دون تصفية حسب نوع العلاقة.", + "propagate-alarm-to-owner": "نشر الإنذار إلى مالك الكيان (العميل أو المستأجر)", + "propagate-alarm-to-owner-hierarchy": "نشر الإنذار إلى تسلسل مالكي الكيان", + "propagate-alarm-to-tenant": "نشر الإنذار إلى المستأجر", + "alarm-rule-condition": "شرط قاعدة الإنذار", + "enter-alarm-rule-condition-prompt": "يرجى إضافة شرط قاعدة الإنذار", + "edit-alarm-rule-condition": "تعديل شرط قاعدة الإنذار", + "device-provisioning": "توفير الجهاز", + "provision-strategy": "استراتيجية التوفير", + "provision-strategy-required": "استراتيجية التوفير مطلوبة.", + "provision-strategy-disabled": "معطل", + "provision-strategy-created-new": "السماح بإنشاء أجهزة جديدة", + "provision-strategy-check-pre-provisioned": "التحقق من الأجهزة المعدة مسبقًا", + "provision-device-key": "مفتاح توفير الجهاز", + "provision-device-key-required": "مفتاح توفير الجهاز مطلوب.", + "copy-provision-key": "نسخ مفتاح التوفير", + "provision-key-copied-message": "تم نسخ مفتاح التوفير إلى الحافظة", + "provision-device-secret": "سر توفير الجهاز", + "provision-device-secret-required": "سر توفير الجهاز مطلوب.", + "copy-provision-secret": "نسخ سر التوفير", + "provision-secret-copied-message": "تم نسخ سر التوفير إلى الحافظة", + "provision-strategy-x509": { + "certificate-chain": "سلسلة شهادات X509", + "certificate-chain-hint": "تُستخدم استراتيجية شهادات X.509 لتوفير الأجهزة عن طريق شهادات العميل في اتصال TLS ثنائي الاتجاه.", + "allow-create-new-devices": "إنشاء أجهزة جديدة", + "allow-create-new-devices-hint": "إذا تم تحديدها، سيتم إنشاء أجهزة جديدة وسيتم استخدام شهادة العميل كبيانات اعتماد الجهاز.", + "certificate-value": "الشهادة بتنسيق PEM", + "certificate-value-required": "الشهادة بتنسيق PEM مطلوبة", + "cn-regex-variable": "متغير التعبير العادي لـ CN", + "cn-regex-variable-required": "متغير التعبير العادي لـ CN مطلوب", + "cn-regex-variable-hint": "مطلوب لجلب اسم الجهاز من الاسم المشترك لشهادة X509 للجهاز." + }, + "condition": "الشرط", + "condition-type": "نوع الشرط", + "condition-type-simple": "بسيط", + "condition-type-duration": "المدة", + "condition-during": "خلال {{during}}", + "condition-during-dynamic": "خلال \"{{ attribute }}\" ({{during}})", + "condition-type-repeating": "متكرر", + "condition-type-required": "نوع الشرط مطلوب.", + "condition-repeating-value": "عدد الأحداث", + "condition-repeating-value-range": "يجب أن يكون عدد الأحداث ضمن نطاق من 1 إلى 2147483647.", + "condition-repeating-value-pattern": "يجب أن يكون عدد الأحداث أعدادًا صحيحة.", + "condition-repeating-value-required": "عدد الأحداث مطلوب.", + "condition-repeat-times": "يتكرر { count, plural, =1 {مرة واحدة} other {# مرات} }", + "condition-repeat-times-dynamic": "يتكرر \"{ attribute }\" ({ count, plural, =1 {مرة واحدة} other {# مرات} })", + "schedule-type": "نوع المجدول", + "schedule-type-required": "نوع المجدول مطلوب.", + "schedule": "الجدول الزمني", + "edit-schedule": "تعديل جدول الإنذار", + "schedule-any-time": "نشط طوال الوقت", + "schedule-specific-time": "نشط في وقت محدد", + "schedule-custom": "مخصص", + "schedule-day": { + "monday": "الاثنين", + "tuesday": "الثلاثاء", + "wednesday": "الأربعاء", + "thursday": "الخميس", + "friday": "الجمعة", + "saturday": "السبت", + "sunday": "الأحد" + }, + "schedule-days": "الأيام", + "schedule-time": "الوقت", + "schedule-time-from": "من", + "schedule-time-to": "إلى", + "schedule-days-of-week-required": "يجب اختيار يوم واحد على الأقل من أيام الأسبوع.", + "create-device-profile": "إنشاء ملف تعريف جهاز جديد", + "import": "استيراد ملف تعريف الجهاز", + "export": "تصدير ملف تعريف الجهاز", + "export-failed-error": "غير قادر على تصدير ملف تعريف الجهاز: {{error}}", + "device-profile-file": "ملف ملف تعريف الجهاز", + "invalid-device-profile-file-error": "غير قادر على استيراد ملف تعريف الجهاز: بنية بيانات ملف تعريف الجهاز غير صالحة.", + "power-saving-mode": "وضع توفير الطاقة", + "power-saving-mode-type": { + "default": "استخدم وضع توفير الطاقة لملف تعريف الجهاز", + "psm": "وضع توفير الطاقة (PSM)", + "drx": "استقبال متقطع (DRX)", + "edrx": "استقبال متقطع موسع (eDRX)" + }, + "edrx-cycle": "دورة eDRX", + "edrx-cycle-required": "دورة eDRX مطلوبة.", + "edrx-cycle-pattern": "يجب أن تكون دورة eDRX عددًا صحيحًا موجبًا.", + "edrx-cycle-min": "العدد الأدنى لدورة eDRX هو {{ min }} ثانية.", + "paging-transmission-window": "نافذة نقل البيجنج", + "paging-transmission-window-required": "نافذة نقل البيجنج مطلوبة.", + "paging-transmission-window-pattern": "يجب أن تكون نافذة نقل البيجنج عددًا صحيحًا موجبًا.", + "paging-transmission-window-min": "العدد الأدنى لنافذة نقل البيجنج هو {{ min }} ثانية.", + "psm-activity-timer": "مؤقت نشاط PSM", + "psm-activity-timer-required": "مؤقت نشاط PSM مطلوب.", + "psm-activity-timer-pattern": "يجب أن يكون مؤقت نشاط PSM عددًا صحيحًا موجبًا.", + "psm-activity-timer-min": "العدد الأدنى لمؤقت نشاط PSM هو {{ min }} ثانية.", + "lwm2m": { + "object-list": "قائمة الكائنات", + "object-list-empty": "لم يتم تحديد كائنات.", + "no-objects-found": "لم يتم العثور على كائنات.", + "no-objects-matching": "لم يتم العثور على كائنات تطابق '{{object}}'.", + "model-tab": "نموذج LWM2M", + "add-new-instances": "إضافة نماذج جديدة", + "instances-list": "قائمة النماذج", + "instances-list-required": "قائمة النماذج مطلوبة.", + "instance-id-pattern": "يجب أن يكون معرف النموذج عددًا صحيحًا موجبًا.", + "instance-id-max": "القيمة القصوى لمعرف النموذج هي {{max}}.", + "instance": "النموذج", + "resource-label": "اسم المورد #ID", + "observe-label": "مراقبة", + "attribute-label": "سمة", + "telemetry-label": "تليمتري", + "edit-observe-select": "لتعديل المراقبة اختر تليمتري أو سمة", + "edit-attributes-select": "لتعديل السمات اختر تليمتري أو سمة", + "no-attributes-set": "لم يتم تعيين سمات", + "key-name": "اسم المفتاح", + "key-name-required": "اسم المفتاح مطلوب", + "attribute-name": "اسم السمة", + "attribute-name-required": "اسم السمة مطلوب.", + "attribute-value": "قيمة السمة", + "attribute-value-required": "قيمة السمة مطلوبة.", + "attribute-value-pattern": "يجب أن تكون قيمة السمة عددًا صحيحًا موجبًا.", + "edit-attributes": "تعديل السمات: {{ name }}", + "view-attributes": "عرض السمات: {{ name }}", + "add-attribute": "إضافة سمة", + "edit-attribute": "تعديل السمة", + "view-attribute": "عرض السمة", + "remove-attribute": "إزالة السمة", + "delete-server-text": "كن حذرًا، بعد التأكيد ستصبح تكوينات الخادم غير قابلة للاسترداد.", + "delete-server-title": "هل أنت متأكد أنك تريد حذف الخادم؟", + "mode": "وضع تكوين الأمان", + "bootstrap-tab": "التمهيد", + "bootstrap-server-legend": "خادم التمهيد (ShortId...)", + "lwm2m-server-legend": "خادم LwM2M (ShortId...)", + "server": "الخادم", + "short-id": "معرف الخادم القصير", + "short-id-tooltip": "معرف الخادم القصير. يُستخدم كرابط لربط نموذج كائن الخادم.\nهذا المعرف يُميز بشكل فريد كل خادم LwM2M المُعد للعميل LwM2M.\nيجب ضبط المورد عندما يكون مورد الخادم التمهيدي بقيمة 'false'.\nلا يجب استخدام قيم المعرف ID:0 و ID:65535 لتحديد خادم LwM2M.", + "short-id-required": "معرف الخادم القصير مطلوب.", + "short-id-range": "يجب أن يكون معرف الخادم القصير ضمن نطاق من 1 إلى 65534.", + "short-id-pattern": "يجب أن يكون معرف الخادم القصير عددًا صحيحًا موجبًا.", + "lifetime": "مدة تسجيل العميل", + "lifetime-required": "مدة تسجيل العميل مطلوبة.", + "lifetime-pattern": "يجب أن تكون مدة تسجيل العميل عددًا صحيحًا موجبًا.", + "default-min-period": "الفترة الدنيا بين إشعارين (ثواني)", + "default-min-period-tooltip": "القيمة الافتراضية التي يجب أن يستخدمها العميل LwM2M للفترة الدنيا لمراقبة في غياب هذا العامل في مراقبة.", + "default-min-period-required": "الفترة الدنيا مطلوبة.", + "default-min-period-pattern": "يجب أن تكون الفترة الدنيا عددًا صحيحًا موجبًا.", + "notification-storing": "تخزين الإشعارات عند التعطيل أو عدم الاتصال", + "binding": "الربط", + "binding-type": { + "u": "U: العميل متاح عبر الربط UDP في أي وقت.", + "m": "M: العميل متاح عبر الربط MQTT في أي وقت.", + "h": "H: العميل متاح عبر الربط HTTP في أي وقت.", + "t": "T: العميل متاح عبر الربط TCP في أي وقت.", + "s": "S: العميل متاح عبر الربط SMS في أي وقت.", + "n": "N: يجب على العميل إرسال الرد على مثل هذا الطلب عبر الربط غير الـ IP (يدعم منذ LWM2M 1.1).", + "uq": "UQ: اتصال UDP في وضع الطابور (لا يدعم منذ LWM2M 1.1)", + "uqs": "UQS: كلا من اتصالات UDP وSMS نشطة؛ UDP في وضع الطابور، SMS في الوضع القياسي (لا يدعم منذ LWM2M 1.1)", + "tq": "TQ: اتصال TCP في وضع الطابور (لا يدعم منذ LWM2M 1.1)", + "tqs": "TQS: كلا من اتصالات TCP وSMS نشطة؛ TCP في وضع الطابور، SMS في الوضع القياسي (لا يدعم منذ LWM2M 1.1)", + "sq": "SQ: اتصال SMS في وضع الطابور (لا يدعم منذ LWM2M 1.1)" + }, + "binding-tooltip": "هذه هي القائمة في مورد \"الربط\" لكائن خادم LwM2M - /1/x/7.\nتشير إلى أوضاع الربط المدعومة في عميل LwM2M.\nيجب أن تكون هذه القيمة هي نفس القيمة في مورد \"أوضاع الربط والدعم\" في كائن الجهاز (/3/0/16).\nبينما يتم دعم العديد من وسائط النقل، يمكن استخدام ربط وسيط نقل واحد فقط خلال جلسة النقل بأكملها.\nعلى سبيل المثال، عندما يتم دعم كل من UDP و SMS، يمكن لعميل LwM2M وخادم LwM2M اختيار التواصل إما عبر UDP أو SMS خلال جلسة النقل بأكملها.", + "bootstrap-server": "خادم التمهيد", + "lwm2m-server": "خادم LwM2M", + "include-bootstrap-server": "تضمين تحديثات خادم التمهيد", + "bootstrap-update-title": "لديك بالفعل خادم تمهيد مُعد. هل أنت متأكد أنك تريد استبعاد التحديثات؟", + "bootstrap-update-text": "كن حذرًا، بعد التأكيد ستصبح بيانات تكوين خادم التمهيد غير قابلة للاسترداد.", + "server-host": "المضيف", + "server-host-required": "المضيف مطلوب.", + "server-port": "المنفذ", + "server-port-required": "المنفذ مطلوب.", + "server-port-pattern": "يجب أن يكون المنفذ عددًا صحيحًا موجبًا.", + "server-port-range": "يجب أن يكون المنفذ ضمن نطاق من 1 إلى 65535.", + "server-public-key": "مفتاح الخادم العام", + "server-public-key-required": "مفتاح الخادم العام مطلوب.", + "client-hold-off-time": "وقت الانتظار للعميل", + "client-hold-off-time-required": "وقت الانتظار للعميل مطلوب.", + "client-hold-off-time-pattern": "يجب أن يكون وقت الانتظار للعميل عددًا صحيحًا موجبًا.", + "client-hold-off-time-tooltip": "وقت الانتظار للعميل للاستخدام مع خادم التمهيد فقط", + "account-after-timeout": "الحساب بعد انتهاء الوقت", + "account-after-timeout-required": "الحساب بعد انتهاء الوقت مطلوب.", + "account-after-timeout-pattern": "يجب أن يكون الحساب بعد انتهاء الوقت عددًا صحيحًا موجبًا.", + "account-after-timeout-tooltip": "حساب خادم التمهيد بعد قيمة الوقت المحددة بواسطة هذا المورد.", + "server-type": "نوع الخادم", + "add-new-server-title": "إضافة تكوين خادم جديد", + "add-server-config": "إضافة تكوين خادم", + "add-lwm2m-server-config": "إضافة خادم LwM2M", + "no-config-servers": "لا توجد خوادم مكونة", + "others-tab": "إعدادات أخرى", + "client-strategy": "استراتيجية العميل عند الاتصال", + "client-strategy-label": "الاستراتيجية", + "client-strategy-only-observe": "طلب المراقبة فقط للعميل بعد الاتصال الأولي", + "client-strategy-read-all": "قراءة جميع الموارد وطلب المراقبة للعميل بعد التسجيل", + "fw-update": "تحديث البرنامج الثابت", + "fw-update-strategy": "استراتيجية تحديث البرنامج الثابت", + "fw-update-strategy-data": "دفع تحديث البرنامج الثابت كملف ثنائي باستخدام الكائن 19 والمورد 0 (البيانات)", + "fw-update-strategy-package": "دفع تحديث البرنامج الثابت كملف ثنائي باستخدام الكائن 5 والمورد 0 (الحزمة)", + "fw-update-strategy-package-uri": "إنشاء عنوان URL فريد لـ CoAP تلقائيًا لتنزيل الحزمة ودفع تحديث البرنامج الثابت ككائن 5 والمورد 1 (عنوان URL للحزمة)", + "sw-update": "تحديث البرمجيات", + "sw-update-strategy": "استراتيجية تحديث البرمجيات", + "sw-update-strategy-package": "دفع ملف ثنائي باستخدام الكائن 9 والمورد 2 (الحزمة)", + "sw-update-strategy-package-uri": "إنشاء عنوان URL فريد لـ CoAP تلقائيًا لتنزيل الحزمة ودفع تحديث البرمجيات باستخدام الكائن 9 والمورد 3 (عنوان URL للحزمة)", + "fw-update-resource": "مورد تحديث البرنامج الثابت CoAP", + "fw-update-resource-required": "مورد تحديث البرنامج الثابت CoAP مطلوب.", + "sw-update-resource": "مورد تحديث البرمجيات CoAP", + "sw-update-resource-required": "مورد تحديث البرمجيات CoAP مطلوب.", + "config-json-tab": "ملف تكوين Json للجهاز", + "attributes-name": { + "min-period": "الفترة الدنيا", + "max-period": "الفترة القصوى", + "greater-than": "أكبر من", + "less-than": "أقل من", + "step": "خطوة", + "min-evaluation-period": "الفترة الدنيا للتقييم", + "max-evaluation-period": "الفترة القصوى للتقييم" + }, + "composite-operations-support": "يدعم عمليات القراءة/الكتابة/المراقبة المركبة" + }, + "snmp": { + "add-communication-config": "إضافة تكوين الاتصال", + "add-mapping": "إضافة تعيين", + "authentication-passphrase": "عبارة مرور المصادقة", + "authentication-passphrase-required": "عبارة مرور المصادقة مطلوبة.", + "authentication-protocol": "بروتوكول المصادقة", + "authentication-protocol-required": "بروتوكول المصادقة مطلوب.", + "communication-configs": "تكوينات الاتصال", + "community": "سلسلة المجتمع", + "community-required": "سلسلة المجتمع مطلوبة.", + "context-name": "اسم السياق", + "data-key": "مفتاح البيانات", + "data-key-required": "مفتاح البيانات مطلوب.", + "data-type": "نوع البيانات", + "data-type-required": "نوع البيانات مطلوب.", + "engine-id": "معرف المحرك", + "host": "المضيف", + "host-required": "المضيف مطلوب.", + "oid": "OID", + "oid-pattern": "تنسيق OID غير صالح.", + "oid-required": "OID مطلوب.", + "please-add-communication-config": "يرجى إضافة تكوين الاتصال", + "please-add-mapping-config": "يرجى إضافة تكوين التعيين", + "port": "المنفذ", + "port-format": "تنسيق المنفذ غير صالح.", + "port-required": "المنفذ مطلوب.", + "privacy-passphrase": "عبارة مرور الخصوصية", + "privacy-passphrase-required": "عبارة مرور الخصوصية مطلوبة.", + "privacy-protocol": "بروتوكول الخصوصية", + "privacy-protocol-required": "بروتوكول الخصوصية مطلوب.", + "protocol-version": "إصدار البروتوكول", + "protocol-version-required": "إصدار البروتوكول مطلوب.", + "querying-frequency": "تردد الاستعلام، ملي ثانية", + "querying-frequency-invalid-format": "يجب أن يكون تردد الاستعلام عددًا صحيحًا موجبًا.", + "querying-frequency-required": "تردد الاستعلام مطلوب.", + "retries": "محاولات", + "retries-invalid-format": "يجب أن تكون المحاولات عددًا صحيحًا موجبًا.", + "retries-required": "المحاولات مطلوبة.", + "scope": "النطاق", + "scope-required": "النطاق مطلوب.", + "security-name": "اسم الأمان", + "security-name-required": "اسم الأمان مطلوب.", + "timeout-ms": "المهلة، ملي ثانية", + "timeout-ms-invalid-format": "يجب أن تكون المهلة عددًا صحيحًا موجبًا.", + "timeout-ms-required": "المهلة مطلوبة.", + "user-name": "اسم المستخدم", + "user-name-required": "اسم المستخدم مطلوب." + } + }, + "dialog": { + "close": "إغلاق الحوار", + "error-message-title": "عنوان رسالة الخطأ:", + "error-details-title": "تفاصيل الخطأ" + }, + "direction": { + "column": "عمود", + "row": "صف" + }, + "edge": { + "all": "الكل", + "all-edges": "جميع الحواف", + "groups": "المجموعات", + "shared": "مشترك", + "edge": "حافة", + "edge-instances": "نماذج الحافة", + "instances": "نماذج", + "edge-file": "ملف الحافة", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "label-max-length": "يجب أن يكون التصنيف أقل من 256", + "type-max-length": "يجب أن يكون النوع أقل من 256", + "management": "إدارة الحافة", + "no-edges-matching": "لم يتم العثور على حواف تطابق '{{entity}}'.", + "add": "إضافة حافة", + "no-edges-text": "لم يتم العثور على حواف", + "edge-details": "تفاصيل الحافة", + "add-edge-text": "إضافة حافة جديدة", + "delete": "حذف الحافة", + "delete-edge-title": "هل أنت متأكد أنك تريد حذف الحافة '{{edgeName}}'؟", + "delete-edge-text": "كن حذرًا، بعد التأكيد ستصبح الحافة وجميع البيانات المرتبطة بها غير قابلة للاسترداد.", + "delete-edges-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {حافة واحدة} other {# حواف} }؟", + "delete-edges-text": "كن حذرًا، بعد التأكيد ستتم إزالة جميع الحواف المحددة وجميع البيانات المرتبطة بها.", + "name": "الاسم", + "name-starts-with": "اسم الحافة يبدأ ب", + "name-required": "الاسم مطلوب.", + "edge-license-key": "مفتاح ترخيص الحافة", + "edge-license-key-required": "مفتاح ترخيص الحافة مطلوب.", + "edge-license-key-max-length": "يجب أن يكون مفتاح ترخيص الحافة أقل من 31", + "edge-license-key-hint": "ملاحظة مهمة: يتم توفير مفتاح ترخيص الحافة الافتراضي لأغراض التقييم فقط.
هذا المفتاح نشط فقط لمدة 30 يومًا بعد التنشيط. ميزة التخصيص معطلة.
للحصول على مفتاح الترخيص الخاص بك، يرجى التوجه إلى صفحة التسعير واختيار أفضل خيار ترخيص لحالتك.", + "cloud-endpoint": "نقطة نهاية السحاب", + "cloud-endpoint-required": "نقطة نهاية السحاب مطلوبة.", + "cloud-endpoint-max-length": "يجب أن تكون نقطة نهاية السحاب أقل من 256", + "cloud-endpoint-hint": "تتطلب الحافة الوصول إلى HTTP(s) إلى السحاب (ThingsBoard PE) للتحقق من مفتاح الترخيص. يرجى تحديد عنوان URL للسحاب الذي يمكن للحافة الاتصال به.", + "description": "الوصف", + "details": "التفاصيل", + "events": "الأحداث", + "copy-id": "نسخ معرف الحافة", + "id-copied-message": "تم نسخ معرف الحافة إلى الحافظة", + "sync": "مزامنة الحافة", + "edge-required": "الحافة مطلوبة", + "edge-type": "نوع الحافة", + "edge-type-required": "نوع الحافة مطلوب.", + "event-action": "إجراء الحدث", + "entity-id": "معرف الكيان", + "select-edge-type": "اختر نوع الحافة", + "assign-to-customer": "تعيين للعميل", + "assign-to-customer-text": "يرجى اختيار العميل لتعيين الحواف", + "assign-edge-to-customer": "تعيين الحواف للعميل", + "assign-edge-to-customer-text": "يرجى اختيار الحواف لتعيينها للعميل", + "assignedToCustomer": "معين للعميل", + "edge-public": "الحافة عامة", + "assigned-to-customer": "معين إلى: {{customerTitle}}", + "unassign-from-customer": "إلغاء التعيين من العميل", + "unassign-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين الحافة '{{edgeName}}'؟", + "unassign-edge-text": "بعد التأكيد ستصبح الحافة غير معينة ولن تكون متاحة للعميل.", + "unassign-edges-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {حافة واحدة} other {# حواف} }؟", + "unassign-edges-text": "بعد التأكيد سيتم إلغاء تعيين جميع الحواف المحددة ولن تكون متاحة للعميل.", + "make-public": "جعل الحافة عامة", + "make-public-edge-title": "هل أنت متأكد أنك تريد جعل الحافة '{{edgeName}}' عامة؟", + "make-public-edge-text": "بعد التأكيد ستصبح الحافة وجميع بياناتها عامة ومتاحة للآخرين.", + "make-private": "جعل الحافة خاصة", + "public": "عام", + "make-private-edge-title": "هل أنت متأكد أنك تريد جعل الحافة '{{edgeName}}' خاصة؟", + "make-private-edge-text": "بعد التأكيد ستصبح الحافة وجميع بياناتها خاصة ولن تكون متاحة للآخرين.", + "import": "استيراد الحافة", + "install-connect-instructions": "تعليمات التثبيت والاتصال", + "install-connect-instructions-edge-created": "تم إنشاء الحافة! تحقق من تعليمات التثبيت والاتصال", + "loading-edge-instructions": "جاري تحميل تعليمات الحافة...", + "label": "التصنيف", + "load-entity-error": "فشل في تحميل البيانات. تم حذف الكيان.", + "assign-new-edge": "تعيين حافة جديدة", + "unassign-from-edge": "إلغاء التعيين من الحافة", + "edge-key": "مفتاح الحافة", + "copy-edge-key": "نسخ مفتاح الحافة", + "edge-key-copied-message": "تم نسخ مفتاح الحافة إلى الحافظة", + "edge-secret": "سر الحافة", + "copy-edge-secret": "نسخ سر الحافة", + "edge-secret-copied-message": "تم نسخ سر الحافة إلى الحافظة", + "manage-assets": "إدارة الأصول", + "manage-devices": "إدارة الأجهزة", + "manage-entity-views": "إدارة عرض الكيانات", + "manage-dashboards": "إدارة لوحات المعلومات", + "manage-rulechains": "إدارة سلاسل القواعد", + "assets": "أصول الحافة", + "devices": "أجهزة الحافة", + "entity-views": "عرض كيانات الحافة", + "dashboard": "لوحة معلومات الحافة", + "dashboards": "لوحات معلومات الحافة", + "rulechain-templates": "قوالب سلسلة القواعد", + "edge-rulechain-templates": "قوالب سلسلة القواعد للحافة", + "converter-templates": "قوالب المحول", + "edge-converter-templates": "قوالب المحول للحافة", + "integration-templates": "قوالب التكامل", + "edge-integration-templates": "قوالب التكامل للحافة", + "rulechains": "سلاسل القواعد للحافة", + "integrations": "التكاملات", + "search": "بحث في الحواف", + "selected-edges": "{ count, plural, =1 {حافة واحدة} other {# حواف} } محددة", + "any-edge": "أي حافة", + "no-edge-types-matching": "لم يتم العثور على أنواع الحواف التي تطابق '{{entitySubtype}}'.", + "edge-type-list-empty": "لم يتم تحديد أنواع الحواف.", + "edge-types": "أنواع الحواف", + "enter-edge-type": "أدخل نوع الحافة", + "deployed": "تم النشر", + "pending": "قيد الانتظار", + "downlinks": "روابط تنزيل", + "no-downlinks-prompt": "لم يتم العثور على روابط تنزيل", + "sync-process-started-successfully": "تم بدء عملية المزامنة بنجاح!", + "missing-related-rule-chains-title": "الحافة تفتقد سلاسل قواعد ذات صلة", + "missing-related-rule-chains-text": "سلاسل القواعد المعينة للحافة تستخدم عقد قواعد ترسل الرسائل إلى سلاسل قواعد ليست معينة لهذه الحافة.

قائمة سلاسل القواعد المفقودة:
{{missingRuleChains}}", + "widget-datasource-error": "هذه الأداة تدعم فقط مصدر بيانات كيان EDGE", + "assign-to-edge": "تعيين للحافة", + "assign-to-edge-title": "تعيين مجموعات الكيانات للحافة", + "manage-edge-user-groups": "إدارة مجموعات المستخدمين للحافة", + "manage-edge-asset-groups": "إدارة مجموعات الأصول للحافة", + "manage-edge-device-groups": "إدارة مجموعات الأجهزة للحافة", + "manage-edge-entity-view-groups": "إدارة مجموعات عرض الكيانات للحافة", + "manage-edge-dashboard-groups": "إدارة مجموعات لوحات المعلومات للحافة", + "manage-edge-rule-chains": "إدارة سلاسل القواعد للحافة", + "manage-edge-integrations": "إدارة التكاملات للحافة", + "manage-edge-scheduler-events": "إدارة أحداث المجدول للحافة", + "select-group-to-add": "حدد المجموعة المستهدفة لإضافة الحواف المحددة", + "select-group-to-move": "حدد المجموعة المستهدفة لنقل الحواف المحددة", + "remove-edges-from-group": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {حافة واحدة} other {# حواف} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة الحواف", + "list-of-groups": "{ count, plural, =1 {مجموعة حافة واحدة} other {قائمة # مجموعات الحواف} }", + "group-name-starts-with": "مجموعات الحواف التي تبدأ أسماؤها بـ '{{prefix}}'", + "unassign-entity-group-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين مجموعة الكيان '{{ entityGroupName }}'؟", + "unassign-entity-group-from-edge-text": "بعد التأكيد ستصبح مجموعة الكيان غير معينة ولن تكون متاحة للحافة.", + "unassign-entity-group-from-edge": "إلغاء تعيين مجموعة الكيان من الحافة", + "unassign-entity-groups-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين {{count}} مجموعات كيان؟", + "unassign-entity-groups-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين مجموعات الكيان ولن تكون متاحة للحافة.", + "unassign-entity-groups-from-edge": "إلغاء تعيين مجموعات الكيان من الحافة", + "unassign-scheduler-events-from-edge": "إلغاء تعيين أحداث المجدول من الحافة", + "unassign-scheduler-event-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين حدث المجدول '{{schedulerEventName}}'؟", + "unassign-scheduler-event-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين حدث المجدول ولن يكون متاحًا للحافة.", + "unassign-scheduler-events-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {حدث مجدول واحد} other {# أحداث مجدولة} }؟", + "unassign-scheduler-events-from-edge-text": "بعد التأكيد سيتم إلغاء تعيين جميع الأحداث المجدولة المحددة ولن تكون متاحة للحافة.", + "manage-user-groups": "إدارة مجموعات المستخدمين", + "manage-asset-groups": "إدارة مجموعات الأصول", + "manage-device-groups": "إدارة مجموعات الأجهزة", + "manage-dashboard-groups": "إدارة مجموعات لوحات المعلومات", + "manage-entity-view-groups": "إدارة مجموعات عرض الكيانات", + "manage-scheduler-events": "إدارة أحداث المجدول", + "manage-integrations": "إدارة التكاملات", + "assign-scheduler-event-to-edge-title": "تعيين أحداث المجدول للحافة", + "assign-scheduler-event-to-edge-text": "يرجى اختيار أحداث المجدول لتعيينها للحافة", + "assign-entity-groups-to-edge-text": "يرجى اختيار مجموعات الكيان لتعيينها للحافة", + "assign-integration-to-edge-title": "تعيين التكاملات للحافة", + "assign-integration-to-edge-text": "يرجى اختيار التكاملات لتعيينها للحافة", + "missing-attributes-title": "الحافة تفتقد لبعض السمات", + "missing-attributes-text": "التكاملات المعينة للحافة لديها عناصر نائبة للسمات غير متوفرة على هذه الحافة.

قائمة السمات المفقودة على الحافة:
{{missingEdgeAttributes}}", + "missing-all-related-attributes-title": "الحواف لديها سمات مفقودة", + "missing-all-related-attributes-text": "الحواف المرتبطة بالتكامل لا تملك السمات الموجودة داخل تكوين التكامل كعناصر نائبة للسمات.

قائمة السمات المفقودة على الحافة:
{{missingEdgeAttributes}}", + "quick-overview-widget-header": "نظرة سريعة على {{edgeName}}", + "upgrade-instructions": "تعليمات الترقية" + }, + "edge-event": { + "type-dashboard": "لوحة المعلومات", + "type-asset": "الأصل", + "type-device": "الجهاز", + "type-device-profile": "ملف تعريف الجهاز", + "type-asset-profile": "ملف تعريف الأصل", + "type-entity-view": "عرض الكيان", + "type-alarm": "إنذار", + "type-entity-group": "مجموعة الكيان", + "type-rule-chain": "سلسلة القواعد", + "type-rule-chain-metadata": "بيانات تعريف سلسلة القواعد", + "type-edge": "الحافة", + "type-user": "المستخدم", + "type-tenant": "المستأجر", + "type-tenant-profile": "ملف تعريف المستأجر", + "type-customer": "العميل", + "type-relation": "العلاقة", + "type-widgets-bundle": "حزمة الأدوات", + "type-widgets-type": "نوع الأدوات", + "type-admin-settings": "إعدادات المسؤول", + "type-ota-package": "حزمة OTA", + "type-queue": "الطابور", + "type-scheduler-event": "حدث المجدول", + "type-white-labeling": "التسمية البيضاء", + "type-login-white-labeling": "تسجيل دخول التسمية البيضاء", + "type-custom-translation": "الترجمة المخصصة", + "type-role": "الدور", + "type-group-permission": "إذن المجموعة", + "type-integration": "التكامل", + "type-converter": "المحول", + "action-type-added": "تمت الإضافة", + "action-type-deleted": "تم الحذف", + "action-type-updated": "تم التحديث", + "action-type-post-attributes": "إرسال السمات", + "action-type-attributes-updated": "تم تحديث السمات", + "action-type-attributes-deleted": "تم حذف السمات", + "action-type-timeseries-updated": "تم تحديث السلاسل الزمنية", + "action-type-credentials-updated": "تم تحديث البيانات الاعتمادية", + "action-type-assigned-to-customer": "تم التعيين للعميل", + "action-type-unassigned-from-customer": "تم إلغاء التعيين من العميل", + "action-type-relation-add-or-update": "إضافة أو تحديث العلاقة", + "action-type-relation-deleted": "تم حذف العلاقة", + "action-type-rpc-call": "RPC استدعاء", + "action-type-alarm-ack": "تأكيد الإنذار", + "action-type-alarm-clear": "مسح الإنذار", + "action-type-alarm-assigned": "تم تعيين الإنذار", + "action-type-alarm-unassigned": "تم إلغاء تعيين الإنذار", + "action-type-assigned-to-edge": "تم التعيين إلى الحافة", + "action-type-unassigned-from-edge": "تم إلغاء التعيين من الحافة", + "action-type-credentials-request": "طلب بيانات اعتماد", + "action-type-entity-merge-request": "طلب دمج الكيان", + "action-type-added-to-entity-group": "تمت الإضافة إلى مجموعة الكيان", + "action-type-removed-from-entity-group": "تم الإزالة من مجموعة الكيان", + "action-type-change-owner": "تغيير المالك" + }, + "error": { + "unable-to-connect": "غير قادر على الاتصال بالخادم! يرجى التحقق من اتصالك بالإنترنت.", + "unhandled-error-code": "رمز خطأ غير معالج: {{errorCode}}", + "unknown-error": "خطأ غير معروف" + }, + "entity": { + "entity": "كيان", + "entities": "الكيانات", + "entities-count": "عدد الكيانات", + "alarms-count": "عدد التنبيهات", + "aliases": "أسماء مستعارة للكيان", + "aliases-short": "الأسماء المستعارة", + "entity-alias": "اسم مستعار للكيان", + "unable-delete-entity-alias-title": "غير قادر على حذف الاسم المستعار للكيان", + "unable-delete-entity-alias-text": "لا يمكن حذف الاسم المستعار '{{entityAlias}}' لأنه مستخدم بواسطة الأدوات التالية:
{{widgetsList}}", + "duplicate-alias-error": "تم العثور على اسم مستعار مكرر '{{alias}}'.
يجب أن تكون الأسماء المستعارة للكيانات فريدة ضمن لوحة المعلومات.", + "missing-entity-filter-error": "الفلتر مفقود للكنية '{{alias}}'.", + "configure-alias": "تكوين الاسم المستعار '{{alias}}'", + "alias": "الاسم المستعار", + "alias-required": "الاسم المستعار للكيان مطلوب.", + "remove-alias": "إزالة الاسم المستعار للكيان", + "add-alias": "إضافة اسم مستعار للكيان", + "entity-list": "قائمة الكيانات", + "entity-type": "نوع الكيان", + "entity-types": "أنواع الكيانات", + "entity-type-list": "قائمة أنواع الكيان", + "any-entity": "أي كيان", + "add-entity-type": "إضافة نوع كيان", + "enter-entity-type": "إدخال نوع الكيان", + "no-entities-matching": "لا توجد كيانات تطابق '{{entity}}'", + "no-entity-types-matching": "لا توجد أنواع كيانات تطابق '{{entityType}}'", + "name-starts-with": "تعبير عن الاسم", + "help-text": "استخدم '%' حسب الحاجة: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'.", + "use-entity-name-filter": "استخدم الفلتر", + "entity-list-empty": "لم يتم اختيار كيانات.", + "entity-type-list-required": "يجب اختيار نوع كيان واحد على الأقل.", + "entity-name-filter-required": "فلتر اسم الكيان مطلوب.", + "entity-name-filter-no-entity-matched": "لم يتم العثور على كيانات تبدأ بـ '{{entity}}'.", + "all-subtypes": "الكل", + "select-entities": "اختر الكيانات", + "no-aliases-found": "لم يتم العثور على أسماء مستعارة.", + "no-alias-matching": "لم يتم العثور على '{{alias}}'.", + "create-new-alias": "إنشاء جديد!", + "create-new": "إنشاء جديد", + "key": "المفتاح", + "key-name": "اسم المفتاح", + "no-keys-found": "لم يتم العثور على مفاتيح.", + "no-key-matching": "لم يتم العثور على '{{key}}'.", + "create-new-key": "إنشاء جديد!", + "type": "النوع", + "type-required": "نوع الكيان مطلوب.", + "type-device": "جهاز", + "type-devices": "الأجهزة", + "list-of-devices": "{ count, plural, =1 {جهاز واحد} other {قائمة من # أجهزة} }", + "device-name-starts-with": "أجهزة تبدأ أسماؤها بـ '{{prefix}}'", + "type-device-profile": "ملف تعريف الجهاز", + "type-device-profiles": "ملفات تعريف الجهاز", + "clear-selected-profiles": "مسح الملفات المختارة", + "list-of-device-profiles": "{ count, plural, =1 {ملف تعريف جهاز واحد} other {قائمة من # ملفات تعريف الجهاز} }", + "device-profile-name-starts-with": "ملفات تعريف الجهاز التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-asset-profile": "ملف تعريف الأصل", + "type-asset-profiles": "ملفات تعريف الأصل", + "list-of-asset-profiles": "{ count, plural, =1 {ملف تعريف أصل واحد} other {قائمة من # ملفات تعريف الأصل} }", + "asset-profile-name-starts-with": "ملفات تعريف الأصل التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-asset": "الأصل", + "type-assets": "الأصول", + "list-of-assets": "{ count, plural, =1 {أصل واحد} other {قائمة من # الأصول} }", + "asset-name-starts-with": "الأصول التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-entity-view": "عرض الكيان", + "type-entity-views": "عروض الكيانات", + "list-of-entity-views": "{ count, plural, =1 {عرض كيان واحد} other {قائمة من # عروض الكيانات} }", + "entity-view-name-starts-with": "عروض الكيانات التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-rule": "القاعدة", + "type-rules": "القواعد", + "list-of-rules": "{ count, plural, =1 {قاعدة واحدة} other {قائمة من # القواعد} }", + "rule-name-starts-with": "القواعد التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-plugin": "المكون الإضافي", + "type-plugins": "المكونات الإضافية", + "list-of-plugins": "{ count, plural, =1 {مكون إضافي واحد} other {قائمة من # المكونات الإضافية} }", + "plugin-name-starts-with": "المكونات الإضافية التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-tenant": "المستأجر", + "type-tenants": "المستأجرون", + "list-of-tenants": "{ count, plural, =1 {مستأجر واحد} other {قائمة من # المستأجرون} }", + "tenant-name-starts-with": "المستأجرون الذين تبدأ أسماؤهم بـ '{{prefix}}'", + "type-tenant-profile": "ملف تعريف المستأجر", + "type-tenant-profiles": "ملفات تعريف المستأجر", + "list-of-tenant-profiles": "{ count, plural, =1 {ملف تعريف مستأجر واحد} other {قائمة من # ملفات تعريف المستأجر} }", + "tenant-profile-name-starts-with": "ملفات تعريف المستأجر التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-customer": "العميل", + "type-customers": "العملاء", + "list-of-customers": "{ count, plural, =1 {عميل واحد} other {قائمة من # العملاء} }", + "customer-name-starts-with": "العملاء الذين تبدأ أسماؤهم بـ '{{prefix}}'", + "type-user": "المستخدم", + "type-users": "المستخدمون", + "list-of-users": "{ count, plural, =1 {مستخدم واحد} other {قائمة من # المستخدمين} }", + "user-name-starts-with": "المستخدمون الذين تبدأ أسماؤهم بـ '{{prefix}}'", + "type-dashboard": "لوحة المعلومات", + "type-dashboards": "لوحات المعلومات", + "list-of-dashboards": "{ count, plural, =1 {لوحة معلومات واحدة} other {قائمة من # لوحات المعلومات} }", + "dashboard-name-starts-with": "لوحات المعلومات التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-alarm": "التنبيه", + "type-alarms": "التنبيهات", + "list-of-alarms": "{ count, plural, =1 {تنبيه واحد} other {قائمة من # التنبيهات} }", + "alarm-name-starts-with": "التنبيهات التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-rulechain": "سلسلة القواعد", + "type-rulechains": "سلاسل القواعد", + "list-of-rulechains": "{ count, plural, =1 {سلسلة قواعد واحدة} other {قائمة من # سلاسل القواعد} }", + "rulechain-name-starts-with": "سلاسل القواعد التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-scheduler-event": "حدث المجدول", + "type-scheduler-events": "أحداث المجدول", + "list-of-scheduler-events": "{ count, plural, =1 {حدث مجدول واحد} other {قائمة من # أحداث المجدول} }", + "scheduler-event-name-starts-with": "أحداث المجدول التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-blob-entity": "كيان البلوب", + "type-blob-entities": "كيانات البلوب", + "list-of-blob-entities": "{ count, plural, =1 {كيان بلوب واحد} other {قائمة من # كيانات البلوب} }", + "blob-entity-name-starts-with": "كيانات البلوب التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-rulenode": "عقدة القاعدة", + "type-rulenodes": "عقد القواعد", + "list-of-rulenodes": "{ count, plural, =1 {عقدة قاعدة واحدة} other {قائمة من # عقد القواعد} }", + "rulenode-name-starts-with": "عقد القواعد التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-current-customer": "العميل الحالي", + "type-current-tenant": "المستأجر الحالي", + "type-current-user": "المستخدم الحالي", + "type-current-user-owner": "مالك المستخدم الحالي", + "type-widgets-bundle": "حزمة الأدوات", + "type-widgets-bundles": "حزم الأدوات", + "list-of-widgets-bundles": "{ count, plural, =1 {حزمة أدوات واحدة} other {قائمة من # حزم الأدوات} }", + "type-widget": "الأداة", + "type-widgets": "الأدوات", + "list-of-widgets": "{ count, plural, =1 {أداة واحدة} other {قائمة من # الأدوات} }", + "search": "بحث في الكيانات", + "selected-entities": "{ count, plural, =1 {1 كيان} other {# كيانات} } مختارة", + "entity-name": "اسم الكيان", + "entity-label": "تسمية الكيان", + "details": "تفاصيل الكيان", + "no-entities-prompt": "لم يتم العثور على كيانات", + "no-data": "لا توجد بيانات لعرضها", + "columns-to-display": "الأعمدة للعرض", + "type-api-usage-state": "حالة استخدام API", + "type-entity-group": "مجموعة الكيان", + "type-converter": "محول البيانات", + "type-converters": "محولات البيانات", + "list-of-converters": "{ count, plural, =1 {محول بيانات واحد} other {قائمة من # محولات البيانات} }", + "converter-name-starts-with": "محولات البيانات التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-integration": "التكامل", + "type-integrations": "التكاملات", + "list-of-integrations": "{ count, plural, =1 {تكامل واحد} other {قائمة من # التكاملات} }", + "integration-name-starts-with": "التكاملات التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-role": "الدور", + "type-roles": "الأدوار", + "list-of-roles": "{ count, plural, =1 {دور واحد} other {قائمة من # الأدوار} }", + "role-name-starts-with": "الأدوار التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-group-permission": "إذن المجموعة", + "type-edge": "الحافة", + "type-edges": "الحواف", + "list-of-edges": "{ count, plural, =1 {حافة واحدة} other {قائمة من # الحواف} }", + "edge-name-starts-with": "الحواف التي تبدأ أسماؤها بـ '{{prefix}}'", + "type-tb-resource": "المورد", + "type-tb-resources": "الموارد", + "list-of-tb-resources": "{ count, plural, =1 {مورد واحد} other {قائمة من # الموارد} }", + "type-ota-package": "حزمة OTA", + "type-rpc": "RPC", + "type-queue": "الطابور", + "type-notification": "الإشعار", + "type-notification-rule": "قاعدة الإشعار", + "type-notification-rules": "قواعد الإشعار", + "list-of-notification-rules": "{ count, plural, =1 {قاعدة إشعار واحدة} other {قائمة من # قواعد الإشعار} }", + "type-notification-target": "مستلم الإشعار", + "type-notification-targets": "مستلمو الإشعارات", + "list-of-notification-targets": "{ count, plural, =1 {مستلم إشعار واحد} other {قائمة من # مستلمي الإشعارات} }", + "type-notification-request": "طلب الإشعار", + "type-notification-template": "قالب الإشعار", + "type-notification-templates": "قوالب الإشعار", + "list-of-notification-templates": "{ count, plural, =1 {قالب إشعار واحد} other {قائمة من # قوالب الإشعار} }", + "customer-name": "اسم العميل", + "sub-customer-name": "اسم العميل الفرعي", + "parent-customer-name": "اسم العميل الأصلي", + "parent-sub-customer-name": "اسم العميل الفرعي الأصلي", + "include-customer-entities": "تضمين كيانات العميل", + "include-sub-customer-entities": "تضمين كيانات العميل الفرعي", + "groups": "المجموعات" + }, + "entity-group": { + "entity-group": "مجموعة الكيان", + "details": "التفاصيل", + "columns": "الأعمدة", + "add-column": "إضافة عمود", + "column-value": "القيمة", + "column-value-required": "قيمة العمود مطلوبة.", + "column-title": "العنوان", + "default-sort-order": "ترتيب الفرز الافتراضي", + "default-sort-order-required": "ترتيب الفرز الافتراضي مطلوب.", + "hide-in-mobile-view": "مخفي في عرض الجوال", + "use-cell-style-function": "استخدم دالة أنماط الخلية", + "use-cell-content-function": "استخدم دالة محتوى الخلية", + "edit-column": "تحرير العمود", + "column-details": "تفاصيل العمود", + "actions": "الإجراءات", + "settings": "الإعدادات", + "search": "بحث في مجموعات الكيان", + "delete": "حذف مجموعة الكيان", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "description": "الوصف", + "add": "إضافة مجموعة كيان", + "open-entity-group": "فتح مجموعة الكيان", + "add-entity-group-text": "إضافة مجموعة كيان جديدة", + "no-entity-groups-text": "لم يتم العثور على مجموعات كيان", + "entity-group-details": "تفاصيل مجموعة الكيان", + "selected-entity-groups": "{ count, plural, =1 {1 مجموعة كيان} other {# مجموعات كيان} } محددة", + "delete-entity-groups": "حذف مجموعات الكيان", + "delete-entity-group-title": "هل أنت متأكد أنك تريد حذف مجموعة الكيان '{{entityGroupName}}'؟", + "delete-entity-group-text": "كن حذرًا، بعد التأكيد سيتم حذف مجموعة الكيان وجميع البيانات المتعلقة بها بشكل لا رجعة فيه.", + "delete-entity-groups-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 مجموعة كيان} other {# مجموعات كيان} }؟", + "delete-entity-groups-action-title": "حذف { count, plural, =1 {1 مجموعة كيان} other {# مجموعات كيان} }", + "delete-entity-groups-text": "كن حذرًا، بعد التأكيد سيتم حذف جميع مجموعات الكيان المحددة وجميع البيانات المتعلقة بها بشكل لا رجعة فيه.", + "device-groups": "مجموعات الأجهزة", + "shared-device-groups": "مجموعات الأجهزة المشتركة", + "asset-groups": "مجموعات الأصول", + "shared-asset-groups": "مجموعات الأصول المشتركة", + "customer-groups": "مجموعات العملاء", + "shared-customer-groups": "مجموعات العملاء المشتركة", + "device-group": "مجموعة الأجهزة", + "asset-group": "مجموعة الأصول", + "user-group": "مجموعة المستخدمين", + "user-groups": "مجموعات المستخدمين", + "customer-group": "مجموعة العملاء", + "entity-view-groups": "مجموعات عرض الكيان", + "shared-entity-view-groups": "مجموعات عرض الكيان المشتركة", + "entity-view-group": "مجموعة عرض الكيان", + "edge-groups": "مجموعات الحافة", + "shared-edge-groups": "مجموعات الحافة المشتركة", + "edge-group": "مجموعة الحافة", + "dashboard-groups": "مجموعات لوحة التحكم", + "shared-dashboard-groups": "مجموعات لوحة التحكم المشتركة", + "dashboard-group": "مجموعة لوحة التحكم", + "fetch-more": "جلب المزيد", + "column-type": { + "column-type": "نوع العمود", + "client-attribute": "صفة العميل", + "shared-attribute": "الصفة المشتركة", + "server-attribute": "صفة الخادم", + "timeseries": "سلسلة زمنية", + "entity-field": "حقل الكيان" + }, + "column-type-required": "نوع العمود مطلوب.", + "entity-field": { + "created-time": "وقت الإنشاء", + "name": "الاسم", + "type": "النوع", + "device_profile": "ملف تعريف الجهاز", + "asset_profile": "ملف تعريف الأصول", + "assigned_customer": "العميل المعين", + "authority": "السلطة", + "first_name": "الاسم الأول", + "last_name": "الاسم الأخير", + "email": "البريد الإلكتروني", + "title": "اللقب", + "country": "البلد", + "state": "الولاية", + "city": "المدينة", + "address": "العنوان", + "address2": "العنوان 2", + "zip": "الرمز البريدي", + "phone": "الهاتف", + "label": "التسمية" + }, + "sort-order": { + "asc": "تصاعدي", + "desc": "تنازلي", + "none": "لا شيء" + }, + "details-mode": { + "on-row-click": "عند النقر على الصف", + "on-action-button-click": "عند النقر على زر التفاصيل", + "disabled": "معطل" + }, + "change-owner": "تغيير المالك", + "select-target-owner": "اختر المالك الهدف", + "no-owners-matching": "لا يوجد مالكين يطابقون '{{owner}}'.", + "target-owner-required": "المالك الهدف مطلوب.", + "confirm-change-owner-title": "هل أنت متأكد أنك تريد تغيير المالك لـ { count, plural, =1 {1 كيان مختار} other {# كيانات مختارة} }؟", + "confirm-change-owner-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع الكيانات المحددة من المالك الحالي وسيتم وضعها في مجموعة 'الكل' للمالك الهدف.", + "add-to-group": "إضافة إلى المجموعة", + "move-to-group": "نقل إلى مجموعة", + "select-entity-group": "اختر مجموعة الكيان", + "no-entity-groups-matching": "لا توجد مجموعات كيان تطابق '{{entityGroup}}'.", + "create-new-entity-group": "إنشاء جديد!", + "target-entity-group-required": "مجموعة الكيان الهدف مطلوبة.", + "select-user-group": "اختر مجموعة المستخدم", + "no-user-groups-matching": "لا توجد مجموعات مستخدمين تطابق '{{entityGroup}}'.", + "target-user-group-required": "مجموعة المستخدم الهدف مطلوبة.", + "remove-from-group": "إزالة من المجموعة", + "group-table-title": "عنوان جدول المجموعة", + "enable-search": "تمكين بحث الكيانات", + "enable-add": "تمكين إضافة الكيانات", + "enable-delete": "تمكين حذف الكيانات", + "enable-selection": "تمكين تحديد الكيانات", + "enable-group-transfer": "تمكين إجراءات نقل المجموعة", + "display-pagination": "عرض الترقيم", + "default-page-size": "حجم الصفحة الافتراضي", + "enable-assignment-actions": "تمكين إجراءات التعيين", + "enable-credentials-management": "تمكين إدارة بيانات الاعتماد", + "enable-login-as-user": "تمكين تسجيل الدخول كمستخدم", + "enable-users-management": "تمكين إدارة المستخدمين", + "enable-customers-management": "تمكين إدارة العملاء", + "enable-assets-management": "تمكين إدارة الأصول", + "enable-devices-management": "تمكين إدارة الأجهزة", + "enable-entity-views-management": "تمكين إدارة عرض الكيانات", + "enable-edges-management": "تمكين إدارة الحواف", + "enable-dashboards-management": "تمكين إدارة لوحات التحكم", + "enable-scheduler-events-management": "تمكين إدارة أحداث المجدول", + "open-details-on": "فتح تفاصيل الكيان على", + "select-existing": "اختر مجموعة الكيان الحالية", + "create-new": "إنشاء مجموعة كيان جديدة", + "new-entity-group-name": "اسم مجموعة الكيان الجديدة", + "entity-group-list": "قائمة مجموعات الكيان", + "entity-group-list-empty": "لم يتم اختيار مجموعات الكيان.", + "name-starts-with": "اسم مجموعة الكيان يبدأ بـ", + "entity-group-name-filter-required": "مطلوب تصفية اسم مجموعة الكيان.", + "roles": "الأدوار", + "permissions": "الأذونات", + "public": "عام", + "entity-group-public": "مجموعة الكيان عامة", + "make-public": "جعل مجموعة الكيان عامة", + "make-private": "جعل مجموعة الكيان خاصة", + "make-public-entity-group-title": "هل أنت متأكد أنك تريد جعل مجموعة الكيان '{{entityGroupName}}' عامة؟", + "make-public-entity-group-text": "بعد التأكيد، سيتم جعل مجموعة الكيان وجميع كياناتها عامة ومتاحة للآخرين.", + "make-private-entity-group-title": "هل أنت متأكد أنك تريد جعل مجموعة الكيان '{{entityGroupName}}' خاصة؟", + "make-private-entity-group-text": "بعد التأكيد، سيتم جعل مجموعة الكيان وجميع كياناتها خاصة ولن تكون متاحة للآخرين.", + "share": "مشاركة مجموعة الكيان", + "copyId": "نسخ معرف مجموعة الكيان", + "idCopiedMessage": "تم نسخ معرف مجموعة الكيان إلى الحافظة", + "entity-group-name": "اسم مجموعة الكيان", + "all-users": "جميع المستخدمين", + "owner": "المالك", + "owner-required": "المالك مطلوب.", + "manage-owner-and-groups": "إدارة المالك والمجموعات", + "owner-and-groups": "المالك والمجموعات" + }, + "entity-field": { + "created-time": "وقت الإنشاء", + "name": "الاسم", + "type": "النوع", + "first-name": "الاسم الأول", + "last-name": "اسم العائلة", + "email": "البريد الإلكتروني", + "title": "اللقب", + "country": "البلد", + "state": "الولاية", + "city": "المدينة", + "address": "العنوان", + "address2": "العنوان 2", + "zip": "الرمز البريدي", + "phone": "الهاتف", + "label": "التسمية", + "configuration": "التكوين", + "schedule": "الجدول", + "originatorId": "معرف المنشئ", + "originatorType": "نوع المنشئ" + }, + "entity-view": { + "all": "الكل", + "all-entity-views": "جميع عرض الكيانات", + "groups": "المجموعات", + "shared": "مشترك", + "entity-view": "عرض الكيان", + "entity-view-required": "عرض الكيان مطلوب.", + "entity-views": "عرض الكيانات", + "management": "إدارة عرض الكيان", + "view-entity-views": "عرض عرض الكيانات", + "entity-view-alias": "اسم مستعار لعرض الكيان", + "aliases": "الأسماء المستعارة", + "no-alias-matching": "لم يتم العثور على '{{alias}}'.", + "no-aliases-found": "لم يتم العثور على أسماء مستعارة.", + "no-key-matching": "لم يتم العثور على '{{key}}'.", + "no-keys-found": "لم يتم العثور على مفاتيح.", + "create-new-alias": "إنشاء جديد!", + "create-new-key": "إنشاء جديد!", + "duplicate-alias-error": "تم العثور على اسم مستعار مكرر '{{alias}}'.
يجب أن تكون الأسماء المستعارة لعرض الكيان فريدة ضمن لوحة القيادة.", + "configure-alias": "تكوين '{{alias}}' الاسم المستعار", + "no-entity-views-matching": "لم يتم العثور على عرض كيانات يطابق '{{entity}}'.", + "public": "عام", + "alias": "الاسم المستعار", + "alias-required": "اسم مستعار لعرض الكيان مطلوب.", + "remove-alias": "إزالة الاسم المستعار لعرض الكيان", + "add-alias": "إضافة اسم مستعار لعرض الكيان", + "name-starts-with": "تعبير اسم عرض الكيان", + "help-text": "استخدم '%' حسب الحاجة: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'.", + "entity-view-list": "قائمة عرض الكيان", + "use-entity-view-name-filter": "استخدم الفلتر", + "entity-view-list-empty": "لم يتم اختيار عرض الكيانات.", + "entity-view-name-filter-required": "مطلوب فلتر اسم عرض الكيان.", + "entity-view-name-filter-no-entity-view-matched": "لم يتم العثور على عرض كيانات يبدأ بـ '{{entityView}}'.", + "add": "إضافة عرض كيان", + "entity-view-public": "عرض الكيان عام", + "assign-to-customer": "تعيين للعميل", + "assign-entity-view-to-customer": "تعيين عرض الكيان(ات) للعميل", + "assign-entity-view-to-customer-text": "الرجاء تحديد عرض الكيانات لتعيينها للعميل", + "no-entity-views-text": "لم يتم العثور على عرض الكيانات", + "assign-to-customer-text": "الرجاء تحديد العميل لتعيين عرض الكيان(ات)", + "entity-view-details": "تفاصيل عرض الكيان", + "add-entity-view-text": "إضافة عرض كيان جديد", + "delete": "حذف عرض الكيان", + "assign-entity-views": "تعيين عرض الكيانات", + "assign-entity-views-text": "تعيين { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} } للعميل", + "delete-entity-views": "حذف عرض الكيانات", + "make-public": "جعل عرض الكيان عام", + "make-private": "جعل عرض الكيان خاص", + "unassign-from-customer": "إلغاء التعيين من العميل", + "unassign-entity-views": "إلغاء تعيين عرض الكيانات", + "unassign-entity-views-action-title": "إلغاء تعيين { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} } من العميل", + "assign-new-entity-view": "تعيين عرض كيان جديد", + "delete-entity-view-title": "هل أنت متأكد أنك تريد حذف عرض الكيان '{{entityViewName}}'؟", + "delete-entity-view-text": "كن حذرًا، بعد التأكيد سيتم حذف عرض الكيان وجميع البيانات المتعلقة به ولن يمكن استردادها.", + "delete-entity-views-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} }؟", + "delete-entity-views-action-title": "حذف { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} }", + "delete-entity-views-text": "كن حذرًا، بعد التأكيد سيتم حذف جميع عرض الكيانات المحددة وجميع البيانات المتعلقة بها ولن يمكن استردادها.", + "make-public-entity-view-title": "هل أنت متأكد أنك تريد جعل عرض الكيان '{{entityViewName}}' عام؟", + "make-public-entity-view-text": "بعد التأكيد، سيتم جعل عرض الكيان وجميع بياناته عامة ومتاحة للآخرين.", + "make-private-entity-view-title": "هل أنت متأكد أنك تريد جعل عرض الكيان '{{entityViewName}}' خاص؟", + "make-private-entity-view-text": "بعد التأكيد، سيتم جعل عرض الكيان وجميع بياناته خاصة ولن تكون متاحة للآخرين.", + "unassign-entity-view-title": "هل أنت متأكد أنك تريد إلغاء تعيين عرض الكيان '{{entityViewName}}'؟", + "unassign-entity-view-text": "بعد التأكيد، سيتم إلغاء تعيين عرض الكيان ولن يكون متاحًا للعميل.", + "unassign-entity-view": "إلغاء تعيين عرض الكيان", + "unassign-entity-views-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} }؟", + "unassign-entity-views-text": "بعد التأكيد، سيتم إلغاء تعيين جميع عرض الكيانات المحددة ولن تكون متاحة للعميل.", + "entity-view-type": "نوع عرض الكيان", + "entity-view-type-required": "نوع عرض الكيان مطلوب.", + "select-entity-view-type": "حدد نوع عرض الكيان", + "enter-entity-view-type": "أدخل نوع عرض الكيان", + "any-entity-view": "أي عرض كيان", + "no-entity-view-types-matching": "لم يتم العثور على أنواع عرض كيان تطابق '{{entitySubtype}}'.", + "entity-view-type-list-empty": "لم يتم اختيار أنواع عرض الكيان.", + "entity-view-types": "أنواع عرض الكيان", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "type-max-length": "يجب أن يكون نوع عرض الكيان أقل من 256", + "description": "الوصف", + "events": "الأحداث", + "details": "التفاصيل", + "copyId": "نسخ معرف عرض الكيان", + "idCopiedMessage": "تم نسخ معرف عرض الكيان إلى الحافظة", + "assignedToCustomer": "معين للعميل", + "unable-entity-view-device-alias-title": "لا يمكن حذف اسم مستعار لعرض الكيان", + "unable-entity-view-device-alias-text": "لا يمكن حذف الاسم المستعار للجهاز '{{entityViewAlias}}' لأنه يستخدم بواسطة القطع التالية:
{{widgetsList}}", + "select-entity-view": "حدد عرض الكيان", + "start-date": "تاريخ البدء", + "start-ts": "وقت البدء", + "end-date": "تاريخ الانتهاء", + "end-ts": "وقت الانتهاء", + "date-limits": "حدود التاريخ", + "client-attributes": "سمات العميل", + "shared-attributes": "سمات مشتركة", + "server-attributes": "سمات الخادم", + "timeseries": "السلاسل الزمنية", + "client-attributes-placeholder": "سمات العميل", + "shared-attributes-placeholder": "سمات مشتركة", + "server-attributes-placeholder": "سمات الخادم", + "timeseries-placeholder": "السلاسل الزمنية", + "target-entity": "الكيان الهدف", + "attributes-propagation": "انتشار السمات", + "attributes-propagation-hint": "سيقوم عرض الكيان تلقائيًا بنسخ السمات المحددة من الكيان الهدف في كل مرة تقوم فيها بحفظ أو تحديث هذا عرض الكيان. لأسباب تتعلق بالأداء، لا يتم انتشار سمات الكيان الهدف إلى عرض الكيان عند كل تغيير في السمة. يمكنك تمكين الانتشار التلقائي عن طريق تكوين عقدة قاعدة \"نسخ إلى العرض\" في سلسلة القواعد الخاصة بك وربط رسائل \"نشر السمات\" و\"تحديث السمات\" بعقدة القاعدة الجديدة.", + "timeseries-data": "بيانات السلاسل الزمنية", + "timeseries-data-hint": "تكوين مفاتيح بيانات السلاسل الزمنية للكيان الهدف التي ستكون متاحة لعرض الكيان. هذه البيانات للقراءة فقط.", + "select-group-to-add": "حدد المجموعة الهدف لإضافة عرض الكيانات المختارة", + "select-group-to-move": "حدد المجموعة الهدف لنقل عرض الكيانات المختارة", + "remove-entity-views-from-group": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة عرض الكيانات", + "list-of-groups": "{ count, plural, =1 {مجموعة عرض كيان واحدة} other {قائمة مجموعات عرض الكيانات #} }", + "group-name-starts-with": "مجموعات عرض الكيانات التي تبدأ أسماؤها بـ '{{prefix}}'", + "search": "بحث في عرض الكيانات", + "selected-entity-views": "{ count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} } محدد", + "assign-entity-view-to-edge": "تعيين عرض الكيان(ات) للحافة", + "assign-entity-view-to-edge-text": "الرجاء تحديد عرض الكيانات لتعيينها للحافة", + "unassign-entity-view-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين عرض الكيان '{{entityViewName}}'؟", + "unassign-entity-view-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين عرض الكيان ولن يكون متاحًا للحافة.", + "unassign-entity-views-from-edge-action-title": "إلغاء تعيين { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} } من الحافة", + "unassign-entity-view-from-edge": "إلغاء تعيين عرض الكيان", + "unassign-entity-views-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {1 عرض كيان} other {# عرض الكيانات} }؟", + "unassign-entity-views-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين جميع عرض الكيانات المحددة ولن تكون متاحة للحافة." + }, + "event": { + "events": "الأحداث", + "event-type": "نوع الحدث", + "events-filter": "تصفية الأحداث", + "clean-events": "مسح الأحداث", + "type-error": "خطأ", + "type-lc-event": "حدث الدورة الحياة", + "type-stats": "إحصائيات", + "type-debug-converter": "تصحيح", + "type-debug-integration": "تصحيح", + "type-debug-rule-node": "تصحيح", + "type-debug-rule-chain": "تصحيح", + "no-events-prompt": "لم يتم العثور على أحداث", + "error": "خطأ", + "alarm": "إنذار", + "event-time": "وقت الحدث", + "server": "الخادم", + "body": "الجسم", + "method": "الطريقة", + "type": "النوع", + "in": "داخل", + "out": "خارج", + "metadata": "البيانات الوصفية", + "message": "الرسالة", + "entity": "الكيان", + "message-id": "معرف الرسالة", + "copy-message-id": "نسخ معرف الرسالة", + "message-type": "نوع الرسالة", + "data-type": "نوع البيانات", + "relation-type": "نوع العلاقة", + "data": "البيانات", + "event": "الحدث", + "status": "الحالة", + "success": "نجاح", + "failed": "فشل", + "messages-processed": "الرسائل المعالجة", + "max-messages-processed": "أقصى عدد للرسائل المعالجة", + "min-messages-processed": "الحد الأدنى للرسائل المعالجة", + "errors-occurred": "الأخطاء المحدثة", + "max-errors-occurred": "أقصى عدد للأخطاء المحدثة", + "min-errors-occurred": "الحد الأدنى للأخطاء المحدثة", + "min-value": "القيمة الدنيا هي 0.", + "all-events": "الكل", + "has-error": "يحتوي على خطأ", + "entity-id": "معرف الكيان", + "copy-entity-id": "نسخ معرف الكيان", + "entity-type": "نوع الكيان", + "clear-filter": "مسح التصفية", + "clear-request-title": "مسح كافة الأحداث", + "clear-request-text": "هل أنت متأكد أنك تريد مسح كافة الأحداث؟", + "started": "بدأت", + "updated": "تم التحديث", + "stopped": "توقفت" + }, + "extension": { + "extensions": "الامتدادات", + "selected-extensions": "{ count, plural, =1 {1 امتداد} other {# امتدادات} } محددة", + "type": "النوع", + "key": "المفتاح", + "value": "القيمة", + "id": "المعرف", + "extension-id": "معرف الامتداد", + "extension-type": "نوع الامتداد", + "transformer-json": "JSON *", + "unique-id-required": "معرف الامتداد الحالي موجود بالفعل.", + "delete": "حذف الامتداد", + "add": "إضافة امتداد", + "edit": "تعديل الامتداد", + "view": "عرض الامتداد", + "delete-extension-title": "هل أنت متأكد أنك تريد حذف الامتداد '{{extensionId}}'؟", + "delete-extension-text": "كن حذرًا، بعد التأكيد سيصبح الامتداد وجميع البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-extensions-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 امتداد} other {# امتدادات} }؟", + "delete-extensions-text": "كن حذرًا، بعد التأكيد سيتم إزالة كافة الامتدادات المحددة.", + "converters": "محولات", + "converter-id": "معرف المحول", + "configuration": "التكوين", + "converter-configurations": "تكوينات المحول", + "token": "رمز الأمان", + "add-converter": "إضافة محول", + "add-config": "إضافة تكوين المحول", + "device-name-expression": "تعبير اسم الجهاز", + "device-type-expression": "تعبير نوع الجهاز", + "custom": "مخصص", + "to-double": "إلى عدد عشري", + "transformer": "محول", + "json-required": "مطلوب JSON المحول.", + "json-parse": "غير قادر على تحليل JSON المحول.", + "attributes": "السمات", + "add-attribute": "إضافة سمة", + "add-map": "إضافة عنصر تخطيط", + "timeseries": "سلاسل زمنية", + "add-timeseries": "إضافة سلسلة زمنية", + "field-required": "الحقل مطلوب", + "brokers": "وسطاء", + "add-broker": "إضافة وسيط", + "host": "المضيف", + "port": "المنفذ", + "port-range": "يجب أن يكون المنفذ في نطاق من 1 إلى 65535.", + "ssl": "SSL", + "credentials": "بيانات الاعتماد", + "username": "اسم المستخدم", + "password": "كلمة المرور", + "retry-interval": "فترة إعادة المحاولة بالميللي ثانية", + "sas": "التوقيع المشترك للوصول", + "anonymous": "مجهول", + "basic": "أساسي", + "pem": "PEM", + "ca-cert": "ملف شهادة السلطة الشهادة *", + "private-key": "ملف المفتاح الخاص *", + "cert": "ملف الشهادة *", + "no-file": "لم يتم تحديد ملف.", + "drop-file": "إسقاط ملف أو انقر لتحديد ملف للتحميل.", + "mapping": "تعيين", + "topic-filter": "مرشح الموضوع", + "converter-type": "نوع المحول", + "converter-json": "Json", + "json-name-expression": "تعبير JSON اسم الجهاز", + "topic-name-expression": "تعبير موضوع اسم الجهاز", + "json-type-expression": "تعبير JSON نوع الجهاز", + "topic-type-expression": "تعبير موضوع نوع الجهاز", + "attribute-key-expression": "تعبير مفتاح السمة", + "attr-json-key-expression": "تعبير JSON مفتاح السمة", + "attr-topic-key-expression": "تعبير موضوع مفتاح السمة", + "request-id-expression": "تعبير معرف الطلب", + "request-id-json-expression": "تعبير JSON معرف الطلب", + "request-id-topic-expression": "تعبير موضوع معرف الطلب", + "response-topic-expression": "تعبير موضوع الرد", + "value-expression": "تعبير القيمة", + "topic": "الموضوع", + "timeout": "مهلة زمنية بالميللي ثانية", + "converter-json-required": "مطلوب JSON المحول.", + "converter-json-parse": "غير قادر على تحليل JSON المحول.", + "filter-expression": "تعبير التصفية", + "connect-requests": "طلبات الاتصال", + "add-connect-request": "إضافة طلب اتصال", + "disconnect-requests": "طلبات الافصال", + "add-disconnect-request": "إضافة طلب فصل", + "attribute-requests": "طلبات السمة", + "add-attribute-request": "إضافة طلب سمة", + "attribute-updates": "تحديثات السمة", + "add-attribute-update": "إضافة تحديث سمة", + "server-side-rpc": "الخادم جانب إجراءات الاتصال البعيد", + "add-server-side-rpc-request": "إضافة طلب جانب الخادم", + "device-name-filter": "مرشح اسم الجهاز", + "attribute-filter": "مرشح السمة", + "method-filter": "مرشح الطريقة", + "request-topic-expression": "تعبير موضوع الطلب", + "response-timeout": "مهلة الاستجابة بالميللي ثانية", + "topic-expression": "تعبير الموضوع", + "client-scope": "نطاق العميل", + "add-device": "إضافة جهاز", + "opc-server": "الخوادم", + "opc-add-server": "إضافة خادم", + "opc-add-server-prompt": "يرجى إضافة خادم", + "opc-application-name": "اسم التطبيق", + "opc-application-uri": "معرف التطبيق", + "opc-scan-period-in-seconds": "فترة المسح بالثواني", + "opc-security": "الأمان", + "opc-identity": "الهوية", + "opc-keystore": "متجر المفاتيح", + "opc-type": "النوع", + "opc-keystore-type": "النوع", + "opc-keystore-location": "الموقع *", + "opc-keystore-password": "كلمة المرور", + "opc-keystore-alias": "الاسم المستعار", + "opc-keystore-key-password": "كلمة مرور المفتاح", + "opc-device-node-pattern": "نمط عقدة الجهاز", + "opc-device-name-pattern": "نمط اسم الجهاز", + "modbus-server": "الخوادم / العبيد", + "modbus-add-server": "إضافة خادم / عبد", + "modbus-add-server-prompt": "يرجى إضافة خادم / عبد", + "modbus-transport": "النقل", + "modbus-tcp-reconnect": "إعادة الاتصال تلقائيًا", + "modbus-rtu-over-tcp": "RTU عبر TCP", + "modbus-port-name": "اسم المنفذ التسلسلي", + "modbus-encoding": "الترميز", + "modbus-parity": "الزوجية", + "modbus-baudrate": "معدل الباود", + "modbus-databits": "بتات البيانات", + "modbus-stopbits": "بتات التوقف", + "modbus-databits-range": "يجب أن تكون بتات البيانات في نطاق من 7 إلى 8.", + "modbus-stopbits-range": "يجب أن تكون بتات التوقف في نطاق من 1 إلى 2.", + "modbus-unit-id": "معرف الوحدة", + "modbus-unit-id-range": "يجب أن يكون معرف الوحدة في نطاق من 1 إلى 247.", + "modbus-device-name": "اسم الجهاز", + "modbus-poll-period": "فترة الاستطلاع (مللي ثانية)", + "modbus-attributes-poll-period": "فترة الاستطلاع للسمات (مللي ثانية)", + "modbus-timeseries-poll-period": "فترة الاستطلاع لسلاسل الزمنية (مللي ثانية)", + "modbus-poll-period-range": "يجب أن تكون فترة الاستطلاع قيمة إيجابية.", + "modbus-tag": "علامة", + "modbus-function": "الوظيفة", + "modbus-register-address": "عنوان التسجيل", + "modbus-register-address-range": "يجب أن يكون عنوان التسجيل في نطاق من 0 إلى 65535.", + "modbus-register-bit-index": "مؤشر البت التسجيل", + "modbus-register-bit-index-range": "يجب أن يكون مؤشر البت في نطاق من 0 إلى 15.", + "modbus-register-count": "عدد التسجيل", + "modbus-register-count-range": "يجب أن يكون عدد التسجيل قيمة إيجابية.", + "modbus-byte-order": "ترتيب البايت", + "sync": { + "status": "الحالة", + "sync": "مزامنة", + "not-sync": "غير متزامن", + "last-sync-time": "وقت آخر مزامنة", + "not-available": "غير متاح" + }, + "export-extensions-configuration": "تصدير تكوينات التمديدات", + "import-extensions-configuration": "استيراد تكوينات التمديدات", + "import-extensions": "استيراد التمديدات", + "import-extension": "استيراد التمديد", + "export-extension": "تصدير التمديد", + "file": "ملف التمديدات", + "invalid-file-error": "ملف تمديد غير صالح", + "text": "نص", + "json": "JSON", + "binary": "ثنائي", + "hex": "سداسي عشري" + }, + "feature": { + "advanced-features": "ميزات متقدمة" + }, + "filter": { + "add": "إضافة فلتر", + "edit": "تعديل الفلتر", + "name": "اسم الفلتر", + "name-required": "اسم الفلتر مطلوب.", + "duplicate-filter": "الفلتر بنفس الاسم موجود بالفعل.", + "filters": "الفلاتر", + "unable-delete-filter-title": "غير قادر على حذف الفلتر", + "unable-delete-filter-text": "لا يمكن حذف الفلتر '{{filter}}' لأنه يتم استخدامه من قبل العناصر التالية:
{{widgetsList}}", + "duplicate-filter-error": "تم العثور على فلتر مكرر '{{filter}}'.
يجب أن تكون الفلاتر فريدة ضمن لوحة القيادة.", + "missing-key-filters-error": "الفلاتر المفتاحية مفقودة للفلتر '{{filter}}'.", + "filter": "فلتر", + "editable": "قابل للتحرير", + "no-filters-found": "لم يتم العثور على فلاتر.", + "no-filter-text": "لم يتم تحديد فلتر", + "add-filter-prompt": "يرجى إضافة فلتر", + "no-filter-matching": "لم يتم العثور على '{{filter}}'.", + "create-new-filter": "أنشئ واحدًا جديدًا!", + "create-new": "إنشاء جديد", + "filter-required": "الفلتر مطلوب.", + "operation": { + "operation": "العملية", + "equal": "متساوي", + "not-equal": "غير متساوي", + "starts-with": "يبدأ بـ", + "ends-with": "ينتهي بـ", + "contains": "يحتوي على", + "not-contains": "لا يحتوي على", + "greater": "أكبر من", + "less": "أقل من", + "greater-or-equal": "أكبر من أو يساوي", + "less-or-equal": "أقل من أو يساوي", + "and": "و", + "or": "أو", + "in": "في", + "not-in": "ليس في" + }, + "ignore-case": "تجاهل الحالة", + "value": "القيمة", + "remove-filter": "إزالة الفلتر", + "duplicate-filter-action": "تكرار الفلتر", + "preview": "معاينة الفلتر", + "no-filters": "لم يتم تكوين أي فلاتر", + "add-filter": "إضافة فلتر", + "add-complex-filter": "إضافة فلتر معقد", + "add-complex": "إضافة معقدة", + "complex-filter": "فلتر معقد", + "edit-complex-filter": "تحرير الفلتر المعقد", + "edit-filter-user-params": "تعديل معلمات مستخدم الفلتر الفعلي", + "filter-user-params": "معلمات مستخدم الفلتر الفعلي", + "user-parameters": "معلمات المستخدم", + "display-label": "تسمية للعرض", + "autogenerated-label": "إنشاء تسمية تلقائية", + "order-priority": "أولوية ترتيب الحقل", + "key-filter": "فلتر المفتاح", + "key-filters": "فلاتر المفاتيح", + "key-name": "اسم المفتاح", + "key-name-required": "اسم المفتاح مطلوب.", + "key-type": { + "key-type": "نوع المفتاح", + "attribute": "سمة", + "timeseries": "سلاسل زمنية", + "entity-field": "حقل الكيان", + "constant": "ثابت", + "client-attribute": "سمة العميل", + "server-attribute": "سمة الخادم", + "shared-attribute": "سمة مشتركة" + }, + "value-type": { + "value-type": "نوع القيمة", + "string": "سلسلة", + "numeric": "رقمي", + "boolean": "منطقي", + "date-time": "تاريخ ووقت" + }, + "value-type-required": "نوع قيمة المفتاح مطلوب.", + "key-value-type-change-title": "هل أنت متأكد أنك تريد تغيير نوع قيمة المفتاح؟", + "key-value-type-change-message": "إذا قمت بتأكيد نوع القيمة الجديدة، سيتم إزالة جميع فلاتر المفتاح المدخلة.", + "no-key-filters": "لم يتم تكوين أي فلاتر مفتاح.", + "add-key-filter": "إضافة فلتر مفتاح", + "remove-key-filter": "إزالة فلتر المفتاح", + "edit-key-filter": "تعديل فلتر المفتاح", + "date": "تاريخ", + "time": "وقت", + "current-tenant": "المستأجر الحالي", + "current-customer": "العميل الحالي", + "current-user": "المستخدم الحالي", + "current-device": "الجهاز الحالي", + "default-value": "القيمة الافتراضية", + "dynamic-source-type": "نوع مصدر القيمة الديناميكية", + "dynamic-value": "القيمة الديناميكية", + "no-dynamic-value": "لا توجد قيمة ديناميكية", + "source-attribute": "سمة المصدر", + "switch-to-dynamic-value": "التبديل إلى القيمة الديناميكية", + "switch-to-default-value": "التبديل إلى القيمة الافتراضية", + "inherit-owner": "الميراث من المالك", + "source-attribute-not-set": "إذا لم يتم تعيين سمة المصدر" + }, + "fullscreen": { + "expand": "توسيع إلى وضع ملء الشاشة", + "exit": "الخروج من وضع ملء الشاشة", + "toggle": "تبديل وضع ملء الشاشة", + "fullscreen": "ملء الشاشة" + }, + "function": { + "function": "الوظيفة" + }, + "gateway": { + "add-entry": "إضافة تكوين", + "advanced": "متقدم", + "checking-device-activity": "فحص نشاط الجهاز", + "command": "أوامر Docker", + "command-copied-message": "تم نسخ أمر Docker إلى الحافظة", + "configuration": "التكوين", + "connector-add": "إضافة موصل جديد", + "connector-enabled": "تمكين الموصل", + "connector-name": "اسم الموصل", + "connector-name-required": "اسم الموصل مطلوب.", + "connector-type": "نوع الموصل", + "connector-type-required": "نوع الموصل مطلوب.", + "connectors": "الموصلات", + "connectors-config": "تكوينات الموصلات", + "connectors-table-enabled": "ممكّن", + "connectors-table-name": "الاسم", + "connectors-table-type": "النوع", + "connectors-table-status": "الحالة", + "connectors-table-actions": "الإجراءات", + "connectors-table-key": "المفتاح", + "connectors-table-class": "الفئة", + "rpc-command-send": "إرسال", + "rpc-command-result": "الاستجابة", + "rpc-command-edit-params": "تحرير المعلمات", + "gateway-configuration": "تكوين عام", + "docker-label": "استخدم التعليمات التالية لتشغيل IoT Gateway في Docker compose مع بيانات اعتماد للجهاز المحدد", + "install-docker-compose": "استخدم التعليمات لتنزيل وتثبيت وإعداد docker compose", + "download-configuration-file": "تنزيل ملف التكوين", + "download-docker-compose": "تنزيل docker-compose.yml لبوابتك", + "launch-gateway": "تشغيل البوابة", + "launch-docker-compose": "بدء تشغيل البوابة باستخدام الأمر التالي في الطرفية من المجلد الذي يحتوي على ملف docker-compose.yml", + "create-new-gateway": "إنشاء بوابة جديدة", + "create-new-gateway-text": "هل أنت متأكد أنك تريد إنشاء بوابة جديدة باسم: '{{gatewayName}}'؟", + "created-time": "وقت الإنشاء", + "configuration-delete-dialog-header": "سيتم حذف التكوينات", + "configuration-delete-dialog-body": "يمكن تعطيل التكوين عن بُعد فقط إذا كان هناك وصول جسدي إلى البوابة. ستتم حذف جميع التكوينات السابقة.

\n لتعطيل التكوين، أدخل اسم البوابة أدناه", + "configuration-delete-dialog-input": "اسم البوابة", + "configuration-delete-dialog-input-required": "اسم البوابة إلزامي", + "configuration-delete-dialog-confirm": "إيقاف التشغيل", + "delete": "حذف التكوين", + "download-tip": "تنزيل ملف التكوين", + "drop-file": "أفلق الملف هنا أو", + "gateway": "البوابة", + "gateway-exists": "الجهاز بنفس الاسم موجود بالفعل.", + "gateway-name": "اسم البوابة", + "gateway-name-required": "اسم البوابة مطلوب.", + "gateway-saved": "تم حفظ تكوين البوابة بنجاح.", + "grpc": "GRPC", + "grpc-keep-alive-timeout": "مهلة البقاء على قيد الحياة (بالمللي ثانية)", + "grpc-keep-alive-timeout-required": "مهلة البقاء على قيد الحياة مطلوبة", + "grpc-keep-alive-timeout-min": "مهلة البقاء على قيد الحياة لا يمكن أن تكون أقل من 1", + "grpc-keep-alive-timeout-pattern": "مهلة البقاء على قيد الحياة غير صالحة", + "grpc-keep-alive": "البقاء على قيد الحياة (بالمللي ثانية)", + "grpc-keep-alive-required": "البقاء على قيد الحياة مطلوب", + "grpc-keep-alive-min": "البقاء على قيد الحياة لا يمكن أن يكون أقل من 1", + "grpc-keep-alive-pattern": "البقاء على قيد الحياة غير صالح", + "grpc-min-time-between-pings": "الحد الأدنى للوقت بين البينغات (بالمللي ثانية)", + "grpc-min-time-between-pings-required": "الحد الأدنى للوقت بين البينغات مطلوب", + "grpc-min-time-between-pings-min": "الحد الأدنى للوقت بين البينغات لا يمكن أن يكون أقل من 1", + "grpc-min-time-between-pings-pattern": "الحد الأدنى للوقت بين البينغات غير صالح", + "grpc-min-ping-interval-without-data": "الحد الأدنى لفاصل البينغ بدون بيانات (بالمللي ثانية)", + "grpc-min-ping-interval-without-data-required": "الحد الأدنى لفاصل البينغ بدون بيانات مطلوب", + "grpc-min-ping-interval-without-data-min": "الحد الأدنى لفاصل البينغ بدون بيانات لا يمكن أن يكون أقل من 1", + "grpc-min-ping-interval-without-data-pattern": "الحد الأدنى لفاصل البينغ بدون بيانات غير صالح", + "grpc-max-pings-without-data": "الحد الأقصى لعدد البينغات بدون بيانات", + "grpc-max-pings-without-data-required": "الحد الأقصى لعدد البينغات بدون بيانات مطلوب", + "grpc-max-pings-without-data-min": "الحد الأقصى لعدد البينغات بدون بيانات لا يمكن أن يكون أقل من 1", + "grpc-max-pings-without-data-pattern": "الحد الأقصى لعدد البينغات بدون بيانات غير صالح", + "inactivity-check-period-seconds": "فترة فحص الخمول (بالثواني)", + "inactivity-check-period-seconds-required": "فترة فحص الخمول مطلوبة", + "inactivity-check-period-seconds-min": "فترة فحص الخمول لا يمكن أن تكون أقل من 1", + "inactivity-check-period-seconds-pattern": "فترة فحص الخمول غير صالحة", + "inactivity-timeout-seconds": "فترة الخمول (بالثواني)", + "inactivity-timeout-seconds-required": "فترة الخمول مطلوبة", + "inactivity-timeout-seconds-min": "فترة الخمول لا يمكن أن تكون أقل من 1", + "inactivity-timeout-seconds-pattern": "فترة الخمول غير صالحة", + "json-parse": "JSON غير صالح.", + "json-required": "الحقل لا يمكن أن يكون فارغًا.", + "logs": { + "logs": "السجلات", + "days": "أيام", + "hours": "ساعات", + "minutes": "دقائق", + "seconds": "ثواني", + "date-format": "تنسيق التاريخ", + "date-format-required": "تنسيق التاريخ مطلوب", + "log-format": "تنسيق السجل", + "log-type": "نوع السجل", + "log-format-required": "تنسيق السجل مطلوب", + "remote": "التسجيل عن بُعد", + "remote-logs": "السجلات عن بُعد", + "local": "التسجيل المحلي", + "level": "مستوى السجل", + "file-path": "مسار الملف", + "file-path-required": "مسار الملف مطلوب", + "saving-period": "فترة حفظ السجل", + "saving-period-min": "فترة حفظ السجل لا يمكن أن تكون أقل من 1", + "saving-period-required": "فترة حفظ السجل مطلوبة", + "backup-count": "عدد النسخ الاحتياطية", + "backup-count-min": "عدد النسخ الاحتياطية لا يمكن أن يكون أقل من 1", + "backup-count-required": "عدد النسخ الاحتياطية مطلوب" + }, + "min-pack-send-delay": "الحد الأدنى لتأخير إرسال الحزمة (بالمللي ثانية)", + "min-pack-send-delay-required": "الحد الأدنى لتأخير إرسال الحزمة مطلوب", + "min-pack-send-delay-min": "لا يمكن أن يكون الحد الأدنى لتأخير إرسال الحزمة أقل من 0", + "no-connectors": "لا توجد موصلات", + "no-data": "لا توجد تكوينات", + "no-gateway-found": "لم يتم العثور على بوابة.", + "no-gateway-matching": "'{{item}}' غير موجود.", + "path-logs": "مسار إلى ملفات السجل", + "path-logs-required": "المسار مطلوب.", + "permit-without-calls": "البقاء على الحياة يسمح بدون مكالمات", + "remote": "التكوين عن بُعد", + "remote-logging-level": "مستوى التسجيل", + "remove-entry": "إزالة التكوين", + "remote-shell": "قشرة عن بُعد", + "remote-configuration": "التكوين عن بُعد", + "other": "آخر", + "save-tip": "حفظ ملف التكوين", + "security-type": "نوع الأمان", + "security-types": { + "access-token": "رمز الوصول", + "username-password": "اسم المستخدم وكلمة المرور", + "tls": "TLS", + "tls-access-token": "TLS + رمز الوصول", + "tls-private-key": "TLS + المفتاح الخاص" + }, + "server-port": "منفذ الخادم", + "statistics": { + "statistic": "إحصائية", + "statistics": "الإحصائيات", + "statistic-commands-empty": "لا تتوفر إحصائيات", + "commands": "الأوامر", + "send-period": "فترة إرسال الإحصائيات (بالثواني)", + "send-period-required": "فترة إرسال الإحصائيات مطلوبة", + "send-period-min": "لا يمكن أن تكون فترة إرسال الإحصائيات أقل من 60", + "send-period-pattern": "فترة إرسال الإحصائيات غير صالحة", + "check-connectors-configuration": "فترة فحص تكوين الموصلات (بالثواني)", + "check-connectors-configuration-required": "فترة فحص تكوين الموصلات مطلوبة", + "check-connectors-configuration-min": "لا يمكن أن تكون فترة فحص تكوين الموصلات أقل من 1", + "check-connectors-configuration-pattern": "فترة فحص تكوين الموصلات غير صالحة", + "add": "إضافة أمر", + "timeout": "المهلة", + "timeout-ms": "المهلة (بالمللي ثانية)", + "timeout-required": "المهلة مطلوبة", + "timeout-min": "لا يمكن أن تكون المهلة أقل من 1", + "timeout-pattern": "المهلة غير صالحة", + "attribute-name": "اسم السمة", + "attribute-name-required": "اسم السمة مطلوب", + "command": "الأمر", + "command-required": "الأمر مطلوب", + "command-pattern": "الأمر غير صالح", + "remove": "إزالة الأمر" + }, + "storage": "التخزين", + "storage-max-file-records": "السجلات القصوى في الملف", + "storage-max-files": "الحد الأقصى لعدد الملفات", + "storage-max-files-min": "الحد الأدنى هو 1.", + "storage-max-files-pattern": "العدد غير صالح.", + "storage-max-files-required": "العدد مطلوب.", + "storage-max-records": "السجلات القصوى في التخزين", + "storage-max-records-min": "الحد الأدنى لعدد السجلات هو 1.", + "storage-max-records-pattern": "العدد غير صالح.", + "storage-max-records-required": "السجلات القصوى مطلوبة.", + "storage-read-record-count": "عدد قراءة السجلات في التخزين", + "storage-read-record-count-min": "الحد الأدنى لعدد السجلات هو 1.", + "storage-read-record-count-pattern": "العدد غير صالح.", + "storage-read-record-count-required": "عدد قراءة السجلات مطلوب.", + "storage-max-read-record-count": "الحد الأقصى لعدد قراءة السجلات في التخزين", + "storage-max-read-record-count-min": "الحد الأدنى لعدد السجلات هو 1.", + "storage-max-read-record-count-pattern": "العدد غير صالح.", + "storage-max-read-record-count-required": "عدد القراءة القصوى مطلوب.", + "storage-data-folder-path": "مسار مجلد البيانات", + "storage-data-folder-path-required": "مسار مجلد البيانات مطلوب.", + "storage-pack-size": "الحد الأقصى لحجم حزمة الحدث", + "storage-pack-size-min": "الحد الأدنى هو 1.", + "storage-pack-size-pattern": "العدد غير صالح.", + "storage-pack-size-required": "الحجم الأقصى لحزمة الحدث مطلوب.", + "storage-path": "مسار التخزين", + "storage-path-required": "مسار التخزين مطلوب.", + "storage-type": "نوع التخزين", + "storage-types": { + "file-storage": "تخزين الملفات", + "memory-storage": "تخزين الذاكرة", + "sqlite": "SQLITE" + }, + "thingsboard": "ثينغزبورد", + "general": "عام", + "thingsboard-host": "مضيف ثينغزبورد", + "thingsboard-host-required": "المضيف مطلوب.", + "thingsboard-port": "منفذ ثينغزبورد", + "thingsboard-port-max": "الحد الأقصى لرقم المنفذ هو 65535.", + "thingsboard-port-min": "الحد الأدنى لرقم المنفذ هو 1.", + "thingsboard-port-pattern": "المنفذ غير صالح.", + "thingsboard-port-required": "المنفذ مطلوب.", + "tidy": "ترتيب", + "tidy-tip": "ترتيب تكوين JSON", + "title-connectors-json": "تكوين موصل {{typeName}}", + "tls-path-ca-certificate": "المسار إلى شهادة CA على البوابة", + "tls-path-client-certificate": "المسار إلى شهادة العميل على البوابة", + "messages-ttl-check-in-hours": "فحص TTL الرسائل بالساعات", + "messages-ttl-check-in-hours-required": "يجب تحديد فحص TTL الرسائل بالساعات.", + "messages-ttl-check-in-hours-min": "الحد الأدنى هو 1.", + "messages-ttl-check-in-hours-pattern": "الرقم غير صالح.", + "messages-ttl-in-days": "TTL الرسائل بالأيام", + "messages-ttl-in-days-required": "يجب تحديد TTL الرسائل بالأيام.", + "messages-ttl-in-days-min": "الحد الأدنى هو 1.", + "messages-ttl-in-days-pattern": "الرقم غير صالح.", + "mqtt-qos": "جودة الخدمة (QoS)", + "mqtt-qos-required": "جودة الخدمة (QoS) مطلوبة", + "mqtt-qos-range": "تتراوح قيم جودة الخدمة (QoS) من 0 إلى 1", + "tls-path-private-key": "المسار إلى المفتاح الخاص على البوابة", + "toggle-fullscreen": "تبديل وضع ملء الشاشة", + "transformer-json-config": "تكوين JSON*", + "update-config": "إضافة/تحديث تكوين JSON", + "hints": { + "remote-configuration": "يمكنك تمكين التكوين وإدارة البوابة عن بُعد", + "remote-shell": "يمكنك تمكين التحكم البعيد في نظام التشغيل مع البوابة من عنصر واجهة المستخدم قشرة عن بُعد", + "host": "اسم المضيف أو عنوان IP لخادم ثينغزبورد", + "port": "منفذ خدمة MQTT على خادم ثينغزبورد", + "token": "رمز الوصول للبوابة من خادم ثينغزبورد", + "client-id": "معرف عميل MQTT للبوابة من خادم ثينغزبورد", + "username": "اسم المستخدم MQTT للبوابة من خادم ثينغزبورد", + "password": "كلمة المرور MQTT للبوابة من خادم ثينغزبورد", + "ca-cert": "المسار إلى ملف شهادة CA", + "date-form": "تنسيق التاريخ في رسالة السجل", + "data-folder": "المسار إلى المجلد الذي سيحتوي على البيانات (نسبي أو مطلق)", + "log-format": "تنسيق رسالة السجل", + "remote-log": "يمكنك تمكين التسجيل البعيد وقراءة السجلات من البوابة", + "backup-count": "إذا كان عدد النسخ الاحتياطية > 0، عند عملية تدوير، لا يتم الاحتفاظ بأكثر من عدد النسخ الاحتياطية المحددة - يتم حذف الأقدم", + "storage": "يوفر تكوينًا لحفظ البيانات الواردة قبل إرسالها إلى المنصة", + "max-file-count": "العدد الأقصى لعدد الملفات التي سيتم إنشاؤها", + "max-read-count": "عدد الرسائل للحصول عليها من التخزين وإرسالها إلى ثينغزبورد", + "max-records": "العدد الأقصى للسجلات التي ستخزن في ملف واحد", + "read-record-count": "عدد الرسائل للحصول عليها من التخزين وإرسالها إلى ثينغزبورد", + "max-records-count": "العدد الأقصى للبيانات في التخزين قبل إرسالها إلى ثينغزبورد", + "ttl-check-hour": "كم مرة سيتحقق البوابة من البيانات القديمة", + "ttl-messages-day": "الحد الأقصى لعدد الأيام التي ستحتفظ فيها التخزين بالبيانات", + "commands": "الأوامر لجمع الإحصائيات الإضافية", + "attribute": "مفتاح تلقي الإحصائيات", + "timeout": "مهلة زمنية لتنفيذ الأمر", + "command": "سيتم استخدام نتيجة تنفيذ الأمر كقيمة لتلقي الإحصائيات", + "check-device-activity": "يمكنك تمكين مراقبة نشاط كل جهاز متصل", + "inactivity-timeout": "الوقت بعد الذي ستفصل البوابة الجهاز", + "inactivity-period": "تكرار فحص نشاط الجهاز", + "minimal-pack-delay": "التأخير بين إرسال حزم الرسائل (يؤدي تقليل هذا الإعداد إلى زيادة استخدام وحدة المعالجة المركزية)", + "qos": "جودة الخدمة في رسائل MQTT (0 - على الأكثر مرة واحدة، 1 - على الأقل مرة واحدة)", + "server-port": "منفذ الشبكة الذي سيستمع فيه خادم GRPC للاستفسارات الواردة.", + "grpc-keep-alive-timeout": "الحد الأقصى للوقت الذي يجب أن ينتظره الخادم لاستجابة رسالة الحفاظ على الاتصال قبل اعتبار الاتصال ميتًا.", + "grpc-keep-alive": "المدة بين رسائل حفظ الاتصال المتعاقبة عند عدم وجود استدعاء RPC نشط.", + "grpc-min-time-between-pings": "الحد الأدنى للوقت الذي يجب فيه أن ينتظر الخادم بين إرسال رسائل حفظ الاتصال", + "grpc-max-pings-without-data": "الحد الأقصى لعدد رسائل حفظ الاتصال التي يمكن للخادم إرسالها دون تلقي أي بيانات قبل اعتبار الاتصال ميتًا.", + "grpc-min-ping-interval-without-data": "الحد الأدنى للوقت الذي يجب فيه أن ينتظر الخادم بين إرسال رسائل حفظ الاتصال عند عدم إرسال أو استلام بيانات.", + "permit-without-calls": "السماح للخادم بإبقاء اتصال GRPC حيًا حتى عندما لا تكون هناك استدعاءات RPC نشطة." + } + }, + "grid": { + "delete-item-title": "هل أنت متأكد أنك تريد حذف هذا العنصر؟", + "delete-item-text": "كن حذرًا، بعد التأكيد سيصبح هذا العنصر وجميع البيانات المتعلقة به لا يمكن استعادتها.", + "delete-items-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 عنصر} other {# عناصر} }؟", + "delete-items-action-title": "حذف { count, plural, =1 {1 عنصر} other {# عناصر} }", + "delete-items-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع العناصر المحددة وسيصبح جميع البيانات المتعلقة بها لا يمكن استعادتها.", + "add-item-text": "إضافة عنصر جديد", + "no-items-text": "لا توجد عناصر", + "item-details": "تفاصيل العنصر", + "delete-item": "حذف العنصر", + "delete-items": "حذف العناصر", + "scroll-to-top": "التمرير إلى الأعلى" + }, + "help": { + "goto-help-page": "الانتقال إلى صفحة المساعدة", + "show-help": "عرض المساعدة" + }, + "home": { + "home": "الرئيسية", + "profile": "الملف الشخصي", + "logout": "تسجيل الخروج", + "menu": "القائمة", + "avatar": "الصورة الرمزية", + "open-user-menu": "فتح قائمة المستخدم" + }, + "file-input": { + "browse-file": "استعراض الملف", + "browse-files": "استعراض الملفات" + }, + "image": { + "gallery": "معرض الصور", + "search": "البحث عن صورة", + "selected-images": "{ count, plural, =1 {صورة واحدة} other {# صور} } محددة", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "resolution": "الدقة", + "size": "الحجم", + "system": "النظام", + "download-image": "تحميل الصورة", + "export-image": "تصدير الصورة إلى JSON", + "import-image": "استيراد الصورة من JSON", + "upload-image": "رفع الصورة", + "edit-image": "تحرير الصورة", + "image-details": "تفاصيل الصورة", + "no-images": "لا توجد صور", + "delete-image": "حذف الصورة", + "delete-image-title": "هل أنت متأكد أنك تريد حذف الصورة '{{imageTitle}}'؟", + "delete-image-text": "كن حذرًا، بعد التأكيد ستصبح الصورة لا يمكن استعادتها.", + "delete-images-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {صورة واحدة} other {# صور} }؟", + "delete-images-text": "كن حذرًا، بعد التأكيد ستتم إزالة جميع الصور المحددة وسيصبح جميع البيانات المتعلقة بها لا يمكن استعادتها.", + "list-mode": "عرض القائمة", + "grid-mode": "عرض الشبكة", + "image-preview": "معاينة الصورة", + "update-image": "تحديث الصورة", + "export-failed-error": "غير قادر على تصدير الصورة: {{error}}", + "image-json-file": "ملف JSON للصورة", + "invalid-image-json-file-error": "غير قادر على استيراد الصورة من JSON: هيكل بيانات JSON غير صالح للصورة.", + "image-is-in-use": "الصورة قيد الاستخدام من قبل كيانات أخرى", + "images-are-in-use": "الصور مستخدمة من قبل كيانات أخرى", + "image-is-in-use-text": "لم يتم حذف الصورة '{{title}}' لأنها مستخدمة من قبل الكيانات التالية:", + "images-are-in-use-text": "لم يتم حذف جميع الصور بسبب استخدامها من قبل كيانات أخرى.
يمكنك عرض الكيانات المرجعية بالنقر على زر المراجع في صف الصورة المقابل.
إذا كنت ترغب في حذف هذه الصور بعد ذلك، حدد الصور في الجدول أدناه وانقر على الزر حذف المحدد.", + "delete-image-in-use-text": "إذا كنت لا تزال ترغب في حذف الصورة، انقر فوق الزر حذف على أي حال.", + "system-entities": "كيانات النظام:", + "entities": "الكيانات:", + "references": "المراجع", + "include-system-images": "تضمين الصور النظامية", + "clear-image": "مسح الصورة", + "no-image": "لا توجد صورة", + "no-image-selected": "لم يتم اختيار أي صورة", + "browse-from-gallery": "استعراض من المعرض", + "set-link": "تعيين الرابط", + "image-link": "رابط الصورة", + "link": "رابط", + "copy-image-link": "نسخ رابط الصورة", + "embed-image": "تضمين الصورة", + "embed-to-html": "تضمين إلى HTML", + "embed-to-html-hint": "ستتاح هذه الميزة لأي مستخدم غير مصرح له.", + "embed-to-html-text": "باستخدام مقطع الكود التالي، يمكنك تضمين صورة في المكونات التي تعتمد على HTML العادي.
تشمل هذه المكونات عناصر بطاقة HTML، ووظائف محتوى الخلية، وما إلى ذلك.", + "embed-to-angular-template": "تضمين إلى قالب HTML للأنغولار", + "embed-to-angular-template-text": "باستخدام مقطع الكود التالي، يمكنك تضمين صورة في قالب HTML للأنغولار.
تشمل هذه المكونات عنصر العلامة التجارية، والقسم HTML في محرر الويدجت، والإجراءات المخصصة، وما إلى ذلك." + }, + "image-input": { + "drop-images-or": "اسحب وأسقط الصور أو", + "drag-and-drop": "اسحب وأسقط", + "or": "أو", + "browse": "استعرض", + "no-images": "لم يتم تحديد أي صور", + "images": "الصور" + }, + "import": { + "no-file": "لم يتم تحديد أي ملف", + "drop-file": "اسحب ملف JSON أو انقر لتحديد ملف لتحميله.", + "drop-csv-file": "اسحب ملف CSV أو انقر لتحديد ملف لتحميله.", + "drop-json-file-or": "اسحب ملف JSON أو", + "drop-file-csv": "اسحب ملف CSV أو انقر لتحديد ملف لتحميله.", + "drop-file-csv-or": "اسحب ملف CSV أو", + "column-value": "القيمة", + "column-title": "العنوان", + "column-example": "قيمة مثالية للبيانات", + "column-key": "السمة / مفتاح البيانات", + "credentials": "بيانات الاعتماد", + "csv-delimiter": "فاصل CSV", + "csv-first-line-header": "السطر الأول يحتوي على أسماء الأعمدة", + "csv-update-data": "تحديث السمات / البيانات", + "details": "التفاصيل", + "import-csv-number-columns-error": "يجب أن يحتوي الملف على على الأقل عمودين", + "import-csv-invalid-format-error": "تنسيق الملف غير صالح. السطر: '{{line}}'", + "column-type": { + "name": "الاسم", + "type": "النوع", + "label": "التسمية", + "column-type": "نوع العمود", + "client-attribute": "سمة العميل", + "shared-attribute": "سمة مشتركة", + "server-attribute": "سمة الخادم", + "timeseries": "سلسلة زمنية", + "entity-field": "حقل الكيان", + "access-token": "رمز الوصول", + "x509": "X.509", + "mqtt": { + "client-id": "معرف عميل MQTT", + "user-name": "اسم مستخدم MQTT", + "password": "كلمة مرور MQTT" + }, + "lwm2m": { + "client-endpoint": "اسم نقطة نهاية عميل LwM2M", + "security-config-mode": "وضع تكوين أمان LwM2M", + "client-identity": "هوية عميل LwM2M", + "client-key": "مفتاح عميل LwM2M", + "client-cert": "شهادة عميل LwM2M العامة", + "bootstrap-server-security-mode": "وضع أمان خادم التمهيد LwM2M", + "bootstrap-server-secret-key": "مفتاح سري لخادم التمهيد LwM2M", + "bootstrap-server-public-key-id": "المفتاح العام أو معرف خادم التمهيد LwM2M", + "lwm2m-server-security-mode": "وضع أمان خادم LwM2M", + "lwm2m-server-secret-key": "مفتاح سري لخادم LwM2M", + "lwm2m-server-public-key-id": "المفتاح العام أو معرف خادم LwM2M" + }, + "snmp": { + "host": "مضيف SNMP", + "port": "منفذ SNMP", + "version": "إصدار SNMP (v1، v2c أو v3)", + "community-string": "سلسلة المجتمع SNMP" + }, + "isgateway": "هل هو بوابة", + "activity-time-from-gateway-device": "وقت النشاط من جهاز البوابة", + "description": "الوصف", + "edge-license-key": "مفتاح الترخيص", + "cloud-endpoint": "نقطة نهاية السحابة", + "routing-key": "مفتاح الحافة", + "secret": "السر" + }, + "stepper-text": { + "select-file": "اختر ملف", + "configuration": "استيراد التكوين", + "column-type": "حدد نوع الأعمدة", + "creat-entities": "إنشاء كيانات جديدة" + }, + "message": { + "create-entities": "تم إنشاء {{count}} كيانات جديدة بنجاح.", + "update-entities": "تم تحديث {{count}} كيان بنجاح.", + "error-entities": "حدث خطأ أثناء إنشاء {{count}} كيان." + } + }, + "integration": { + "integration": "التكامل", + "integrations": "التكاملات", + "integrations-center": "مركز التكاملات", + "select-integration": "اختر التكامل", + "no-integrations-matching": "لم يتم العثور على تكاملات تطابق '{{entity}}'.", + "integration-required": "التكامل مطلوب", + "delete": "حذف التكامل", + "management": "إدارة التكاملات", + "add-integration-text": "إضافة تكامل جديد", + "no-integrations-text": "لم يتم العثور على تكاملات", + "data-convertor-name": "محول البيانات {{convertorType}} لـ {{integrationName}}", + "integration-name": "تكامل {{integrationType}}", + "selected-integrations": "{ count, plural, =1 {1 تكامل} other {# تكاملات} } محددة", + "delete-integration-title": "هل أنت متأكد أنك تريد حذف التكامل '{{integrationName}}'؟", + "delete-integration-text": "كن حذراً، بعد التأكيد سيتم حذف التكامل وجميع البيانات المتعلقة به نهائياً.", + "delete-integrations-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 تكامل} other {# تكاملات} }؟", + "delete-integrations-action-title": "حذف { count, plural, =1 {1 تكامل} other {# تكاملات} }", + "delete-integrations-text": "كن حذراً، بعد التأكيد سيتم حذف جميع التكاملات المحددة وجميع البيانات المتعلقة بها نهائياً.", + "events": "الأحداث", + "enabled": "تمكين التكامل", + "allow-create-devices-or-assets": "السماح بإنشاء أجهزة أو أصول", + "all-types": "جميع أنواع التكامل", + "add": "إضافة تكامل", + "search": "بحث عن التكاملات", + "integration-details": "تفاصيل التكامل", + "details": "التفاصيل", + "copyId": "نسخ معرف التكامل", + "idCopiedMessage": "تم نسخ معرف التكامل إلى الحافظة", + "debug-mode": "وضع تصحيح الأخطاء", + "enable-security": "تمكين الأمان", + "enable-security-new": "تمكين الأمان لتحديثات الرمز التلقائية", + "headers-filter": "تصفية الرؤوس", + "header": "الرأس", + "no-headers-filter": "لا توجد عمليات تصفية للرؤوس", + "downlink-url": "رابط الاتصال الهابط", + "downlink-url-required": "رابط الاتصال الهابط مطلوب", + "create-loriot-output": "إنشاء مخرجات تطبيق Loriot", + "send-downlink": "إرسال رابط الاتصال الهابط", + "allow-downlink": "السماح بالاتصال الهابط", + "server": "الخادم", + "server-required": "الخادم مطلوب", + "domain": "النطاق", + "app-id": "معرف التطبيق", + "app-id-required": "معرف التطبيق مطلوب", + "app-token": "رمز الوصول للتطبيق", + "app-token-required": "رمز الوصول للتطبيق مطلوب", + "email": "البريد الإلكتروني", + "email-required": "البريد الإلكتروني مطلوب", + "application-uri": "عنوان تطبيق", + "as-id": "معرف AS", + "as-id-required": "معرف AS مطلوب.", + "as-key": "AS مفتاح", + "as-key-required": "AS مفتاح مطلوب.", + "client-id-new": "معرف العميل", + "client-id-new-required": "معرف العميل (تسجيل الدخول) مطلوب (تسجيل الدخول).", + "client-secret": "سر العميل", + "client-secret-required": "سر العميل (كلمة المرور) مطلوب (كلمة المرور).", + "max-time-diff-in-seconds": "أقصى فارق زمني (ثواني)", + "max-time-diff-in-seconds-required": "الفارق الزمني الأقصى مطلوب.", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "description": "الوصف", + "base-url": "الرابط الأساسي", + "base-url-required": "الرابط الأساسي مطلوب", + "security-key": "مفتاح الأمان", + "http-endpoint": "رابط نقطة النهاية HTTP", + "replace-no-content-to-ok": "استبدال حالة الرد من 'لا محتوى' إلى 'موافق'", + "copy-http-endpoint-url": "نسخ رابط نقطة النهاية HTTP", + "http-endpoint-url-copied-message": "تم نسخ رابط نقطة النهاية HTTP إلى الحافظة", + "host": "المضيف", + "host-required": "المضيف مطلوب.", + "host-private": "يجب أن يكون المضيف في الشبكة العامة.", + "url-private": "يجب أن يكون الرابط في الشبكة العامة.", + "host-type": "نوع المضيف", + "host-type-required": "نوع المضيف مطلوب.", + "api-version": "استخدم API v3", + "custom-host": "مضيف مخصص", + "custom-host-required": "المضيف المخصص مطلوب.", + "port": "المنفذ", + "port-required": "المنفذ مطلوب.", + "port-range": "يجب أن يكون المنفذ في نطاق من 1 إلى 65535.", + "connect-timeout": "مهلة الاتصال (ثانية)", + "connect-timeout-required": "مهلة الاتصال مطلوبة.", + "connect-timeout-range": "يجب أن تكون مهلة الاتصال في نطاق من 1 إلى 200.", + "client-id": "معرف العميل", + "client-id-required": "معرف العميل مطلوب.", + "client-id-hint": "تلميح: اختياري. اتركه فارغًا للحصول على معرف عميل مولد تلقائيًا. كن حذرًا عند تحديد معرف العميل. سيمنع معظم خوادم MQTT الاتصالات المتعددة بنفس معرف العميل. للاتصال بمثل هذه الخوادم ، يجب أن يكون معرف عميل MQTT الخاص بك فريدًا.", + "max-bytes-in-message": "الحد الأقصى للبيانات في الرسالة", + "max-bytes-in-message-range": "يجب أن تكون الحد الأقصى للبيانات في الرسالة في نطاق من 1 إلى 256000000.", + "device-id": "معرف الجهاز", + "device-id-required": "معرف الجهاز مطلوب.", + "device-id-range": "يجب أن يكون معرف الجهاز في نطاق من 1 إلى 65535 حرفًا.", + "device-id-pattern": "يجب أن يتكون معرف الجهاز من أرقام وأحرف كبيرة وصغيرة. [MQTT-3.1.3-5]", + "group-id": "معرف المجموعة", + "group-id-required": "معرف المجموعة مطلوب.", + "topics": "المواضيع", + "topics-required": "الموضوع مطلوب.", + "routing-keys": "مفاتيح التوجيه", + "routing-keys-required": "مفتاح التوجيه مطلوب.", + "queues": "الطوابير", + "queues-required": "اسم الطابور مطلوب.", + "durable": "دائم", + "exclusive": "حصري", + "autoDelete": "حذف تلقائي", + "exchange-name": "اسم التبادل", + "exchange-name-required": "اسم التبادل مطلوب.", + "downlink-topic": "موضوع الاتصال الهابط", + "connection-timeout": "مهلة الاتصال، مللي ثانية", + "connection-timeout-min": "قيمة غير صالحة لمهلة الاتصال.", + "handshake-timeout": "مهلة المصافحة، مللي ثانية", + "handshake-timeout-min": "قيمة غير صالحة لمهلة المصافحة.", + "virtual-host": "المضيف الافتراضي", + "rabbit-mq-poll-interval": "فاصل استطلاع، مللي ثانية", + "rabbit-mq-poll-interval-min": "قيمة غير صالحة لفاصل الاستطلاع.", + "application-server-url": "رابط خادم التطبيق", + "application-server-url-required": "رابط خادم التطبيق مطلوب.", + "application-server-api-token": "رمز API لخادم التطبيق", + "application-server-api-token-required": "رمز API لخادم التطبيق مطلوب.", + "use-api-four-plus": "استخدم واجهة برمجة التطبيقات لـ ChirpStack 4+", + "bootstrap-servers": "خوادم البدء", + "bootstrap-servers-required": "خوادم البدء مطلوبة.", + "poll-interval": "فاصل الاستطلاع", + "poll-interval-required": "فاصل الاستطلاع مطلوب.", + "poll-interval-min-value": "لا يمكن أن يكون قيمة فاصل الاستطلاع أقل من 1", + "auto-create-topics": "إنشاء المواضيع تلقائيًا", + "clean-session": "جلسة نظيفة", + "enable-ssl": "تمكين SSL", + "credentials": "بيانات الاعتماد", + "credentials-type": "نوع بيانات الاعتماد", + "credentials-type-required": "نوع بيانات الاعتماد مطلوب.", + "username": "اسم المستخدم", + "username-required": "اسم المستخدم مطلوب.", + "password": "كلمة المرور", + "password-required": "كلمة المرور مطلوبة.", + "destination-shared-secret": "سر المشاركة المقصد", + "destination-shared-secret-required": "سر المشاركة المقصد مطلوب", + "customer-id": "معرف العميل", + "grip-tenant-id": "معرف المستأجر GRIP", + "api-id": "معرف الواجهة", + "azure-ca-cert": "ملف شهادة CA", + "ca-cert": "ملف شهادة CA", + "private-key": "ملف المفتاح الخاص", + "private-key-password": "كلمة مرور المفتاح الخاص", + "cert": "ملف الشهادة", + "no-file": "لم يتم تحديد ملف.", + "drop-file": "أفلق ملفًا أو انقر لتحديد ملف لتحميله.", + "drop-file-or": "اسحب وأفلق ملفًا أو", + "check-connection": "تحقق من الاتصال", + "check-success": "تم إنشاء الاتصال بنجاح.", + "topic-filters": "مرشحات الموضوع", + "remove-topic-filter": "إزالة مرشح الموضوع", + "add-topic-filter": "إضافة مرشح الموضوع", + "add-topic-filter-prompt": "يرجى إضافة مرشح الموضوع", + "topic": "الموضوع", + "mqtt-qos": "جودة الخدمة", + "mqtt-qos-required": "جودة الخدمة مطلوبة", + "mqtt-qos-range": "تتراوح قيم جودة الخدمة من 0 إلى 2", + "mqtt-qos-at-most-once": "في الغالب مرة واحدة", + "mqtt-qos-at-least-once": "على الأقل مرة واحدة", + "mqtt-qos-exactly-once": "بالضبط مرة واحدة", + "downlink-topic-pattern": "نمط موضوع الاتصال الهابط", + "downlink-topic-pattern-required": "نمط موضوع الاتصال الهابط مطلوب.", + "retained-message": "الرسالة المُحتفظ بها", + "aws-access-key-id": "معرف مفتاح الوصول إلى AWS", + "aws-secret-access-key": "مفتاح الوصول السري لـ AWS", + "aws-region": "المنطقة", + "aws-iot-endpoint": "نقطة النهاية لخدمة AWS IoT", + "aws-iot-endpoint-required": "نقطة النهاية لخدمة AWS IoT مطلوبة.", + "aws-iot-credentials": "بيانات اعتماد AWS IoT", + "aws-sqs-polling-period-in-seconds": "فترة الاستطلاع في ثوانٍ", + "aws-sqs-queue-url": "عنوان URL لطابور SQS", + "aws-sqs-queue-url-required": "عنوان URL لطابور SQS مطلوب", + "aws-sqs-access-key-id-required": "معرف مفتاح الوصول مطلوب", + "aws-sqs-secret-access-key-required": "مفتاح الوصول السري مطلوب", + "application-credentials": "بيانات اعتماد التطبيق", + "api-key": "مفتاح API", + "api-key-required": "مفتاح API مطلوب.", + "api-key-format": "تنسيق مفتاح API غير صالح.", + "auth-token": "رمز المصادقة", + "auth-token-required": "رمز المصادقة مطلوب.", + "region": "المنطقة", + "region-required": "المنطقة مطلوبة.", + "application-id": "معرف التطبيق", + "application-id-required": "معرف التطبيق مطلوب.", + "access-id": "معرف الوصول", + "access-id-required": "معرف الوصول مطلوب.", + "access-key": "مفتاح الوصول", + "access-key-required": "مفتاح الوصول مطلوب.", + "access-key-min-length": "مفتاح الوصول قصير جدًا.", + "access-key-max-length": "مفتاح الوصول طويل جدًا.", + "connection-parameters": "معلمات الاتصال", + "service-bus-namespace-name": "اسم مساحة أسماء خدمة الحافلة", + "service-bus-namespace-name-required": "اسم مساحة أسماء خدمة الحافلة مطلوب.", + "connection-string": "سلسلة الاتصال", + "consumer-group": "مجموعة المستهلكين", + "connection-string-required": "سلسلة الاتصال مطلوبة!", + "event-hub-name": "اسم نقطة النهاية للحدث", + "event-hub-name-required": "اسم نقطة النهاية للحدث مطلوب.", + "event-iot-hub-name-required": "اسم Hub IoT مطلوب للاتصال الهابط", + "sas-key-name": "اسم مفتاح SAS", + "sas-key-name-required": "اسم مفتاح SAS مطلوب.", + "sas-key": "مفتاح SAS", + "sas-key-required": "مفتاح SAS مطلوب.", + "iot-hub-name": "اسم Hub IoT (مطلوب للاتصال الهابط)", + "hostname": "اسم المضيف", + "hostname-required": "اسم المضيف مطلوب", + "integration-clazz": "فئة التكامل", + "integration-clazz-required": "فئة التكامل مطلوبة", + "integration-configuration": "تكوين JSON للتكامل", + "metadata": "البيانات الوصفية", + "type": "نوع التكامل", + "select-integration-type": "حدد نوع التكامل", + "type-required": "نوع التكامل مطلوب.", + "type-not-found": "نوع التكامل غير موجود", + "uplink-converter": "محول البيانات الصاعدة", + "uplink-converter-required": "محول البيانات الصاعدة مطلوب.", + "downlink-converter": "محول البيانات الهابطة", + "type-http": "HTTP", + "type-http-description": "بروتوكول نقل الملفات النصي أو متعدد الوسائط.", + "type-ocean-connect": "OceanConnect", + "type-ocean-connect-description": "تكامل الأجهزة ذات القدرات المختلفة مع منصة الإنترنت من الأشياء.", + "type-sigfox": "SigFox", + "type-sigfox-description": "بروتوكول خفيف الوزن للتعامل مع عمليات نقل البيانات الصغيرة.", + "type-thingpark": "ThingPark", + "type-thingpark-description": "بروتوكول النقل الموجه نحو الرسائل.", + "type-loriot": "Loriot", + "type-loriot-description": "بنية LoRaWAN الموزعة.", + "type-thingpark-enterprise": "ThingParkEnterprise", + "type-thingpark-enterprise-description": "منصة الرسائل للشبكة LPWA.", + "type-tmobile-iot-cdp": "iotcreators.com (T-Mobile – IoT CDP)", + "type-tmobile-iot-cdp-description": "خدمة الاتصال بالإنترنت IoT للجوال.", + "type-mqtt": "MQTT", + "type-mqtt-description": "بروتوكول شبكة الآلات إلى الآلات.", + "type-aws-iot": "AWS IoT", + "type-aws-iot-description": "خدمة الإدارة والتخزين والتحليل.", + "type-aws-sqs": "AWS SQS", + "type-aws-sqs-description": "خدمة إدارة الرسائل المُدارة بالكامل.", + "type-aws-kinesis": "AWS Kinesis", + "type-aws-kinesis-description": "خدمة للفيديو والبيانات في الوقت الفعلي.", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ibm-watson-iot-description": "خدمة مُدارة بالكامل على السحابة.", + "type-ttn": "The Things Stack Community", + "type-ttn-description": "شبكة LoRaWAN المفتوحة والمركزة.", + "type-tti": "The Things Stack Industries", + "type-tti-description": "برنامج لتشغيل شبكات LoRaWAN الخاصة بنطاق واسع.", + "type-chirpstack": "ChirpStack", + "type-chirpstack-description": "خادم تكوين شبكة LoRaWAN.", + "type-particle": "Particle", + "type-particle-description": "منصة تمكين IoT كخدمة.", + "type-kpn": "KPN Things", + "type-kpn-description": "منصة IoT المقدمة من KPN، تقدم حلولًا قوية للتوصيل وإدارة البيانات لأجهزة IoT.", + "type-azure-event-hub": "Azure Event Hub", + "type-azure-event-hub-description": "منصة بيانات كبيرة للتدفق وخدمة استيعاب الأحداث.", + "type-azure-iot-hub": "Azure IoT Hub", + "type-azure-iot-hub-description": "خدمة الرسائل المُستضافة في السحابة.", + "type-opc-ua": "OPC-UA", + "type-opc-ua-description": "منصة عبر النطاق لتبادل البيانات من الحساسات إلى تطبيقات السحابة.", + "type-custom": "مخصص", + "type-coap": "CoAP", + "type-coap-description": "بروتوكول شبكة الآلات إلى الآلات للأجهزة المقيدة.", + "type-udp": "UDP", + "type-udp-description": "بروتوكول النقل الموجه نحو الرسائل.", + "type-tcp": "TCP", + "type-tcp-description": "بروتوكول موجه نحو الاتصال.", + "type-kafka": "Kafka", + "type-kafka-description": "منصة لتحليل البيانات بالتدفق وتكامل البيانات.", + "type-rabbitmq": "RabbitMQ", + "type-rabbitmq-description": "سمسار الرسائل الذي يدعم عدة بروتوكولات للرسائل.", + "type-pubsub": "Pub/Sub", + "type-pubsub-description": "خدمة للرسائل اللازمة بين التطبيقات بشكل غير متزامن.", + "type-apache-pulsar": "Apache Pulsar", + "type-apache-pulsar-description": "منصة الرسائل العامة والتدفق الموزعة.", + "type-tuya": "Tuya", + "type-tuya-description": "رسائل مستضافة في السحابة. منصة الجهاز.", + "type-azure-service-bus": "Azure Service Bus", + "type-azure-service-bus-description": "سمسار الرسائل الشركية المدارة بالكامل مع طوابير الرسائل ومواضيع النشر والاشتراك.", + "opc-ua-application-name": "اسم التطبيق", + "opc-ua-application-uri": "عنوان التطبيق URI", + "opc-ua-scan-period-in-seconds": "فترة المسح بالثواني", + "opc-ua-scan-period-in-seconds-required": "الفترة المسحية مطلوبة", + "opc-ua-timeout": "المهلة الزمنية بالميلي ثانية", + "opc-ua-timeout-required": "المهلة الزمنية مطلوبة", + "opc-ua-security": "الأمان", + "opc-ua-security-required": "الأمان مطلوب", + "opc-ua-identity": "الهوية", + "opc-ua-identity-required": "الهوية مطلوبة", + "opc-ua-keystore": "مخزن المفاتيح", + "add-opc-ua-keystore-prompt": "الرجاء إضافة ملف مخزن المفاتيح", + "opc-ua-keystore-required": "مخزن المفاتيح مطلوب", + "opc-ua-type": "النوع", + "opc-ua-keystore-type": "النوع", + "opc-ua-keystore-type-required": "النوع مطلوب", + "opc-ua-keystore-location": "الموقع *", + "opc-ua-keystore-password": "كلمة المرور", + "opc-ua-keystore-password-required": "كلمة المرور مطلوبة", + "opc-ua-keystore-alias": "اسم الرمز", + "opc-ua-keystore-alias-required": "الرمز مطلوب", + "opc-ua-keystore-key-password": "كلمة مرور المفتاح", + "opc-ua-keystore-key-password-required": "كلمة مرور المفتاح مطلوبة", + "opc-ua-mapping": "التصفيف", + "add-opc-ua-mapping-prompt": "الرجاء إضافة تصفيف", + "opc-ua-mapping-type": "نوع التصفيف", + "opc-ua-mapping-type-required": "نوع التصفيف مطلوب", + "opc-ua-device-node-pattern": "نمط الجهاز الفرعي", + "opc-ua-device-node-pattern-required": "نمط الجهاز الفرعي مطلوب", + "opc-ua-namespace": "مساحة الأسماء", + "opc-ua-add-map": "إضافة عنصر التصفيف", + "kinesis-stream-name": "اسم التيار", + "kinesis-stream-name-required": "اسم التيار مطلوب", + "kinesis-region": "المنطقة", + "kinesis-region-required": "المنطقة مطلوبة", + "kinesis-access-key-id": "معرف مفتاح الوصول", + "kinesis-access-key-id-required": "معرف مفتاح الوصول مطلوب", + "kinesis-secret-access-key": "مفتاح الوصول السري", + "kinesis-secret-access-key-required": "مفتاح الوصول السري مطلوب", + "kinesis-use-consumers-with-enhanced-fan-out": "استخدم المستهلكين مع تعزيز النافذة", + "kinesis-use-credentials-from-instance-metadata": "استخدم بيانات اعتماد من خدمة معاد بيانات الواجهة EC2 من Amazon", + "kinesis-application-name": "اسم التطبيق (بشكل افتراضي يساوي اسم التيار)", + "kinesis-initial-position-in-stream": "الموضع الأولي في التيار", + "kinesis-initial-position-in-stream-required": "الموضع الأولي في التيار مطلوب", + "kinesis-max-records": "الحد الأقصى للسجلات", + "kinesis-max-records-required": "الحد الأقصى للسجلات مطلوب", + "kinesis-max-records-length-range": "يجب أن يكون طول السجل الأقصى في نطاق من 1 إلى 10000", + "kinesis-request-timeout": "مهلة زمنية للطلب بالثواني", + "kinesis-request-timeout-required": "مهلة الطلب مطلوبة", + "other-properties": "خصائص أخرى", + "subscription-tags": "علامات الاشتراك", + "remove-subscription-tag": "إزالة علامة الاشتراك", + "add-subscription-tag": "إضافة علامة الاشتراك", + "add-subscription-tag-prompt": "الرجاء إضافة علامة الاشتراك", + "key": "مفتاح", + "path": "المسار", + "required": "مطلوب", + "integration-key": "مفتاح التكامل", + "copy-integration-key": "نسخ مفتاح التكامل", + "integration-key-copied-message": "تم نسخ مفتاح التكامل إلى الحافظة", + "integration-secret": "سر التكامل", + "copy-integration-secret": "نسخ سر التكامل", + "integration-secret-copied-message": "تم نسخ سر التكامل إلى الحافظة", + "execute-remotely": "تنفيذ عن بُعد", + "remote": "بُعد", + "handler-configuration": "تكوين المعالج", + "handler-configuration-type": "نوع المعالج", + "so-broadcast": "تمكين البث - التكامل سيقبل حزم العنوان العام", + "so-keepalive-option": "تمكين إرسال رسائل البقاء على الاتصال في مقابس التوجيه", + "so-reuse-addr": "ربط العملية بمنفذ", + "tcp-no-delay": "يجبر المقبس على إرسال البيانات دون تخزينها (تعطيل خوارزمية التخزين المؤقت Nagle)", + "fail-fast": "إرسال استثناء عندما يلاحظ فك الترميز أن طول الإطار سيتجاوز الحد الأقصى", + "strip-delimiter": "شطب الفاصل", + "length-field-offset": "إزاحة حقل الطول", + "length-field-offset-required": "إزاحة حقل الطول مطلوبة.", + "length-field-offset-range": "يجب أن تكون إزاحة حقل الطول في نطاق من 0 إلى 8.", + "length-field-length": "طول حقل الطول", + "length-field-length-required": "طول حقل الطول مطلوب.", + "length-field-length-range": "يجب أن يكون طول حقل الطول في نطاق من 0 إلى 8.", + "length-adjustment": "ضبط الطول (القيمة التعويضية لإضافتها إلى قيمة حقل الطول)", + "length-adjustment-required": "ضبط الطول مطلوب.", + "length-adjustment-range": "يجب أن يكون ضبط الطول في نطاق من 0 إلى 8.", + "byte-order": "ترتيب البايت لحقل الطول", + "initial-bytes-to-strip": "عدد البايتات الأولى لشطبها من الإطار المفكك", + "initial-bytes-to-strip-required": "عدد البايتات الأولى لشطبها من الإطار المفكك مطلوب.", + "initial-bytes-to-strip-range": "يجب أن يكون عدد البايتات الأولى لشطبها من الإطار المفكك في نطاق من 0 إلى 8.", + "so-backlog-option": "الحد الأقصى لعدد الاتصالات المعلقة على المقبس", + "so-backlog-option-required": "الحد الأقصى لعدد الاتصالات المعلقة على المقبس مطلوب.", + "so-backlog-option-range": "يجب أن يكون الحد الأقصى لعدد الاتصالات المعلقة على المقبس في نطاق من 1 إلى 65535.", + "so-rcv-buf": "حجم الذاكرة المؤقتة للمقبس الواردة (بالكيلوبايت)", + "so-rcv-buf-required": "حجم الذاكرة المؤقتة للمقبس الواردة (بالكيلوبايت) مطلوب.", + "so-rcv-buf-range": "يجب أن يكون حجم الذاكرة المؤقتة للمقبس الواردة (بالكيلوبايت) في نطاق من 1 إلى 65535.", + "so-snd-buf": "حجم الذاكرة المؤقتة للمقبس الصادرة (بالكيلوبايت)", + "so-snd-buf-required": "حجم الذاكرة المؤقتة للمقبس الصادرة (بالكيلوبايت) مطلوب.", + "so-snd-buf-range": "يجب أن يكون حجم الذاكرة المؤقتة للمقبس الصادرة (بالكيلوبايت) في نطاق من 1 إلى 65535.", + "charset-name": "اسم الترميز الحرفي", + "charset-name-required": "اسم الترميز الحرفي مطلوب.", + "message-separator": "فاصل الرسالة", + "message-separator-required": "فاصل الرسالة مطلوب.", + "character-sequence": "تسلسل الحرف", + "character-sequence-required": "تسلسل الحرف مطلوب.", + "max-frame-length": "الحد الأقصى لطول الإطار (بالبايت)", + "max-frame-length-required": "الحد الأقصى لطول الإطار (بالبايت) مطلوب.", + "max-frame-length-range": "يجب أن يكون الحد الأقصى لطول الإطار (بالبايت) في نطاق من 1 إلى 65535.", + "handler-type": "نوع المعالج", + "message-size": "حجم الرسالة", + "message-size-required": "حجم الرسالة مطلوب.", + "service-url": "عنوان URL للخدمة", + "service-url-required": "عنوان URL للخدمة مطلوب.", + "subscription-name": "اسم الاشتراك", + "subscription-name-required": "اسم الاشتراك مطلوب.", + "max-num-messages": "الحد الأقصى لعدد الرسائل", + "max-num-messages-required": "الحد الأقصى لعدد الرسائل مطلوب.", + "max-num-bytes": "الحد الأقصى لعدد البايتات", + "max-num-bytes-required": "الحد الأقصى لعدد البايتات مطلوب.", + "timeout-in-ms": "المهلة الزمنية بالميلي ثانية", + "timeout-in-ms-required": "المهلة الزمنية بالميلي ثانية مطلوبة.", + "user-id": "معرف المستخدم", + "user-id-required": "معرف المستخدم مطلوب.", + "token": "رمز", + "token-required": "الرمز مطلوب.", + "project-id": "معرف المشروع", + "project-id-required": "معرف المشروع مطلوب.", + "subscription-id": "معرف الاشتراك", + "subscription-id-required": "معرف الاشتراك مطلوب.", + "service-account-key": "ملف مفتاح حساب الخدمة", + "service-account-key-required": "ملف مفتاح حساب الخدمة مطلوب.", + "tcp": { + "system-line-separator": "فاصل السطر النظامي", + "nul-delimiter": "محدد NUL", + "byte-order-little-endian": "ترتيب البايتات الصغيرة", + "byte-order-big-endian": "ترتيب البايتات الكبيرة" + }, + "cache-size": "حجم الذاكرة المؤقتة", + "cache-time-to-live": "مدة حياة البيانات في الذاكرة المؤقتة بالدقائق", + "min-cache-size": "لا يمكن أن يكون حجم الذاكرة المؤقتة أقل من 0", + "min-cache-time-to-live": "لا يمكن أن تكون مدة حياة البيانات في الذاكرة المؤقتة أقل من 0", + "max-cache-time-to-live": "مدة حياة البيانات في الذاكرة المؤقتة غير صالحة، يرجى تحديد قيمة بين 0 و 525600", + "coap-security-mode": "وضع الأمان في CoAP", + "coap-security-mode-required": "وضع الأمان في CoAP مطلوب", + "coap-security-mode-no-secure": "غير آمن", + "coap-security-mode-dtls": "DTLS", + "coap-security-mode-mixed": "مختلط", + "coap-endpoint": "عنوان URL النقطة النهائية لـ CoAP", + "coap-endpoint-url-copied-message": "تم نسخ عنوان URL النقطة النهائية لـ CoAP إلى الحافظة", + "copy-coap-endpoint-url": "نسخ عنوان URL النقطة النهائية لـ CoAP", + "copy-coap-dtls-endpoint-url": "نسخ عنوان URL النقطة النهائية لـ CoAP DTLS", + "coap-dtls-base-url": "عنوان URL الأساسي لـ DTLS", + "coap-dtls-base-url-required": "عنوان URL الأساسي لـ DTLS مطلوب", + "coap-dtls-endpoint": "عنوان URL النقطة النهائية لـ CoAP DTLS", + "coap-dtls-endpoint-url-copied-message": "تم نسخ عنوان URL النقطة النهائية لـ CoAP DTLS إلى الحافظة", + "unassign-integration-title": "هل أنت متأكد من رغبتك في إلغاء تعيين التكامل '{{integrationName}}'؟", + "unassign-integration-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين التكامل ولن يكون متاحًا للحافة.", + "unassign-integrations-from-edge-title": "هل أنت متأكد من رغبتك في إلغاء تعيين { count, plural, =1 {1 تكامل} other {# تكاملات} }؟", + "unassign-integrations-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين جميع التكاملات المحددة ولن تكون متاحة للحافة.", + "unassign-integrations": "إلغاء تعيين التكاملات", + "edge-placeholder-hint": "يمكنك استخدام العنصر النائب ${{ATTRIBUTE_KEY}} لاستبدال حقل التكامل بقيمة السمة من كيان الحافة المحدد. على سبيل المثال، 'Edge A' لديه سمة 'baseUrl' تساوي 'http://localhost:9999'. يمكنك تعيين ${{baseUrl}} كأحد حقول التكامل، وسيتم استبداله بـ 'http://localhost:9999' أثناء تعيين هذا التكامل لـ 'Edge A'. بالإضافة إلى ذلك، إذا تم تحديث سمة 'baseUrl' لـ 'Edge A' - سيتم إنشاء تكامل مع القيمة المحدثة تلقائيًا لـ 'Edge A'.", + "status": { + "status": "الحالة", + "active": "نشط", + "disabled": "معطل", + "failed": "فشل", + "pending": "قيد الانتظار" + }, + "daily-activity": "النشاط اليومي", + "enable-debug-mode": "تمكين وضع التصحيح", + "disable-debug-mode": "تعطيل وضع التصحيح", + "advanced-settings": "الإعدادات المتقدمة", + "basic-settings": "الإعدادات الأساسية", + "existing-converter": "تحديد موجود", + "new-converter": "إنشاء جديد", + "connection": "الاتصال", + "connected": "متصل", + "not-connected": "غير متصل", + "environment": "البيئة", + "environment-required": "البيئة مطلوبة.", + "region-cn": "الصين", + "region-us": "الولايات المتحدة", + "region-eu": "أوروبا", + "region-in": "الهند", + "topic-name": "اسم الموضوع", + "topic-name-required": "اسم الموضوع مطلوب", + "sub-name": "اسم الاشتراك", + "sub-name-required": "اسم الاشتراك مطلوب", + "downlink-connection-string": "سلسلة اتصال التنزيل", + "downlink-connection-string-required": "سلسلة اتصال التنزيل مطلوبة", + "downlink-topic-name": "اسم موضوع التنزيل", + "downlink-topic-name-required": "اسم موضوع التنزيل مطلوب" + }, + "item": { + "selected": "محدد" + }, + "js-func": { + "no-return-error": "يجب على الوظيفة إرجاع قيمة!", + "return-type-mismatch": "يجب على الوظيفة إرجاع قيمة من النوع '{{type}}'!", + "tidy": "تنظيم", + "mini": "مصغر" + }, + "key-val": { + "key": "المفتاح", + "value": "القيمة", + "remove-entry": "إزالة الإدخال", + "add-entry": "إضافة إدخال", + "no-data": "لا توجد بيانات" + }, + "layout": { + "layout": "التخطيط", + "layouts": "التخطيطات", + "manage": "إدارة التخطيطات", + "settings": "إعدادات التخطيط", + "color": "اللون", + "main": "الرئيسي", + "right": "اليمين", + "left": "اليسار", + "select": "تحديد التخطيط المستهدف", + "percentage-width": "العرض بالنسبة المئوية (%)", + "fixed-width": "العرض الثابت (بكسل)", + "left-width": "نسبة عرض الجهة اليسرى (%)", + "right-width": "نسبة عرض الجهة اليمنى (%)", + "pick-fixed-side": "الجانب الثابت: ", + "layout-fixed-width": "العرض الثابت (بكسل)", + "value-min-error": "يجب أن تكون القيمة أكبر من {{min}}{{unit}}", + "value-max-error": "يجب أن تكون القيمة أقل من {{max}}{{unit}}", + "layout-fixed-width-required": "العرض الثابت مطلوب", + "right-width-percentage-required": "النسبة المئوية اليمنى مطلوبة", + "left-width-percentage-required": "النسبة المئوية اليسرى مطلوبة", + "divider": "فاصل", + "right-side": "تخطيط الجهة اليمنى", + "left-side": "تخطيط الجهة اليسرى" + }, + "legend": { + "direction": "الاتجاه", + "position": "الموضع", + "show-values": "إظهار القيم", + "min-option": "الحد الأدنى", + "max-option": "الحد الأقصى", + "average-option": "المتوسط", + "total-option": "الإجمالي", + "latest-option": "الأحدث", + "sort-legend": "فرز مفاتيح البيانات في الأسطورة", + "show-max": "إظهار القيمة القصوى", + "show-min": "إظهار القيمة الدنيا", + "show-avg": "إظهار القيمة المتوسطة", + "show-total": "إظهار القيمة الإجمالية", + "show-latest": "إظهار القيمة الأحدث", + "settings": "إعدادات الأسطورة", + "min": "الحد الأدنى", + "max": "الحد الأقصى", + "avg": "المتوسط", + "total": "الإجمالي", + "latest": "الأحدث", + "comparison-time-ago": { + "previousInterval": "(الفاصل الزمني السابق)", + "customInterval": "(الفاصل الزمني المخصص)", + "days": "(قبل يوم)", + "weeks": "(قبل أسبوع)", + "months": "(قبل شهر)", + "years": "(قبل سنة)" + } + }, + "login": { + "login": "تسجيل الدخول", + "request-password-reset": "طلب إعادة تعيين كلمة المرور", + "reset-password": "إعادة تعيين كلمة المرور", + "create-password": "إنشاء كلمة مرور", + "two-factor-authentication": "المصادقة الثنائية العاملة", + "passwords-mismatch-error": "يجب أن تكون كلمات المرور المدخلة متطابقة!", + "password-again": "إعادة كلمة المرور", + "sign-in": "الرجاء تسجيل الدخول", + "username": "اسم المستخدم (البريد الإلكتروني)", + "remember-me": "تذكرني", + "forgot-password": "نسيت كلمة المرور؟", + "password-reset": "إعادة تعيين كلمة المرور", + "expired-password-reset-message": "انتهت صلاحية بيانات اعتمادك! يرجى إنشاء كلمة مرور جديدة.", + "new-password": "كلمة المرور الجديدة", + "new-password-again": "تأكيد كلمة المرور الجديدة", + "password-link-sent-message": "تم إرسال رابط إعادة التعيين", + "email": "البريد الإلكتروني", + "no-account": "لا تمتلك حساب؟", + "create-account": "إنشاء حساب", + "login-with": "تسجيل الدخول باستخدام {{name}}", + "or": "أو", + "error": "خطأ في تسجيل الدخول", + "verify-your-identity": "تحقق من هويتك", + "select-way-to-verify": "حدد طريقة التحقق", + "resend-code": "إعادة إرسال الرمز", + "resend-code-wait": "إعادة إرسال الرمز في { time, plural, =1 {1 ثانية} other {# ثواني} }", + "try-another-way": "جرب طريقة أخرى", + "totp-auth-description": "الرجاء إدخال رمز الأمان من تطبيق المصادقة الخاص بك.", + "totp-auth-placeholder": "الرمز", + "sms-auth-description": "تم إرسال رمز أمان إلى هاتفك على {{contact}}.", + "sms-auth-placeholder": "رمز الرسالة القصيرة", + "email-auth-description": "تم إرسال رمز أمان إلى عنوان البريد الإلكتروني الخاص بك على {{contact}}.", + "email-auth-placeholder": "رمز البريد الإلكتروني", + "backup-code-auth-description": "الرجاء إدخال أحد رموز الاحتياط الخاصة بك.", + "backup-code-auth-placeholder": "رمز الاحتياط" + }, + "signup": { + "firstname": "الاسم الأول", + "lastname": "الاسم الأخير", + "email": "البريد الإلكتروني", + "signup": "التسجيل", + "create-password": "إنشاء كلمة مرور", + "repeat-password": "تكرار كلمة المرور", + "have-account": "هل لديك حساب بالفعل؟", + "signin": "تسجيل الدخول", + "no-captcha-message": "يجب عليك التأكيد بأنك لست روبوتًا", + "password-length-message": "يجب أن تتكون كلمة المرور الخاصة بك من ما لا يقل عن 6 أحرف", + "email-verification": "تحقق من البريد الإلكتروني", + "email-verification-message": "تم إرسال بريد إلكتروني يحتوي على تفاصيل التحقق إلى العنوان البريدي المحدد.
يرجى اتباع التعليمات المقدمة في البريد الإلكتروني لإكمال إجراء التسجيل الخاص بك.
ملاحظة: إذا لم تر البريد الإلكتروني لفترة من الوقت ، يرجى التحقق من مجلد 'البريد العشوائي' أو محاولة إعادة إرسال البريد الإلكتروني بالنقر فوق زر 'إعادة إرسال'.", + "account-activation-title": "تفعيل الحساب", + "account-activated": "تم تفعيل الحساب بنجاح!", + "account-activated-text": "تهانينا!
تم تفعيل حسابك.", + "resend": "إعادة الإرسال", + "inactive-user-exists-title": "المستخدم غير النشط موجود بالفعل", + "inactive-user-exists-text": "هناك مستخدم مسجل بالفعل بعنوان بريد إلكتروني غير مؤكد.
انقر فوق الزر 'إعادة الإرسال' إذا كنت ترغب في إعادة إرسال بريد التحقق.", + "activating-account": "تفعيل الحساب...", + "activating-account-text": "حسابك يقوم بالتفعيل حاليًا. يرجى الانتظار...", + "accept-privacy-policy": "قبول سياسة الخصوصية", + "accept": "قبول", + "privacy-policy": "سياسة الخصوصية", + "accept-privacy-policy-message": "يجب عليك قبول سياسة الخصوصية الخاصة بنا", + "recaptcha-title": "reCAPTCHA", + "terms-of-use": "شروط الاستخدام", + "accept-terms-of-use-message": "يجب عليك قبول شروط الاستخدام الخاصة بنا" + }, + "notification": { + "action-button": "زر الإجراء", + "action-type": "نوع الإجراء", + "active": "نشط", + "add-notification-recipients-group": "إضافة مجموعة مستلمي الإشعارات", + "add-notification-template": "إضافة قالب إشعار", + "add-recipient": "إضافة مستلم", + "add-recipients": "إضافة المستلمين", + "add-rule": "إضافة قاعدة", + "add-stage": "إضافة مرحلة", + "add-template": "إضافة قالب", + "after": "بعد", + "alarm-assignment-trigger-settings": "إعدادات تنبيه تعيين المهمة", + "alarm-comment-trigger-settings": "إعدادات تنبيه تعليق التنبيه", + "alarm-trigger-settings": "إعدادات تنبيه التنبيه", + "all": "الكل", + "api-feature-hint": "إذا كانت الحقل فارغًا ، فسيتم تطبيق المشغل على جميع ميزات API", + "api-usage-trigger-settings": "إعدادات مشغل استخدام API", + "new-platform-version-trigger-settings": "إعدادات مشغل الإصدار الجديد للمنصة", + "rate-limits-trigger-settings": "إعدادات مشغل تجاوز حدود السرعة", + "at-least-one-should-be-selected": "يجب تحديد واحد على الأقل", + "basic-settings": "الإعدادات الأساسية", + "button-text": "نص الزر", + "button-text-required": "نص الزر مطلوب", + "button-text-max-length": "يجب أن يكون نص الزر أقل من أو يساوي {{ length }} حرفًا", + "compose": "تأليف", + "conversation": "المحادثة", + "conversation-required": "المحادثة مطلوبة", + "copy-notification-template": "نسخ قالب الإشعار", + "copy-rule": "نسخ القاعدة", + "copy-template": "نسخ القالب", + "create-new": "إنشاء جديد", + "created": "تم الإنشاء", + "delete-notification-text": "كن حذرًا ، بعد التأكيد سيصبح الإشعار غير قابل للاسترداد.", + "delete-notification-title": "هل أنت متأكد أنك تريد حذف الإشعار؟", + "delete-notifications-text": "كن حذرًا ، بعد التأكيد ستصبح الإشعارات غير قابلة للاسترداد.", + "delete-notifications-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 إشعار} other {# إشعارات} }؟", + "delete-recipient-text": "كن حذرًا ، بعد التأكيد سيصبح المستلم غير قابل للاسترداد.", + "delete-recipient-title": "هل أنت متأكد أنك تريد حذف المستلم '{{recipientName}}'؟", + "delete-recipients-text": "كن حذرًا ، بعد التأكيد ستصبح المستلمين غير قابلين للاسترداد.", + "delete-recipients-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 مستلم} other {# مستلمين} }؟", + "delete-request-text": "كن حذرًا ، بعد التأكيد سيصبح الطلب غير قابل للاسترداد.", + "delete-request-title": "هل أنت متأكد أنك تريد حذف الطلب؟", + "delete-requests-text": "كن حذرًا ، بعد التأكيد ستصبح الطلبات غير قابلة للاسترداد.", + "delete-requests-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 طلب} other {# طلبات} }؟", + "delete-rule-text": "كن حذرًا ، بعد التأكيد ستصبح القاعدة غير قابلة للاسترداد.", + "delete-rule-title": "هل أنت متأكد أنك تريد حذف القاعدة '{{ruleName}}'؟", + "delete-rules-text": "كن حذرًا ، بعد التأكيد ستصبح القواعد غير قابلة للاسترداد.", + "delete-rules-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 قاعدة} other {# قواعد} }؟", + "delete-template-text": "كن حذرًا ، بعد التأكيد سيصبح القالب غير قابل للاسترداد.", + "delete-template-title": "هل أنت متأكد أنك تريد حذف القالب '{{templateName}}'؟", + "delete-templates-text": "كن حذرًا ، بعد التأكيد ستصبح القوالب غير قابلة للاسترداد.", + "delete-templates-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 قالب} other {# قوالب} }؟", + "deleted": "تم الحذف", + "delivery-method": { + "delivery-method": "طريقة التسليم", + "email": "البريد الإلكتروني", + "email-preview": "معاينة إشعار البريد الإلكتروني", + "slack": "سلاك", + "slack-preview": "معاينة إشعار سلاك", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "معاينة إشعار Microsoft Teams", + "sms": "رسالة قصيرة", + "sms-preview": "معاينة إشعار الرسائل القصيرة", + "web": "الويب", + "web-preview": "معاينة إشعار الويب" + }, + "delivery-method-not-configure-click": "لم يتم تكوين طريقة التسليم. انقر للإعداد.", + "delivery-method-not-configure-contact": "لم يتم تكوين طريقة التسليم. اتصل بمسؤول النظام الخاص بك.", + "delivery-methods": "طرق التسليم", + "description": "الوصف", + "device-activity-trigger-settings": "إعدادات مشغل النشاط الجهاز", + "device-list-rule-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع الأجهزة", + "device-profiles-list-rule-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع ملفات تعريف الجهاز", + "disabled": "معطل", + "edit-notification-recipients-group": "تحرير مجموعة مستلمي الإشعارات", + "edit-notification-template": "تحرير قالب الإشعار", + "edit-rule": "تحرير القاعدة", + "edit-template": "تحرير القالب", + "enabled": "مفعل", + "entities-limit-trigger-settings": "إعدادات مشغل حدود الكيانات", + "entity-action-trigger-settings": "إعدادات مشغل إجراء الكيان", + "entity-type": "نوع الكيان", + "escalation-chain": "سلسلة التصعيد", + "failed-send": "فشل الإرسال", + "fails": "{ count, plural, =1 {1 فشل} other {# أخطاء} }", + "filter": "مرشح", + "first-recipient": "المستلم الأول", + "inactive": "غير نشط", + "inbox": "صندوق الوارد", + "notification-inbox": "الإشعارات / الوارد", + "input-field-support-templatization": "دعم الحقول الإدخال للتخطيط.", + "input-fields-support-templatization": "تدعم الحقول الإدخال التخطيط.", + "integration-action-trigger-settings": "إعدادات مشغل أحداث التكامل", + "integration-list-rule-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع التكاملات", + "link": "رابط", + "link-required": "الرابط مطلوب", + "link-type": { + "dashboard": "فتح لوحة المعلومات", + "link": "فتح رابط URL" + }, + "loading-notifications": "تحميل الإشعارات...", + "management": "إدارة الإشعارات", + "mark-all-as-read": "وضع علامة على الكل كمقروء", + "mark-as-read": "وضع علامة كمقروء", + "message": "الرسالة", + "message-required": "الرسالة مطلوبة", + "message-max-length": "يجب أن تكون الرسالة أقل من أو تساوي {{ length }} حرفًا", + "name": "الاسم", + "name-required": "الاسم مطلوب", + "new-notification": "إشعار جديد", + "no-inbox-notification": "لم يتم العثور على إشعارات", + "no-notification-request": "لا توجد طلبات إشعار", + "no-notification-templates": "لم يتم العثور على قوالب الإشعار", + "no-notifications-yet": "لا توجد إشعارات بعد", + "no-recipients-notification": "لا توجد إشعارات للمستلمين", + "no-rule": "لم يتم تكوين قاعدة", + "no-rules-notification": "لا توجد إشعارات للقواعد", + "no-severity-found": "لم يتم العثور على خطورة", + "no-severity-matching": "لم يتم العثور على '{{severity}}'.", + "no-template-matching": "لم يتم العثور على مورد مطابق لـ '{{template}}'.", + "not-found-slack-recipient": "لم يتم العثور على مستلم Slack", + "notification": "الإشعار", + "notification-center": "مركز الإشعارات", + "notify": "إعلام", + "notify-again": "إعلام مرة أخرى", + "notify-alarm-action": { + "acknowledged": "الإنذار تم الاعتراف به", + "assigned": "الإنذار تم تعيينه", + "cleared": "تم تطهير الإنذار", + "created": "تم إنشاء الإنذار", + "severity-changed": "تم تغيير خطورة الإنذار", + "unassigned": "تم إلغاء تعيين الإنذار" + }, + "notify-on": "الإعلام عند", + "notify-on-comment-update": "الإعلام عند تحديث التعليق", + "notify-on-required": "الإعلام عند مطلوب", + "notify-on-unassign": "الإعلام عند إلغاء التعيين", + "notify-only-integrations-errors": "الإعلام فقط في حالة الأخطاء", + "notify-only-user-comments": "الإعلام فقط بالتعليقات المستخدمة", + "only-rule-chain-lifecycle-failures": "فقط في حالات فشل دورة حياة سلسلة القواعد", + "only-rule-node-lifecycle-failures": "فقط في حالات فشل دورة حياة عقدة القاعدة", + "platform-users": "مستخدمو المنصة", + "rate-limits": "حدود معدل الطلبات", + "rate-limits-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع حدود معدل الطلبات", + "recipient": "المستلم", + "recipient-group": "مجموعة المستلمين", + "recipient-type": { + "affected-tenant-administrators": "مسؤولو المستأجر المتأثرون", + "affected-user": "المستخدم المتأثر", + "all-users": "جميع المستخدمين", + "customer-users": "مستخدمي العملاء", + "system-administrators": "مسؤولو النظام", + "tenant-administrators": "مسؤولو المستأجر", + "user-filters": "تصفية المستخدمين", + "user-group-list": "قائمة مجموعات المستخدمين", + "user-list": "قائمة المستخدمين", + "user-role": "دور المستخدم", + "users-entity-owner": "مستخدمو مالك الكيان" + }, + "recipients": "المستلمون", + "notification-recipients": "الإشعارات / المستلمون", + "recipients-count": "{ count, plural, =1 {1 مستلم} other {# مستلمين} }", + "recipients-required": "المستلمون مطلوبون", + "refresh-allow-delivery-method": "تحديث السماح بطريقة التسليم", + "request-search": "بحث الطلب", + "request-status": { + "processing": "معالجة", + "scheduled": "مجدول", + "sent": "مرسل" + }, + "review": "مراجعة", + "rule": "القاعدة", + "rule-chain-list-rule-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع سلاسل القواعد", + "rule-engine-events-trigger-settings": "إعدادات مشغلات أحداث محرك القواعد", + "rule-engine-filter": "تصفية محرك القواعد", + "rule-name": "اسم القاعدة", + "rule-name-required": "الاسم مطلوب", + "rule-disable": "تعطيل قاعدة الإشعار", + "rule-enable": "تمكين قاعدة الإشعار", + "rule-node-filter": "تصفية عقدة القاعدة", + "rules": "القواعد", + "notification-rules": "الإشعارات / القواعد", + "scheduler-later": "الجدول الزمني لاحقًا", + "search-notification": "بحث في الإشعارات", + "search-recipients": "البحث عن المستلمين", + "search-rules": "البحث في القواعد", + "search-templates": "البحث في القوالب", + "see-documentation": "راجع التوثيق", + "selected-notifications": "{ count, plural, =1 {1 إشعار} other {# إشعارات} } المحددة", + "selected-recipients": "{ count, plural, =1 {1 مستلم} other {# مستلمين} } المحددة", + "selected-requests": "{ count, plural, =1 {1 طلب} other {# طلبات} } المحددة", + "selected-rules": "{ count, plural, =1 {1 قاعدة} other {# قواعد} } المحددة", + "selected-template": "{ count, plural, =1 {1 قالب} other {# قوالب} } المحددة", + "send-notification": "إرسال الإشعار", + "sent": "تم الإرسال", + "notification-sent": "الإشعارات / تم الإرسال", + "set-entity-from-notification": "تعيين الكيان من الإشعار إلى حالة لوحة المعلومات", + "slack-chanel-type": "نوع قناة Slack", + "slack-chanel-types": { + "direct": "رسالة مباشرة", + "private-channel": "قناة خاصة", + "public-channel": "قناة عامة" + }, + "start-from-scratch": "ابدأ من البداية", + "status": "الحالة", + "stop-escalation-alarm-status-become": "إيقاف التصعيد عندما يصبح حالة الإنذار:", + "subject": "الموضوع", + "subject-required": "الموضوع مطلوب", + "template": "القالب", + "template-name": "اسم القالب", + "template-required": "القالب مطلوب", + "template-type": { + "alarm": "إنذار", + "alarm-assignment": "تعيين الإنذار", + "alarm-comment": "تعليق الإنذار", + "api-usage-limit": "حد استخدام واجهة برمجة التطبيقات", + "device-activity": "نشاط الجهاز", + "entities-limit": "حدود الكيانات", + "entity-action": "إجراء الكيان", + "general": "عام", + "integration-lifecycle-event": "حدث دورة حياة الدمج", + "rule-engine-lifecycle-event": "حدث دورة حياة محرك القواعد", + "rule-node": "عقدة القاعدة", + "new-platform-version": "إصدار المنصة الجديد", + "rate-limits": "تجاوز حدود المعدل" + }, + "templates": "القوالب", + "notification-templates": "الإشعارات / القوالب", + "tenant-profiles-list-rule-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع ملفات تعريف المستأجر", + "tenants-list-rule-hint": "إذا كان الحقل فارغًا ، سيتم تطبيق المشغل على جميع المستأجرين", + "threshold": "الحد الأدنى", + "theme-color": "لون السمة", + "time": "الوقت", + "track-rule-node-events": "تتبع حدث عقدة القاعدة", + "trigger": { + "alarm": "إنذار", + "alarm-assignment": "تعيين الإنذار", + "alarm-comment": "تعليق الإنذار", + "api-usage-limit": "حد استخدام واجهة برمجة التطبيقات", + "device-activity": "نشاط الجهاز", + "entities-limit": "حدود الكيانات", + "entity-action": "إجراء الكيان", + "integration-lifecycle-event": "حدث دورة حياة الدمج", + "rule-engine-lifecycle-event": "حدث دورة حياة محرك القواعد", + "new-platform-version": "إصدار المنصة الجديد", + "rate-limits": "تجاوز حدود المعدل", + "trigger": "المشغل", + "trigger-required": "المشغل مطلوب" + }, + "type": "النوع", + "unread": "غير مقروء", + "updated": "مُحدث", + "use-template": "استخدم القالب", + "view-all": "عرض الكل", + "view-notification-recipients-group": "عرض مجموعة مستلمي الإشعار", + "view-notification-template": "عرض قالب الإشعار", + "view-rule": "عرض القاعدة", + "warning": "تحذير", + "webhook-url": "رابط ويب هوك", + "webhook-url-required": "رابط ويب هوك مطلوب", + "channel-name": "اسم القناة", + "channel-name-required": "اسم القناة مطلوب", + "settings": { + "notification-settings": "إعدادات الإشعار", + "reset-all": "إعادة تعيين كل الإعدادات", + "reset-all-title": "هل أنت متأكد أنك تريد إعادة تعيين النموذج؟", + "reset-all-text": "بعد التأكيد ، سيتم إعادة تعيين نموذج الإعدادات إلى القيمة الافتراضية وحفظه.", + "type": "النوع", + "enable-all": "تمكين الكل", + "disable-all": "تعطيل الكل", + "delivery-not-configured": "طريقة التسليم غير مكونة" + } + }, + "ota-update": { + "add": "إضافة حزمة", + "assign-firmware": "تعيين البرنامج الثابت", + "assign-firmware-required": "تعيين البرنامج الثابت مطلوب", + "assign-software": "تعيين البرنامج", + "assign-software-required": "تعيين البرنامج مطلوب", + "auto-generate-checksum": "توليد تحقق تلقائي للمجموع التكميلي", + "cant-applied-group-all": "لا يمكن تطبيقه على المجموعة الكل", + "checksum": "التحقق التلقائي للمجموع التكميلي", + "checksum-hint": "إذا كان التحقق التلقائي للمجموع التكميلي فارغًا ، فسيتم توليده تلقائيًا", + "checksum-algorithm": "خوارزمية التحقق التلقائي للمجموع التكميلي", + "checksum-copied-message": "تم نسخ التحقق التلقائي للمجموع التكميلي للحزمة إلى الحافظة", + "change-firmware": "تغيير البرنامج الثابت قد يؤدي إلى تحديث { count, plural, =1 {جهاز واحد} other {# أجهزة} }.", + "change-software": "تغيير البرنامج قد يؤدي إلى تحديث { count, plural, =1 {جهاز واحد} other {# أجهزة} }.", + "chose-compatible-device-profile": "سيكون الملف الذي تم تحميله متاحًا فقط للأجهزة ذات الملف المختار.", + "chose-firmware-distributed-device": "اختر البرنامج الثابت الذي سيتم توزيعه على الأجهزة", + "chose-software-distributed-device": "اختر البرنامج الذي سيتم توزيعه على الأجهزة", + "content-type": "نوع المحتوى", + "copy-checksum": "نسخ التحقق التلقائي للمجموع التكميلي", + "copy-direct-url": "نسخ عنوان URL المباشر", + "copyId": "نسخ معرف الحزمة", + "copied": "تم النسخ!", + "delete": "حذف الحزمة", + "delete-ota-update-text": "تنبيه: بعد التأكيد، لن يمكن استعادة تحديث OTA.", + "delete-ota-update-title": "هل أنت متأكد أنك تريد حذف تحديث OTA '{{title}}'؟", + "delete-ota-updates-text": "تنبيه: بعد التأكيد، سيتم حذف جميع تحديثات OTA المحددة.", + "delete-ota-updates-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {تحديث OTA واحد} other {# تحديثات OTA} }؟", + "description": "الوصف", + "direct-url": "عنوان URL المباشر", + "direct-url-copied-message": "تم نسخ عنوان URL المباشر للحزمة إلى الحافظة", + "direct-url-required": "عنوان URL المباشر مطلوب", + "download": "تحميل الحزمة", + "drop-file": "اسحب وأسقط ملف حزمة أو انقر لتحديد ملف لتحميله.", + "drop-package-file-or": "اسحب وأسقط ملف حزمة أو", + "file-name": "اسم الملف", + "file-size": "حجم الملف", + "file-size-bytes": "حجم الملف بالبايت", + "idCopiedMessage": "تم نسخ معرف الحزمة إلى الحافظة", + "no-firmware-matching": "لم يتم العثور على حزم التحديث التلقائي للبرنامج الثابت المتوافقة مع '{{entity}}'.", + "no-firmware-text": "لم يتم توفير أي حزم تحديث تلقائي للبرنامج الثابت المتوافقة.", + "no-packages-text": "لم يتم العثور على حزم", + "no-software-matching": "لم يتم العثور على حزم التحديث التلقائي للبرنامج المتوافقة مع '{{entity}}'.", + "no-software-text": "لم يتم توفير أي حزم تحديث تلقائي للبرنامج المتوافقة.", + "ota-update": "تحديث OTA", + "ota-update-details": "تفاصيل تحديث OTA", + "ota-updates": "تحديثات OTA", + "package-file": "ملف الحزمة", + "package-type": "نوع الحزمة", + "packages-repository": "مستودع الحزم", + "search": "البحث في الحزم", + "selected-package": "{ count, plural, =1 {حزمة واحدة} other {# حزم} } محددة", + "title": "العنوان", + "title-required": "العنوان مطلوب.", + "title-max-length": "يجب أن يكون العنوان أقل من 256 حرفًا", + "types": { + "firmware": "البرنامج الثابت", + "software": "البرنامج" + }, + "upload-binary-file": "تحميل ملف ثنائي", + "use-external-url": "استخدم عنوان URL الخارجي", + "version": "الإصدار", + "version-required": "الإصدار مطلوب.", + "version-tag": "علامة الإصدار", + "version-tag-hint": "يجب أن تتطابق العلامة المخصصة مع الإصدار الذي يتم الإبلاغ عنه من قبل جهازك.", + "version-max-length": "يجب أن يكون الإصدار أقل من 256 حرفًا", + "warning-after-save-no-edit": "بمجرد تحميل الحزمة، لن تتمكن من تعديل العنوان، الإصدار، ملف ملف التعريف الخاص بالجهاز، ونوع الحزمة." + }, + "position": { + "top": "أعلى", + "bottom": "أسفل", + "left": "يسار", + "right": "يمين" + }, + "profile": { + "profile": "الملف الشخصي", + "last-login-time": "آخر تسجيل دخول", + "change-password": "تغيير كلمة المرور", + "current-password": "كلمة المرور الحالية", + "copy-jwt-token": "نسخ رمز JWT", + "jwt-token": "رمز JWT", + "token-valid-till": "الرمز صالح حتى", + "tokenCopiedSuccessMessage": "تم نسخ رمز JWT إلى الحافظة", + "tokenCopiedWarnMessage": "انتهت صلاحية رمز JWT! الرجاء، قم بتحديث الصفحة." + }, + "profiles": { + "profiles": "الملفات الشخصية" + }, + "security": { + "security": "الأمان", + "general-settings": "إعدادات الأمان العامة", + "access-token": "رمز الوصول", + "access-token-required": "رمز الوصول مطلوب", + "clientId": "معرف العميل", + "clientId-required": "معرف العميل مطلوب", + "username": "اسم المستخدم", + "username-required": "اسم المستخدم مطلوب", + "ca-cert": "شهادة CA", + "2fa": { + "2fa": "المصادقة ذات العاملين", + "2fa-description": "تحمي المصادقة ذات العاملين حسابك من الوصول غير المصرح به. كل ما عليك فعله هو إدخال رمز أمان عند تسجيل الدخول.", + "authenticate-with": "يمكنك المصادقة مع:", + "disable-2fa-provider-text": "سيجعل تعطيل {{name}} حسابك أقل أمانًا", + "disable-2fa-provider-title": "هل أنت متأكد أنك تريد تعطيل {{name}}؟", + "get-new-code": "الحصول على رمز جديد", + "main-2fa-method": "استخدم كأسلوب أساسي للمصادقة ذات العاملين", + "dialog": { + "activation-step-description-email": "في المرة القادمة التي تقوم فيها بتسجيل الدخول، سيُطلب منك إدخال الرمز الأمني الذي سيتم إرساله إلى عنوان بريدك الإلكتروني.", + "activation-step-description-sms": "في المرة القادمة التي تقوم فيها بتسجيل الدخول، سيُطلب منك إدخال الرمز الأمني الذي سيتم إرساله إلى رقم هاتفك.", + "activation-step-description-totp": "في المرة القادمة التي تقوم فيها بتسجيل الدخول، ستحتاج إلى تقديم رمز مصادقة ذو عاملين.", + "activation-step-label": "التفعيل", + "backup-code-description": "قم بطباعة الرموز حتى تتمكن من الوصول إليها عندما تحتاج إلى استخدامها لتسجيل الدخول إلى حسابك. يمكنك استخدام كل رمز احتياطي مرة واحدة.", + "backup-code-warn": "بمجرد مغادرتك لهذه الصفحة، لن يمكن عرض هذه الرموز مرة أخرى. قم بتخزينها بشكل آمن باستخدام الخيارات أدناه.", + "download-txt": "تنزيل (txt)", + "email-step-description": "أدخل بريدًا إلكترونيًا لاستخدامه كمصادق لك.", + "email-step-label": "بريد إلكتروني", + "enable-email-title": "تمكين المصادق الإلكتروني", + "enable-sms-title": "تمكين المصادق بالرسائل القصيرة", + "enable-totp-title": "تمكين تطبيق المصادق", + "enter-verification-code": "أدخل الرمز المكون من 6 أرقام هنا", + "get-backup-code-title": "الحصول على رمز احتياطي", + "next": "التالي", + "scan-qr-code": "مسح رمز الاستجابة السريعة هذا باستخدام تطبيق التحقق", + "send-code": "إرسال الرمز", + "sms-step-description": "أدخل رقم هاتف لاستخدامه كمصادق لك.", + "sms-step-label": "رقم الهاتف", + "success": "نجاح!", + "totp-step-description-install": "يمكنك تثبيت تطبيقات مثل Google Authenticator، Authy، أو Duo.", + "totp-step-description-open": "افتح تطبيق المصادق على هاتفك المحمول.", + "totp-step-label": "الحصول على التطبيق", + "verification-code": "الرمز المكون من 6 أرقام", + "verification-code-invalid": "تنسيق رمز التحقق غير صالح", + "verification-code-incorrect": "رمز التحقق غير صحيح", + "verification-code-many-request": "الكثير من الطلبات لفحص رمز التحقق", + "verification-step-description": "أدخل الرمز المكون من 6 أرقام الذي أرسلناه لـ {{address}}", + "verification-step-label": "التحقق" + }, + "provider": { + "email": "البريد الإلكتروني", + "email-description": "استخدم رمز أمان يتم إرساله إلى عنوان بريدك الإلكتروني للمصادقة.", + "email-hint": "يتم إرسال رموز المصادقة عبر البريد الإلكتروني إلى {{ info }}", + "sms": "الرسائل القصيرة", + "sms-description": "استخدم هاتفك للمصادقة. سنرسل لك رمز أمان عبر رسالة نصية عند تسجيل الدخول.", + "sms-hint": "يتم إرسال رموز المصادقة عبر الرسائل النصية إلى {{ info }}", + "totp": "تطبيق المصادق", + "totp-description": "استخدم تطبيقات مثل Google Authenticator، Authy، أو Duo على هاتفك للمصادقة. سيقوم بتوليد رمز أمان لتسجيل الدخول.", + "totp-hint": "تم تهيئة تطبيق المصادق لحسابك", + "backup_code": "رمز النسخ الاحتياطي", + "backup-code-description": "تتيح لك هذه الرموز القابلة للطباعة استخدامها لتسجيل الدخول عندما تكون بعيدًا عن هاتفك، مثل عند السفر.", + "backup-code-hint": "{{ info }} من رموز الاستخدام مرة واحدة نشطة في الوقت الحالي" + } + }, + "password-requirement": { + "at-least": "على الأقل:", + "character": "{ count, plural, =1 {حرف واحد} other {# أحرف} }", + "digit": "{ count, plural, =1 {رقم واحد} other {# أرقام} }", + "incorrect-password-try-again": "كلمة المرور غير صحيحة. حاول مرة أخرى", + "lowercase-letter": "{ count, plural, =1 {حرف صغير واحد} other {# حروف صغيرة} }", + "new-passwords-not-match": "لم تتطابق كلمة المرور الجديدة", + "password-should-not-contain-spaces": "كلمة المرور الخاصة بك لا يجب أن تحتوي على مسافات", + "password-not-meet-requirements": "لم تفي كلمة المرور بالمتطلبات", + "password-requirements": "متطلبات كلمة المرور", + "password-should-difference": "يجب أن تكون كلمة المرور الجديدة مختلفة عن الحالية", + "special-character": "{ count, plural, =1 {حرف خاص واحد} other {# أحرف خاصة} }", + "uppercase-letter": "{ count, plural, =1 {حرف كبير واحد} other {# حروف كبيرة} }", + "at-most": "في الحد الأقصى:" + } + }, + "relation": { + "relations": "العلاقات", + "direction": "الاتجاه", + "clear-relation-type": "مسح نوع العلاقة", + "search-direction": { + "FROM": "من", + "TO": "إلى" + }, + "direction-type": { + "FROM": "من", + "TO": "إلى" + }, + "from-relations": "العلاقات الصادرة", + "to-relations": "العلاقات الواردة", + "selected-relations": "{ count, plural, =1 {1 علاقة} other {# علاقات} } تم اختيارها", + "type": "النوع", + "to-entity-type": "نوع الكيان المستلم", + "to-entity-name": "اسم الكيان المستلم", + "from-entity-type": "نوع الكيان المرسل", + "from-entity-name": "اسم الكيان المرسل", + "to-entity": "الكيان المستلم", + "from-entity": "الكيان المرسل", + "delete": "حذف العلاقة", + "relation-type": "نوع العلاقة", + "relation-type-required": "مطلوب نوع العلاقة.", + "relation-type-max-length": "يجب أن يكون طول نوع العلاقة أقل من 256 حرفًا", + "any-relation-type": "أي نوع", + "add": "إضافة علاقة", + "edit": "تحرير العلاقة", + "view": "عرض العلاقة", + "delete-to-relation-title": "هل أنت متأكد أنك تريد حذف العلاقة إلى الكيان '{{entityName}}'؟", + "delete-to-relation-text": "كن حذرًا، بعد التأكيد سيتم فصل الكيان '{{entityName}}' عن الكيان الحالي.", + "delete-to-relations-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 علاقة} other {# علاقات} }؟", + "delete-to-relations-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع العلاقات المحددة وستفصل الكيانات المقابلة عن الكيان الحالي.", + "delete-from-relation-title": "هل أنت متأكد أنك تريد حذف العلاقة من الكيان '{{entityName}}'؟", + "delete-from-relation-text": "كن حذرًا، بعد التأكيد سيتم فصل الكيان الحالي عن الكيان '{{entityName}}'.", + "delete-from-relations-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 علاقة} other {# علاقات} }؟", + "delete-from-relations-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع العلاقات المحددة وستفصل الكيانات الحالية عن الكيانات المقابلة.", + "remove-relation-filter": "إزالة فلتر العلاقة", + "remove-filter": "إزالة الفلتر", + "add-relation-filter": "إضافة فلتر العلاقة", + "any-relation": "أي علاقة", + "relation-filters": "مرشحات العلاقة", + "additional-info": "معلومات إضافية (JSON)", + "invalid-additional-info": "تعذر تحليل معلومات JSON الإضافية.", + "no-relations-text": "لم يتم العثور على علاقات" + }, + "resource": { + "add": "إضافة مورد", + "all-types": "الكل", + "copyId": "نسخ معرّف المورد", + "delete": "حذف المورد", + "delete-resource-text": "كن حذرًا، بعد التأكيد سيصبح المورد غير قابل للاسترداد.", + "delete-resource-title": "هل أنت متأكد أنك تريد حذف المورد '{{resourceTitle}}'؟", + "delete-resources-action-title": "حذف { count, plural, =1 {1 مورد} other {# موارد} }", + "delete-resources-text": "يرجى ملاحظة أن الموارد المحددة، حتى لو كانت مستخدمة في ملفات تعريف الجهاز، ستتم حذفها.", + "delete-resources-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 مورد} other {# موارد} }؟", + "download": "تنزيل المورد", + "drop-file": "اسحب ملف مورد أو انقر لتحديد ملف للتحميل.", + "drop-resource-file-or": "اسحب وأفلت ملف مورد أو", + "empty": "المورد فارغ", + "file-name": "اسم الملف", + "idCopiedMessage": "تم نسخ معرّف المورد إلى الحافظة", + "no-resource-matching": "لم يتم العثور على مورد مطابق لـ '{{widgetsBundle}}'.", + "no-resource-text": "لم يتم العثور على موارد", + "open-widgets-bundle": "فتح حزمة الودجات", + "resource": "مورد", + "resource-file": "ملف المورد", + "resource-files": "ملفات المورد", + "resource-library-details": "تفاصيل المورد", + "resource-type": "نوع المورد", + "resources-library": "مكتبة الموارد", + "search": "البحث في الموارد", + "selected-resources": "{ count, plural, =1 {1 مورد} other {# موارد} } تم اختيارها", + "system": "النظام", + "title": "العنوان", + "title-required": "العنوان مطلوب.", + "title-max-length": "يجب أن يكون العنوان أقل من 256 حرفًا", + "type": { + "jks": "JKS", + "js-module": "وحدة JS", + "lwm2m-model": "نموذج LWM2M", + "pkcs-12": "PKCS #12" + } + }, + "rulechain": { + "rulechain": "سلسلة القواعد", + "rulechain-events": "أحداث سلسلة القواعد", + "rulechains": "سلاسل القواعد", + "root": "الجذر", + "delete": "حذف سلسلة القواعد", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256 حرفًا", + "description": "الوصف", + "add": "إضافة سلسلة قواعد", + "set-root": "جعل سلسلة القواعد الجذر", + "set-root-rulechain-title": "هل أنت متأكد أنك تريد جعل سلسلة القواعد '{{ruleChainName}}' جذر؟", + "set-root-rulechain-text": "بعد التأكيد، ستصبح سلسلة القواعد جذرًا وستتعامل مع جميع رسائل النقل الواردة.", + "delete-rulechain-title": "هل أنت متأكد أنك تريد حذف سلسلة القواعد '{{ruleChainName}}'؟", + "delete-rulechain-text": "كن حذرًا، بعد التأكيد ستصبح سلسلة القواعد وجميع البيانات المتعلقة بها غير قابلة للاسترداد.", + "delete-rulechains-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 سلسلة قواعد} other {# سلاسل القواعد} }؟", + "delete-rulechains-action-title": "حذف { count, plural, =1 {1 سلسلة قواعد} other {# سلاسل القواعد} }", + "delete-rulechains-text": "كن حذرًا، بعد التأكيد ستتم إزالة جميع سلاسل القواعد المحددة وستصبح جميع البيانات المتعلقة بها غير قابلة للاسترداد.", + "add-rulechain-text": "إضافة سلسلة قواعد جديدة", + "no-rulechains-text": "لم يتم العثور على سلاسل قواعد", + "rulechain-details": "تفاصيل سلسلة القواعد", + "details": "تفاصيل", + "events": "أحداث", + "system": "النظام", + "import": "استيراد سلسلة القواعد", + "export": "تصدير سلسلة القواعد", + "export-failed-error": "يتعذر تصدير سلسلة القواعد: {{error}}", + "create-new-rulechain": "إنشاء سلسلة قواعد جديدة", + "rulechain-file": "ملف سلسلة القواعد", + "invalid-rulechain-file-error": "يتعذر استيراد سلسلة القواعد: هيكل البيانات غير صالح لسلسلة القواعد.", + "copyId": "نسخ معرّف سلسلة القواعد", + "idCopiedMessage": "تم نسخ معرّف سلسلة القواعد إلى الحافظة", + "select-rulechain": "اختر سلسلة القواعد", + "no-rulechains-matching": "لم يتم العثور على سلاسل قواعد تطابق '{{entity}}'.", + "rulechain-required": "سلسلة القواعد مطلوبة", + "management": "إدارة القواعد", + "debug-mode": "وضع التصحيح", + "search": "البحث في سلاسل القواعد", + "selected-rulechains": "{ count, plural, =1 {1 سلسلة قواعد} other {# سلاسل القواعد} } تم اختيارها", + "open-rulechain": "فتح سلسلة القواعد", + "edge-template-root": "الجذر النموذجي", + "assign-to-edge": "تعيين إلى حافة", + "edge-rulechain": "سلسلة القواعد الحافة", + "unassign-rulechain-from-edge-text": "بعد التأكيد، سيتم إلغاء تعيين سلسلة القواعد ولن تكون متاحة للحافة.", + "unassign-rulechains-from-edge-title": "هل أنت متأكد أنك تريد إلغاء تعيين { count, plural, =1 {1 سلسلة قواعد} other {# سلاسل القواعد} }؟", + "unassign-rulechains-from-edge-text": "بعد التأكيد، ستتم إلغاء تعيين جميع سلاسل القواعد المحددة ولن تكون متاحة للحافة.", + "assign-rulechain-to-edge-title": "تعيين سلسلة القواعد إلى الحافة", + "assign-rulechain-to-edge-text": "يرجى تحديد سلاسل القواعد لتعيينها إلى الحافة", + "set-edge-template-root-rulechain": "جعل سلسلة القواعد الجذر النموذجي للحافة", + "set-edge-tscheduler-eventemplate-root-rulechain": "جعل سلسلة القواعد الجذر النموذجي للحافة", + "set-edge-template-root-rulechain-title": "هل أنت متأكد أنك تريد جعل سلسلة القواعد '{{ruleChainName}}' جذرًا نموذجيًا للحافة؟", + "set-edge-template-root-rulechain-text": "بعد التأكيد، ستصبح سلسلة القواعد جذرًا نموذجيًا وستكون سلسلة القواعد الجذر للحافة الجديدة.", + "invalid-rulechain-type-error": "يتعذر استيراد سلسلة القواعد: نوع سلسلة القواعد غير صالح. النوع المتوقع هو {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "تعيين سلسلة القواعد للحافة عند الإنشاء تلقائيًا", + "set-auto-assign-to-edge-title": "هل أنت متأكد أنك تريد تعيين سلسلة القواعد الحافة '{{ruleChainName}}' إلى الحافة عند الإنشاء؟", + "set-auto-assign-to-edge-text": "بعد التأكيد، سيتم تعيين سلسلة القواعد الحافة تلقائيًا إلى الحافة عند الإنشاء.", + "unset-auto-assign-to-edge": "عدم تعيين سلسلة القواعد للحافة عند الإنشاء", + "unset-auto-assign-to-edge-title": "هل أنت متأكد أنك لا تريد تعيين سلسلة القواعد الحافة '{{ruleChainName}}' إلى الحافة عند الإنشاء؟", + "unset-auto-assign-to-edge-text": "بعد التأكيد، لن تتم تعيين سلسلة القواعد الحافة تلقائيًا إلى الحافة عند الإنشاء.", + "unassign-rulechain-title": "هل أنت متأكد أنك تريد إلغاء تعيين سلسلة القواعد '{{ruleChainName}}'؟", + "unassign-rulechains": "إلغاء تعيين سلاسل القواعد" + }, + "rulenode": { + "rule-node-events": "أحداث عقدة القاعدة", + "details": "تفاصيل", + "events": "الأحداث", + "search": "البحث عن العقد", + "open-node-library": "فتح مكتبة العقد", + "close-node-library": "إغلاق مكتبة العقد", + "add": "إضافة عقدة قاعدة", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256 حرفًا", + "type": "النوع", + "rule-node-description": "وصف عقدة القاعدة", + "delete": "حذف عقدة القاعدة", + "select-all-objects": "تحديد جميع العقد والاتصالات", + "deselect-all-objects": "إلغاء تحديد جميع العقد والاتصالات", + "delete-selected-objects": "حذف العقد المحدد والاتصالات", + "delete-selected": "حذف المحدد", + "create-nested-rulechain": "إنشاء سلسلة قواعد متداخلة", + "select-all": "تحديد الكل", + "copy-selected": "نسخ المحدد", + "deselect-all": "إلغاء تحديد الكل", + "rulenode-details": "تفاصيل عقدة القاعدة", + "debug-mode": "وضع التصحيح", + "singleton-mode": "وضع المفرد", + "configuration": "التكوين", + "link": "رابط", + "link-details": "تفاصيل رابط عقدة القاعدة", + "add-link": "إضافة رابط", + "link-label": "تسمية الرابط", + "link-label-required": "تسمية الرابط مطلوبة.", + "custom-link-label": "تسمية الرابط المخصصة", + "custom-link-label-required": "تسمية الرابط المخصصة مطلوبة.", + "link-labels": "تسميات الرابط", + "link-labels-required": "تسميات الرابط مطلوبة.", + "no-link-labels-found": "لم يتم العثور على تسميات الرابط", + "no-link-label-matching": "لم يتم العثور على '{{label}}'.", + "create-new-link-label": "إنشاء واحدة جديدة!", + "type-filter": "تصفية", + "type-filter-details": "تصفية الرسائل الواردة بالشروط المكونة", + "type-enrichment": "تحسين", + "type-enrichment-details": "إضافة معلومات إضافية إلى بيانات الرسالة", + "type-transformation": "تحويل", + "type-transformation-details": "تغيير حمولة الرسالة والبيانات الوصفية", + "type-action": "إجراء", + "type-action-details": "القيام بإجراء خاص", + "type-analytics": "تحليلات", + "type-analytics-details": "إجراء تحليل للبيانات المبثوثة أو المخزنة", + "type-external": "خارجي", + "type-external-details": "التفاعل مع النظام الخارجي", + "type-rule-chain": "سلسلة القواعد", + "type-rule-chain-details": "إعادة توجيه الرسائل الواردة إلى سلسلة قواعد محددة", + "type-flow": "تدفق", + "type-flow-details": "تنظيم تدفق الرسالة", + "type-input": "الإدخال", + "type-input-details": "المدخل اللوجي لسلسلة القواعد، يعيد الرسائل الواردة إلى العقدة القاعدية المتصلة التالية", + "type-unknown": "غير معروف", + "type-unknown-details": "عقدة قاعدة غير محلولة", + "directive-is-not-loaded": "تعليمة التكوين المحددة '{{directiveName}}' غير متوفرة.", + "ui-resources-load-error": "فشل في تحميل موارد واجهة المستخدم للتكوين.", + "invalid-target-rulechain": "يتعذر حل سلسلة القواعد المستهدفة!", + "test-script-function": "وظيفة اختبار النص", + "script-lang-java-script": "جافا سكريبت", + "script-lang-tbel": "TBEL", + "message": "الرسالة", + "message-type": "نوع الرسالة", + "select-message-type": "حدد نوع الرسالة", + "message-type-required": "نوع الرسالة مطلوب", + "metadata": "البيانات الوصفية", + "metadata-required": "يجب ألا تكون إدخالات البيانات الوصفية فارغة.", + "output": "الإخراج", + "test": "اختبار", + "help": "مساعدة", + "reset-debug-mode": "إعادة تعيين وضع التصحيح في جميع العقد", + "test-with-this-message": "{{test}} مع هذه الرسالة" + }, + "role": { + "role": "الدور", + "roles": "الأدوار", + "management": "إدارة الأدوار", + "view-roles": "عرض الأدوار", + "no-roles-matching": "لا توجد أدوار تطابق '{{entity}}' تم العثور عليها.", + "role-list": "قائمة الأدوار", + "role-list-required": "قائمة الأدوار مطلوبة", + "add": "إضافة دور", + "view": "عرض الدور", + "search": "البحث عن الأدوار", + "selected-roles": "{ count, plural, =1 {1 دور} other {# أدوار} } تم اختيارها", + "no-roles-text": "لا توجد أدوار متاحة", + "role-details": "تفاصيل الدور", + "add-role-text": "إضافة دور جديد", + "delete": "حذف الدور", + "delete-roles": "حذف الأدوار", + "delete-role-title": "هل أنت متأكد أنك تريد حذف الدور '{{roleName}}'؟", + "delete-role-text": "تنبيه، بعد التأكيد سيتم حذف الدور وجميع البيانات المرتبطة به نهائياً.", + "delete-roles-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 دور} other {# أدوار} }؟", + "delete-roles-action-title": "حذف { count, plural, =1 {1 دور} other {# أدوار} }", + "delete-roles-text": "تنبيه، بعد التأكيد سيتم حذف جميع الأدوار المحددة وجميع البيانات المرتبطة بها نهائياً.", + "role-type": "نوع الدور", + "role-type-required": "نوع الدور مطلوب.", + "select-role-type": "حدد نوع الدور", + "enter-role-type": "أدخل نوع الدور", + "any-role": "أي دور", + "no-role-types-matching": "لا توجد أنواع أدوار تطابق '{{entitySubtype}}' تم العثور عليها.", + "role-type-list-empty": "لم يتم اختيار أنواع الأدوار.", + "role-types": "أنواع الأدوار", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "name-max-length": "يجب أن يكون الاسم أقل من 256 حرفًا", + "description": "الوصف", + "events": "الأحداث", + "details": "تفاصيل", + "copyId": "نسخ معرف الدور", + "idCopiedMessage": "تم نسخ معرف الدور إلى الحافظة", + "permissions": "الأذونات", + "role-required": "الدور مطلوب", + "roles-required": "الأدوار مطلوبة", + "display-type": { + "GENERIC": "عام", + "GROUP": "مجموعة" + } + }, + "group-permission": { + "user-group-roles": "أدوار مجموعة المستخدمين", + "entity-group-permissions": "أذونات مجموعة الكيانات", + "role-type": "نوع الدور", + "role-name": "اسم الدور", + "group-type": "نوع المجموعة", + "group-name": "اسم المجموعة", + "group-owner": "مالك المجموعة", + "user-group-name": "اسم مجموعة المستخدمين", + "user-group-owner": "مالك مجموعة المستخدمين", + "edit": "تعديل الأذونات", + "delete": "حذف الأذونات", + "selected-group-permissions": "{ count, plural, =1 {1 أذن مجموعة} other {# أذونات مجموعة} } محددة", + "delete-group-permission-title": "هل أنت متأكد أنك تريد حذف إذن المجموعة '{{roleName}}'؟", + "delete-group-permission-text": "تنبيه، بعد التأكيد سيتم حذف إذن المجموعة وجميع البيانات المرتبطة به نهائياً.", + "delete-group-permission": "حذف إذن المجموعة", + "delete-group-permissions-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 أذن مجموعة} other {# أذن مجموعة} }؟", + "delete-group-permissions-text": "تنبيه، بعد التأكيد سيتم حذف جميع أذونات المجموعة المحددة وسيفقد المستخدمون المقابلين الوصول إلى الموارد المحددة.", + "delete-group-permissions": "حذف أذونات المجموعة", + "add-group-permission": "إضافة إذن مجموعة", + "edit-group-permission": "تعديل إذن مجموعة", + "entity-group": "مجموعة الكيان", + "user-group": "مجموعة المستخدمين", + "no-owners-matching": "لم يتم العثور على مالكين مطابقين '{{owner}}'.", + "target-owner-required": "مالك مجموعة الكيان مطلوب.", + "target-user-group-owner-required": "مالك مجموعة المستخدمين مطلوب.", + "no-group-permissions-text": "لم يتم العثور على أذونات المجموعة" + }, + "permission": { + "permissions-required": "يجب تحديد مدخل واحد على الأقل للإذن.", + "remove-permission": "إزالة مدخل الإذن", + "add-permission": "إضافة مدخل الإذن", + "other": "آخر", + "resource": { + "resource": "المورد", + "select-resource": "حدد المورد", + "resource-required": "المورد مطلوب", + "no-resources-matching": "لم يتم العثور على موارد تطابق '{{resource}}'.", + "display-type": { + "ALL": "الكل", + "PROFILE": "الملف الشخصي", + "ADMIN_SETTINGS": "إعدادات المسؤول", + "ALARM": "التنبيه", + "DEVICE": "الجهاز", + "DEVICE_PROFILE": "ملف الجهاز", + "ASSET": "الأصل", + "CUSTOMER": "العميل", + "DASHBOARD": "لوحة المعلومات", + "ENTITY_VIEW": "عرض الكيان", + "EDGE": "الحافة", + "TENANT": "المستأجر", + "TENANT_PROFILE": "ملف المستأجر", + "RULE_CHAIN": "سلسلة القواعد", + "USER": "المستخدم", + "WIDGETS_BUNDLE": "حزمة الودجات", + "WIDGET_TYPE": "نوع الودجة", + "CONVERTER": "المحول", + "INTEGRATION": "التكامل", + "SCHEDULER_EVENT": "حدث المجدول", + "BLOB_ENTITY": "كيان البيانات الكبيرة", + "CUSTOMER_GROUP": "مجموعة العملاء", + "DEVICE_GROUP": "مجموعة الأجهزة", + "ASSET_GROUP": "مجموعة الأصول", + "USER_GROUP": "مجموعة المستخدمين", + "ENTITY_VIEW_GROUP": "مجموعة عرض الكيان", + "DASHBOARD_GROUP": "مجموعة لوحة المعلومات", + "ROLE": "الدور", + "GROUP_PERMISSION": "إذن المجموعة", + "WHITE_LABELING": "العلامات البيضاء", + "AUDIT_LOG": "سجل التدقيق", + "API_USAGE_STATE": "حالة استخدام واجهة البرمجة", + "TB_RESOURCE": "المورد", + "EDGE_GROUP": "مجموعة الحافة", + "OTA_PACKAGE": "حزمة OTA", + "QUEUE": "الطابور", + "VERSION_CONTROL": "التحكم في الإصدارات", + "ASSET_PROFILE": "ملف الأصول", + "NOTIFICATION": "الإشعار" + } + }, + "operation": { + "operation": "العملية", + "operations": "العمليات", + "operations-required": "يجب تحديد عملية واحدة على الأقل.", + "enter-operation": "أدخل العملية", + "no-operations-matching": "لم يتم العثور على عمليات تطابق '{{operation}}'.", + "display-type": { + "ALL": "الكل", + "CREATE": "إنشاء", + "READ": "قراءة", + "WRITE": "كتابة", + "DELETE": "حذف", + "ASSIGN_TO_CUSTOMER": "تعيين للعميل", + "UNASSIGN_FROM_CUSTOMER": "إلغاء التعيين من العميل", + "RPC_CALL": "استدعاء RPC", + "READ_CREDENTIALS": "قراءة الاعتمادات", + "WRITE_CREDENTIALS": "كتابة الاعتمادات", + "READ_ATTRIBUTES": "قراءة السمات", + "WRITE_ATTRIBUTES": "كتابة السمات", + "READ_TELEMETRY": "قراءة البيانات الحيوية", + "WRITE_TELEMETRY": "كتابة البيانات الحيوية", + "CLAIM_DEVICES": "مطالبة الأجهزة", + "IMPERSONATE": "تقمص الشخصية", + "CHANGE_OWNER": "تغيير المالك", + "ADD_TO_GROUP": "إضافة إلى المجموعة", + "REMOVE_FROM_GROUP": "إزالة من المجموعة", + "SHARE_GROUP": "مشاركة المجموعة", + "ASSIGN_TO_TENANT": "تعيين للمستأجر" + } + } + }, + "scheduler": { + "scheduler": "الجدول الزمني", + "scheduler-event": "حدث الجدول الزمني", + "select-scheduler-event": "حدد حدث الجدول الزمني", + "no-scheduler-events-matching": "لم يتم العثور على أحداث جدول زمني تطابق '{{entity}}'.", + "scheduler-event-required": "حدث الجدول الزمني مطلوب", + "management": "إدارة الجدول الزمني", + "scheduler-events": "أحداث الجدول الزمني", + "add-scheduler-event": "إضافة حدث جدول زمني", + "search-scheduler-events": "بحث عن أحداث جدول زمني", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "name-required": "الاسم مطلوب", + "name-max-length": "يجب أن يكون الاسم أقل من 256 حرفًا", + "type": "النوع", + "created_customer": "تم إنشاؤه بواسطة العميل", + "edit-scheduler-event": "تحرير حدث الجدول الزمني", + "view-scheduler-event": "عرض حدث الجدول الزمني", + "delete-scheduler-event": "حذف حدث الجدول الزمني", + "no-scheduler-events": "لا توجد أحداث جدول زمني", + "selected-scheduler-events": "{ count, plural, =1 {1 حدث جدول زمني} other {# أحداث جدول زمني} } محددة", + "delete-scheduler-event-title": "هل أنت متأكد أنك تريد حذف حدث الجدول الزمني '{{schedulerEventName}}'؟", + "delete-scheduler-event-text": "تنبيه، بعد التأكيد سيتم حذف حدث الجدول الزمني وجميع البيانات المرتبطة به نهائياً.", + "delete-scheduler-events-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 حدث جدول زمني} other {# أحداث جدول زمني} }؟", + "delete-scheduler-events-text": "تنبيه، بعد التأكيد سيتم حذف جميع أحداث الجدول الزمني المحددة وسيتم حذف جميع البيانات المرتبطة بها نهائياً.", + "create": "إنشاء حدث جدول زمني", + "edit": "تحرير حدث جدول زمني", + "view": "عرض حدث جدول زمني", + "configuration": "التكوين", + "schedule": "الجدول", + "start-time": "وقت البدء", + "repeat": "كرر", + "repeats": "تكرارات", + "daily": "يومياً", + "every-n-days": "كل يوم", + "every-n-days-text": "كل { days, plural, =1 {يوم} other {# أيام} }", + "weekly": "أسبوعياً", + "every-n-weeks": "كل أسبوع", + "every-n-weeks-text": "كل { weeks, plural, =1 {أسبوع} other {# أسابيع} }", + "monthly": "شهرياً", + "yearly": "سنوياً", + "timer": "بناءً على المؤقت", + "repeats-required": "التكرارات مطلوبة.", + "repeat-on": "تكرار على", + "repeat-every": "كرر كل", + "repeat-every-n-days": "كرر كل يوم", + "repeat-days-required": "عدد الأيام للتكرار مطلوب.", + "invalid-repeat-days-value": "يجب أن يكون عدد الأيام للتكرار عددًا صحيحًا إيجابيًا.", + "repeat-every-n-weeks": "كرر كل أسبوع", + "repeat-weeks-required": "عدد الأسابيع للتكرار مطلوب.", + "invalid-repeat-weeks-value": "يجب أن يكون عدد الأسابيع للتكرار عددًا صحيحًا إيجابيًا.", + "ends-on": "ينتهي في", + "sunday-label": "ح", + "monday-label": "ن", + "tuesday-label": "ث", + "wednesday-label": "ر", + "thursday-label": "خ", + "friday-label": "ج", + "saturday-label": "س", + "repeat-on-sunday": "كرر يوم الأحد", + "repeat-on-monday": "كرر يوم الاثنين", + "repeat-on-tuesday": "كرر يوم الثلاثاء", + "repeat-on-wednesday": "كرر يوم الأربعاء", + "repeat-on-thursday": "كرر يوم الخميس", + "repeat-on-friday": "كرر يوم الجمعة", + "repeat-on-saturday": "كرر يوم السبت", + "event-type": "نوع الحدث", + "select-event-type": "حدد نوع الحدث", + "event-type-required": "نوع الحدث مطلوب.", + "event-type-max-length": "يجب أن يكون نوع الحدث أقل من 256 حرفًا.", + "list-mode": "عرض القائمة", + "calendar-mode": "عرض التقويم", + "calendar-view-type": "نوع عرض التقويم", + "month": "شهر", + "week": "أسبوع", + "day": "يوم", + "agenda-week": "جدول أسبوع", + "agenda-day": "جدول يوم", + "list-year": "قائمة السنة", + "list-month": "قائمة الشهر", + "list-week": "قائمة الأسبوع", + "list-day": "قائمة اليوم", + "today": "اليوم", + "navigate-before": "التنقل قبل", + "navigate-next": "التنقل التالي", + "starting-from": "ابتداءً من", + "until": "حتى", + "on": "في", + "sunday": "الأحد", + "monday": "الاثنين", + "tuesday": "الثلاثاء", + "wednesday": "الأربعاء", + "thursday": "الخميس", + "friday": "الجمعة", + "saturday": "السبت", + "originator": "المبدأ", + "single-entity": "كيان فردي", + "group-of-entities": "مجموعة من الكيانات", + "entities-group-owner": "مالك مجموعة الكيانات", + "single-device": "جهاز فردي", + "group-of-devices": "مجموعة من الأجهزة", + "devices-group-owner": "مالك مجموعة الأجهزة", + "message-body": "نص الرسالة", + "target": "الهدف", + "rpc-method": "الطريقة", + "rpc-method-required": "الطريقة مطلوبة", + "rpc-method-white-space": "لا يُسمح بالمسافات البيضاء.", + "rpc-params": "المعلمات", + "select-dashboard-state": "حدد حالة لوحة التحكم", + "hours": "ساعات", + "minutes": "دقائق", + "seconds": "ثواني", + "time-interval-required": "فترة الزمن مطلوبة", + "time-unit-required": "وحدة الزمن مطلوبة", + "every-hour": "كل { count, plural, =1 {ساعة} other {# ساعات} }", + "every-minute": "كل { count, plural, =1 {دقيقة} other {# دقائق} }", + "every-second": "كل { count, plural, =1 {ثانية} other {# ثواني} }", + "invalid-time": "وقت غير صالح", + "rpc-request-timeout": "مهلة طلب RPC (مللي ثانية)", + "rpc-request-persistent": "طلب RPC دائم", + "rpc-request-oneway-command": "طلب RPC في اتجاه واحد", + "rpc-value-min-error": "يجب أن تكون القيمة أكبر من {{min}} مللي ثانية.", + "set": "تعيين", + "rpc-settings": "إعدادات RPC" + }, + "report": { + "report-config": "تكوين التقرير", + "email-config": "تكوين البريد الإلكتروني", + "dashboard-state-param": "قيمة معلمة حالة لوحة التحكم", + "base-url": "عنوان URL الأساسي", + "base-url-required": "عنوان URL الأساسي مطلوب.", + "use-dashboard-timewindow": "استخدم نافذة الوقت للوحة التحكم", + "timewindow": "نافذة الوقت", + "name-pattern": "نمط اسم التقرير", + "name-pattern-required": "نمط اسم التقرير مطلوب", + "type": "نوع التقرير", + "use-current-user-credentials": "استخدم بيانات اعتماد المستخدم الحالي", + "customer-user-credentials": "بيانات اعتماد مستخدم العميل", + "customer-user-credentials-required": "بيانات اعتماد مستخدم العميل مطلوبة", + "generate-test-report": "إنشاء تقرير اختباري", + "send-email": "إرسال بريد إلكتروني", + "from": "من", + "from-required": "المرسل مطلوب.", + "to": "إلى", + "to-required": "المرسل إليه مطلوب.", + "cc": "نسخة", + "bcc": "نسخة مخفية", + "subject": "الموضوع", + "subject-required": "الموضوع مطلوب.", + "body": "النص", + "body-required": "النص مطلوب." + }, + "blob-entity": { + "blob-entity": "كيان بيانات كبيرة", + "select-blob-entity": "حدد كيان بيانات كبيرة", + "no-blob-entities-matching": "لم يتم العثور على كيانات بيانات كبيرة تطابق '{{entity}}'.", + "blob-entity-required": "الكيان بيانات كبيرة مطلوب", + "files": "الملفات", + "search": "البحث عن ملفات", + "clear-search": "مسح البحث", + "no-blob-entities-prompt": "لم يتم العثور على ملفات", + "report": "تقرير", + "created-time": "وقت الإنشاء", + "name": "الاسم", + "type": "النوع", + "created_customer": "تم الإنشاء بواسطة العميل", + "selected-blob-entities": "{ count, plural, =1 {ملف واحد} other {# ملفات} } محددة", + "download-blob-entity": "تنزيل الملف", + "delete-blob-entity": "حذف الملف", + "delete-blob-entity-title": "هل أنت متأكد أنك تريد حذف الملف '{{blobEntityName}}'؟", + "delete-blob-entity-text": "كن حذرًا، بعد التأكيد ستصبح بيانات الملف غير قابلة للاسترداد.", + "delete-blob-entities-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {ملف واحد} other {# ملفات} }؟", + "delete-blob-entities-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع الملفات المحددة وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد." + }, + "timezone": { + "timezone": "المنطقة الزمنية", + "select-timezone": "حدد المنطقة الزمنية", + "no-timezones-matching": "لم يتم العثور على مناطق زمنية تطابق '{{timezone}}'.", + "timezone-required": "المنطقة الزمنية مطلوبة.", + "browser-time": "وقت المتصفح" + }, + "queue": { + "queue-name": "اسم الطابور", + "no-queues-found": "لم يتم العثور على أي طوابور.", + "no-queues-matching": "لم يتم العثور على أي طوابور مطابقة لـ '{{queue}}'.", + "select-name": "حدد اسم الطابور", + "name": "الاسم", + "name-required": "اسم الطابور مطلوب!", + "name-unique": "اسم الطابور غير فريد!", + "name-pattern": "اسم الطابور يحتوي على حرف غير الأحرف الأبجدية الرقمية ASCII و '.', '_' و '-'!", + "queue-required": "الطابور مطلوب!", + "topic-required": "موضوع الطابور مطلوب!", + "poll-interval-required": "فاصل التصويت مطلوب!", + "poll-interval-min-value": "لا يمكن أن يكون قيمة فاصل التصويت أقل من 1", + "partitions-required": "التقسيمات مطلوبة!", + "partitions-min-value": "لا يمكن أن تكون قيمة التقسيمات أقل من 1", + "pack-processing-timeout-required": "مهلة معالجة الحزمة مطلوبة", + "pack-processing-timeout-min-value": "لا يمكن أن تكون قيمة مهلة معالجة الحزمة أقل من 1", + "batch-size-required": "حجم الدفعة مطلوب!", + "batch-size-min-value": "لا يمكن أن يكون حجم الدفعة أقل من 1", + "retries-required": "عدد محاولات إعادة المحاولة مطلوب!", + "retries-min-value": "لا يمكن أن يكون قيمة إعادة المحاولة سالبة", + "failure-percentage-required": "نسبة الفشل مطلوبة!", + "failure-percentage-min-value": "لا يمكن أن تكون قيمة نسبة الفشل أقل من 0", + "failure-percentage-max-value": "لا يمكن أن تكون قيمة نسبة الفشل أكثر من 100", + "pause-between-retries-required": "وقت التوقف بين المحاولات مطلوب!", + "pause-between-retries-min-value": "لا يمكن أن يكون وقت التوقف بين المحاولات أقل من 1", + "max-pause-between-retries-required": "الحد الأقصى لوقت التوقف بين المحاولات مطلوب!", + "max-pause-between-retries-min-value": "لا يمكن أن يكون الحد الأقصى لوقت التوقف بين المحاولات أقل من 1", + "submit-strategy-type-required": "نوع استراتيجية الإرسال مطلوب!", + "processing-strategy-type-required": "نوع استراتيجية المعالجة مطلوب!", + "queues": "الطوابور", + "selected-queues": "{ count, plural, =1 {1 طابور} other {# طوابور} } تم تحديدها", + "delete-queue-title": "هل أنت متأكد أنك تريد حذف الطابور '{{queueName}}'؟", + "delete-queues-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 طابور} other {# طوابور} }؟", + "delete-queue-text": "كن حذرًا، بعد التأكيد سيصبح الطابور وجميع البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-queues-text": "بعد التأكيد سيتم حذف جميع الطوابور المحددة ولن يمكن الوصول إليها.", + "search": "البحث عن طابور", + "add": "إضافة طابور", + "details": "تفاصيل الطابور", + "topic": "الموضوع", + "submit-settings": "إعدادات الإرسال", + "submit-strategy": "نوع الاستراتيجية *", + "grouping-parameter": "معلمة التجميع", + "processing-settings": "إعدادات معالجة إعادة المحاولة", + "processing-strategy": "نوع المعالجة *", + "retries-settings": "إعدادات إعادة المحاولة", + "polling-settings": "إعدادات التصويت", + "batch-processing": "معالجة دفعة", + "poll-interval": "فاصل التصويت", + "partitions": "التقسيمات", + "immediate-processing": "المعالجة الفورية", + "consumer-per-partition": "إرسال رسالة تصويت لكل مستهلك", + "consumer-per-partition-hint": "تمكين مستهلك (أو مستهلكين) منفصلين لكل قسم", + "processing-timeout": "المعالجة ضمن، مللي ثانية", + "batch-size": "حجم الدفعة", + "retries": "عدد محاولات الإعادة (0 - غير محدود)", + "failure-percentage": "نسبة الرسائل الفاشلة لتخطي محاولات الإعادة", + "pause-between-retries": "التوقف بين المحاولات، ثانية", + "max-pause-between-retries": "التوقف الأقصى بين المحاولات، ثانية", + "delete": "حذف الطابور", + "copyId": "نسخ معرّف الطابور", + "idCopiedMessage": "تم نسخ معرّف الطابور إلى الحافظة", + "description": "الوصف", + "description-hint": "سيتم عرض هذا النص في وصف الطابور بدلاً من الاستراتيجية المحددة", + "alt-description": "استراتيجية الإرسال: {{submitStrategy}}, استراتيجية المعالجة: {{processingStrategy}}", + "custom-properties": "خصائص مخصصة", + "custom-properties-hint": "خصائص إنشاء طابور (موضوع) مخصصة، على سبيل المثال 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "تسلسلي حسب المبادر", + "sequential-by-originator-hint": "لا يتم تقديم رسالة جديدة عن طريق الجهاز أ حتى يتم الاعتراف بالرسالة السابقة للجهاز أ", + "sequential-by-tenant-label": "تسلسلي حسب المستأجر", + "sequential-by-tenant-hint": "لا يتم تقديم رسالة جديدة للمستأجر أ حتى يتم الاعتراف بالرسالة السابقة للمستأجر أ", + "sequential-label": "تسلسلي", + "sequential-hint": "لا يتم تقديم رسالة جديدة حتى يتم الاعتراف بالرسالة السابقة", + "burst-label": "انفجار", + "burst-hint": "تتم تقديم جميع الرسائل إلى سلاسل القواعد بالترتيب الذي يصلون به", + "batch-label": "دفعة", + "batch-hint": "لا يتم تقديم دفعة جديدة حتى يتم الاعتراف بالدفعة السابقة", + "skip-all-failures-label": "تخطي جميع الفشل", + "skip-all-failures-hint": "تجاهل جميع الأخطاء", + "skip-all-failures-and-timeouts-label": "تخطي جميع الفشل والمهلات", + "skip-all-failures-and-timeouts-hint": "تجاهل جميع الأخطاء والمهلات", + "retry-all-label": "إعادة المحاولة للجميع", + "retry-all-hint": "إعادة محاولة جميع الرسائل من حزمة المعالجة", + "retry-failed-label": "إعادة محاولة الفشل", + "retry-failed-hint": "إعادة محاولة جميع الرسائل الفاشلة من حزمة المعالجة", + "retry-timeout-label": "إعادة المحاولة للمهلة", + "retry-timeout-hint": "إعادة محاولة جميع الرسائل المنتهية المهلة من حزمة المعالجة", + "retry-failed-and-timeout-label": "إعادة المحاولة للفشل والمهلة", + "retry-failed-and-timeout-hint": "إعادة محاولة جميع الرسائل الفاشلة والمنتهية المهلة من حزمة المعالجة" + } + }, + "server-error": { + "general": "خطأ عام في الخادم", + "authentication": "خطأ في المصادقة", + "jwt-token-expired": "انتهت صلاحية رمز JWT", + "tenant-trial-expired": "انتهت مدة التجربة للمستأجر", + "credentials-expired": "انتهت صلاحية الاعتمادات", + "permission-denied": "رفض الإذن", + "invalid-arguments": "وسيطات غير صالحة", + "bad-request-params": "معلمات الطلب غير صالحة", + "item-not-found": "العنصر غير موجود", + "too-many-requests": "الكثير من الطلبات", + "too-many-updates": "الكثير من التحديثات" + }, + "tenant": { + "tenant": "المستأجر", + "tenants": "المستأجرون", + "management": "إدارة المستأجرين", + "add": "إضافة مستأجر", + "admins": "المسؤولون", + "manage-tenant-admins": "إدارة مسؤولي المستأجر", + "delete": "حذف المستأجر", + "add-tenant-text": "إضافة مستأجر جديد", + "no-tenants-text": "لم يتم العثور على مستأجرين", + "tenant-details": "تفاصيل المستأجر", + "title-max-length": "يجب أن يكون العنوان أقل من 256 حرفًا", + "delete-tenant-title": "هل أنت متأكد أنك تريد حذف المستأجر '{{tenantTitle}}'؟", + "delete-tenant-text": "كن حذرًا، بعد التأكيد سيصبح المستأجر وجميع البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-tenants-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {1 مستأجر} other {# مستأجرون} }؟", + "delete-tenants-action-title": "حذف { count, plural, =1 {1 مستأجر} other {# مستأجرون} }", + "delete-tenants-text": "بعد التأكيد سيتم حذف جميع المستأجرين المحددين ولن يمكن الوصول إليها.", + "title": "العنوان", + "title-required": "العنوان مطلوب.", + "description": "الوصف", + "details": "تفاصيل", + "events": "الأحداث", + "copyId": "نسخ معرّف المستأجر", + "idCopiedMessage": "تم نسخ معرّف المستأجر إلى الحافظة", + "select-tenant": "تحديد المستأجر", + "no-tenants-matching": "لم يتم العثور على مستأجرين مطابقين لـ '{{entity}}'.", + "tenant-required": "المستأجر مطلوب", + "allow-white-labeling": "السماح بالتسمية البيضاء", + "allow-customer-white-labeling": "السماح بالتسمية البيضاء للعميل", + "search": "البحث عن المستأجرين", + "selected-tenants": "{ count, plural, =1 {1 مستأجر} other {# مستأجرون} } تم تحديدها", + "isolated-tb-rule-engine": "استخدام طوابور محرك قواعد ThingsBoard المعزولة", + "isolated-tb-rule-engine-details": "سيكون لكل مستأجر طوابور محرك قواعد مخصصة" + }, + "tenant-profile": { + "tenant-profile": "ملف المستأجر", + "tenant-profiles": "ملفات المستأجرين", + "add": "إضافة ملف مستأجر", + "add-profile": "إضافة ملف", + "edit": "تعديل ملف المستأجر", + "tenant-profile-details": "تفاصيل ملف المستأجر", + "no-tenant-profiles-text": "لم يتم العثور على ملفات مستأجرين", + "name-max-length": "يجب أن يكون الاسم أقل من 256", + "search": "بحث في ملفات المستأجرين", + "selected-tenant-profiles": "{ count, plural, =1 {ملف مستأجر واحد} other {# من ملفات المستأجرين} } مُحدد", + "no-tenant-profiles-matching": "لم يتم العثور على ملف مستأجر يطابق '{{entity}}'.", + "tenant-profile-required": "ملف المستأجر مطلوب", + "idCopiedMessage": "تم نسخ معرف ملف المستأجر إلى الحافظة", + "set-default": "جعل ملف المستأجر افتراضي", + "delete": "حذف ملف المستأجر", + "copyId": "نسخ معرف ملف المستأجر", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "data": "بيانات الملف", + "profile-configuration": "تهيئة الملف", + "description": "الوصف", + "default": "افتراضي", + "delete-tenant-profile-title": "هل أنت متأكد أنك تريد حذف ملف المستأجر '{{tenantProfileName}}'؟", + "delete-tenant-profile-text": "كن حذرًا، بعد التأكيد سيصبح ملف المستأجر وجميع البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-tenant-profiles-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {ملف مستأجر واحد} other {# من ملفات المستأجرين} }؟", + "delete-tenant-profiles-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع ملفات المستأجرين المحددة وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد.", + "set-default-tenant-profile-title": "هل أنت متأكد أنك تريد جعل ملف المستأجر '{{tenantProfileName}}' افتراضي؟", + "set-default-tenant-profile-text": "بعد التأكيد، سيتم وضع علامة على ملف المستأجر كافتراضي وسيتم استخدامه للمستأجرين الجدد بدون ملف محدد.", + "no-tenant-profiles-found": "لم يتم العثور على ملفات مستأجرين.", + "create-new-tenant-profile": "إنشاء واحد جديد!", + "create-tenant-profile": "إنشاء ملف مستأجر جديد", + "import": "استيراد ملف المستأجر", + "export": "تصدير ملف المستأجر", + "export-failed-error": "تعذر تصدير ملف المستأجر: {{error}}", + "tenant-profile-file": "ملف مستأجر", + "invalid-tenant-profile-file-error": "تعذر استيراد ملف المستأجر: هيكل بيانات ملف المستأجر غير صالح.", + "advanced-settings": "الإعدادات المتقدمة", + "entities": "الكيانات", + "rule-engine": "محرك القواعد", + "time-to-live": "الوقت حتى الحياة", + "alarms-and-notifications": "التنبيهات والإخطارات", + "ota-files-in-bytes": "الملفات", + "ws-title": "WS", + "unlimited": "(0 - غير محدود)", + "maximum-devices": "الحد الأقصى لعدد الأجهزة", + "maximum-devices-required": "الحد الأقصى لعدد الأجهزة مطلوب.", + "maximum-devices-range": "لا يمكن أن يكون الحد الأقصى لعدد الأجهزة سالبًا", + "maximum-assets": "الحد الأقصى لعدد الأصول", + "maximum-assets-required": "الحد الأقصى لعدد الأصول مطلوب.", + "maximum-assets-range": "لا يمكن أن يكون الحد الأقصى لعدد الأصول سالبًا", + "maximum-customers": "الحد الأقصى لعدد العملاء", + "maximum-customers-required": "الحد الأقصى لعدد العملاء مطلوب.", + "maximum-customers-range": "لا يمكن أن يكون الحد الأقصى لعدد العملاء سالبًا", + "maximum-users": "الحد الأقصى لعدد المستخدمين", + "maximum-users-required": "الحد الأقصى لعدد المستخدمين مطلوب.", + "maximum-users-range": "لا يمكن أن يكون الحد الأقصى لعدد المستخدمين سالبًا", + "maximum-dashboards": "الحد الأقصى لعدد لوحات التحكم", + "maximum-dashboards-required": "الحد الأقصى لعدد لوحات التحكم مطلوب.", + "maximum-dashboards-range": "لا يمكن أن يكون الحد الأقصى لعدد لوحات التحكم سالبًا", + "maximum-rule-chains": "الحد الأقصى لعدد سلاسل القواعد", + "maximum-rule-chains-required": "الحد الأقصى لعدد سلاسل القواعد مطلوب.", + "maximum-rule-chains-range": "لا يمكن أن يكون الحد الأقصى لعدد سلاسل القواعد سالبًا", + "maximum-integrations": "الحد الأقصى لعدد التكاملات", + "maximum-integrations-required": "الحد الأقصى لعدد التكاملات مطلوب.", + "maximum-integrations-range": "لا يمكن أن يكون الحد الأقصى لعدد التكاملات سالبًا", + "maximum-converters": "الحد الأقصى لعدد المحولات", + "maximum-converters-required": "الحد الأقصى لعدد المحولات مطلوب.", + "maximum-converters-range": "لا يمكن أن يكون الحد الأقصى لعدد المحولات سالبًا", + "maximum-scheduler-events": "الحد الأقصى لعدد أحداث المجدول", + "maximum-scheduler-events-required": "الحد الأقصى لعدد أحداث المجدول مطلوب.", + "maximum-scheduler-events-range": "لا يمكن أن يكون الحد الأقصى لعدد أحداث المجدول سالبًا", + "maximum-resources-sum-data-size": "الحجم الإجمالي الأقصى لملفات الموارد (بايت)", + "maximum-resources-sum-data-size-required": "الحجم الإجمالي الأقصى لملفات الموارد مطلوب.", + "maximum-resources-sum-data-size-range": "لا يمكن أن يكون الحجم الإجمالي الأقصى لملفات الموارد سالبًا", + "maximum-resource-size": "الحد الأقصى لحجم ملف المورد (بايت)", + "maximum-resource-size-required": "الحد الأقصى لحجم ملف المورد مطلوب", + "maximum-resource-size-range": "لا يمكن أن يكون الحد الأقصى لحجم ملف المورد سالبًا", + "maximum-ota-packages-sum-data-size": "الحجم الإجمالي الأقصى لملفات حزم OTA (بايت)", + "maximum-ota-package-sum-data-size-required": "الحجم الإجمالي الأقصى لملفات حزم OTA مطلوب.", + "maximum-ota-package-sum-data-size-range": "لا يمكن أن يكون الحجم الإجمالي الأقصى لملفات حزم OTA سالبًا", + "rest-requests-for-tenant": "طلبات REST للمستأجر", + "transport-tenant-telemetry-msg-rate-limit": "رسائل تليمتري المستأجر عبر النقل", + "transport-tenant-telemetry-data-points-rate-limit": "نقاط بيانات تليمتري المستأجر عبر النقل", + "transport-device-msg-rate-limit": "رسائل الجهاز عبر النقل", + "transport-device-telemetry-msg-rate-limit": "رسائل تليمتري الجهاز عبر النقل", + "transport-device-telemetry-data-points-rate-limit": "نقاط بيانات تليمتري الجهاز عبر النقل", + "tenant-entity-export-rate-limit": "إنشاء نسخة الكيان", + "tenant-entity-import-rate-limit": "تحميل نسخة الكيان", + "tenant-notification-request-rate-limit": "طلبات الإخطار", + "tenant-notification-requests-per-rule-rate-limit": "طلبات الإخطار لكل قاعدة إخطار", + "integration-msgs-per-tenant-rate-limit": "رسائل التكامل لكل مستأجر", + "integration-msgs-per-device-rate-limit": "رسائل التكامل لكل جهاز", + "max-transport-messages": "الحد الأقصى لعدد رسائل النقل", + "max-transport-messages-required": "الحد الأقصى لعدد رسائل النقل مطلوب.", + "max-transport-messages-range": "لا يمكن أن يكون الحد الأقصى لعدد رسائل النقل سالبًا", + "max-transport-data-points": "الحد الأقصى لعدد نقاط بيانات النقل", + "max-transport-data-points-required": "الحد الأقصى لعدد نقاط بيانات النقل مطلوب.", + "max-transport-data-points-range": "لا يمكن أن يكون الحد الأقصى لعدد نقاط بيانات النقل سالبًا", + "max-r-e-executions": "الحد الأقصى لعدد تنفيذات محرك القواعد", + "max-r-e-executions-required": "الحد الأقصى لعدد تنفيذات محرك القواعد مطلوب.", + "max-r-e-executions-range": "لا يمكن أن يكون الحد الأقصى لعدد تنفيذات محرك القواعد سالبًا", + "max-j-s-executions": "الحد الأقصى لعدد تنفيذات JavaScript", + "max-j-s-executions-required": "الحد الأقصى لعدد تنفيذات JavaScript مطلوب.", + "max-j-s-executions-range": "لا يمكن أن يكون الحد الأقصى لعدد تنفيذات JavaScript سالبًا", + "max-tbel-executions": "الحد الأقصى لعدد تنفيذات TBEL", + "max-tbel-executions-required": "الحد الأقصى لعدد تنفيذات TBEL مطلوب.", + "max-tbel-executions-range": "لا يمكن أن يكون الحد الأقصى لعدد تنفيذات TBEL سالبًا", + "max-d-p-storage-days": "الحد الأقصى لعدد أيام تخزين نقاط البيانات", + "max-d-p-storage-days-required": "الحد الأقصى لعدد أيام تخزين نقاط البيانات مطلوب.", + "max-d-p-storage-days-range": "لا يمكن أن يكون الحد الأقصى لعدد أيام تخزين نقاط البيانات سالبًا", + "default-storage-ttl-days": "أيام TTL التخزين بشكل افتراضي", + "default-storage-ttl-days-required": "أيام TTL التخزين بشكل افتراضي مطلوبة.", + "default-storage-ttl-days-range": "لا يمكن أن تكون أيام TTL التخزين بشكل افتراضي سالبة", + "alarms-ttl-days": "أيام TTL التنبيهات", + "alarms-ttl-days-required": "أيام TTL التنبيهات مطلوبة", + "alarms-ttl-days-days-range": "لا يمكن أن تكون أيام TTL التنبيهات سالبة", + "rpc-ttl-days": "أيام TTL RPC", + "rpc-ttl-days-required": "أيام TTL RPC مطلوبة", + "rpc-ttl-days-days-range": "لا يمكن أن تكون أيام TTL RPC سالبة", + "queue-stats-ttl-days": "أيام TTL إحصائيات الطابور", + "queue-stats-ttl-days-required": "أيام TTL إحصائيات الطابور مطلوبة", + "queue-stats-ttl-days-range": "لا يمكن أن تكون أيام TTL إحصائيات الطابور سالبة", + "rule-engine-exceptions-ttl-days": "أيام TTL استثناءات محرك القواعد", + "rule-engine-exceptions-ttl-days-required": "أيام TTL استثناءات محرك القواعد مطلوبة", + "rule-engine-exceptions-ttl-days-range": "لا يمكن أن تكون أيام TTL استثناءات محرك القواعد سالبة", + "max-rule-node-executions-per-message": "الحد الأقصى لعدد تنفيذات عقدة القاعدة لكل رسالة", + "max-rule-node-executions-per-message-required": "الحد الأقصى لعدد تنفيذات عقدة القاعدة لكل رسالة مطلوب.", + "max-rule-node-executions-per-message-range": "لا يمكن أن يكون الحد الأقصى لعدد تنفيذات عقدة القاعدة لكل رسالة سالبًا", + "max-emails": "الحد الأقصى لعدد الرسائل الإلكترونية المرسلة", + "max-emails-required": "الحد الأقصى لعدد الرسائل الإلكترونية المرسلة مطلوب.", + "max-emails-range": "لا يمكن أن يكون الحد الأقصى لعدد الرسائل الإلكترونية المرسلة سالبًا", + "sms-enabled": "تمكين SMS", + "max-sms": "الحد الأقصى لعدد الرسائل القصيرة المرسلة", + "max-sms-required": "الحد الأقصى لعدد الرسائل القصيرة المرسلة مطلوب.", + "max-sms-range": "لا يمكن أن يكون الحد الأقصى لعدد الرسائل القصيرة المرسلة سالبًا", + "max-created-alarms": "الحد الأقصى لعدد التنبيهات المُنشأة", + "max-created-alarms-required": "الحد الأقصى لعدد التنبيهات المُنشأة مطلوب.", + "max-created-alarms-range": "لا يمكن أن يكون الحد الأقصى لعدد التنبيهات المُنشأة سالبًا", + "no-queue": "لم يتم تكوين طابور", + "add-queue": "إضافة طابور", + "queues-with-count": "الطوابير ({{count}})", + "tenant-rest-limits": "طلبات REST للمستأجر", + "customer-rest-limits": "طلبات REST للعميل", + "incorrect-pattern-for-rate-limits": "التنسيق هو أزواج مفصولة بفواصل من السعة والفترة (بالثواني) مع وضع نقطتين بينهما، مثل 100:1,2000:60", + "too-small-value-zero": "يجب أن تكون القيمة أكبر من 0", + "too-small-value-one": "يجب أن تكون القيمة أكبر من 1", + "queue-size-is-limited-by-system-configuration": "حجم الطابور محدود أيضًا بواسطة تكوين النظام.", + "cassandra-tenant-limits-configuration": "استعلام Cassandra للمستأجر", + "ws-limit-max-sessions-per-tenant": "الحد الأقصى لعدد الجلسات لكل مستأجر", + "ws-limit-max-sessions-per-customer": "الحد الأقصى لعدد الجلسات لكل عميل", + "ws-limit-max-sessions-per-regular-user": "الحد الأقصى لعدد الجلسات لكل مستخدم عادي", + "ws-limit-max-sessions-per-public-user": "الحد الأقصى لعدد الجلسات لكل مستخدم عام", + "ws-limit-queue-per-session": "الحد الأقصى لحجم طابور الرسائل لكل جلسة", + "ws-limit-max-subscriptions-per-tenant": "الحد الأقصى لعدد الاشتراكات لكل مستأجر", + "ws-limit-max-subscriptions-per-customer": "الحد الأقصى لعدد الاشتراكات لكل عميل", + "ws-limit-max-subscriptions-per-regular-user": "الحد الأقصى لعدد الاشتراكات لكل مستخدم عادي", + "ws-limit-max-subscriptions-per-public-user": "الحد الأقصى لعدد الاشتراكات لكل مستخدم عام", + "ws-limit-updates-per-session": "تحديثات WS لكل جلسة", + "rate-limits": { + "add-limit": "إضافة حد", + "advanced-settings": "الإعدادات المتقدمة", + "edit-limit": "تعديل الحد", + "but-less-than": "ولكن أقل من", + "edit-transport-tenant-msg-title": "تعديل حدود معدل رسائل نقل المستأجر", + "edit-transport-tenant-telemetry-msg-title": "تعديل حدود معدل رسائل تليمتري نقل المستأجر", + "edit-transport-tenant-telemetry-data-points-title": "تعديل حدود معدل نقاط بيانات تليمتري نقل المستأجر", + "edit-transport-device-msg-title": "تعديل حدود معدل رسائل نقل الجهاز", + "edit-transport-device-telemetry-msg-title": "تعديل حدود معدل رسائل تليمتري نقل الجهاز", + "edit-transport-device-telemetry-data-points-title": "تعديل حدود معدل نقاط بيانات تليمتري نقل الجهاز", + "edit-tenant-rest-limits-title": "تعديل حدود معدل طلبات REST للمستأجر", + "edit-customer-rest-limits-title": "تعديل حدود معدل طلبات REST للعميل", + "edit-ws-limit-updates-per-session-title": "تعديل حدود معدل تحديثات WS لكل جلسة", + "edit-cassandra-tenant-limits-configuration-title": "تعديل حدود معدل استعلام Cassandra للمستأجر", + "edit-tenant-entity-export-rate-limit-title": "تعديل حدود معدل إنشاء نسخة الكيان", + "edit-tenant-entity-import-rate-limit-title": "تعديل حدود معدل تحميل نسخة الكيان", + "edit-tenant-notification-request-rate-limit-title": "تعديل حدود معدل طلبات الإخطار", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "تعديل حدود معدل طلبات الإخطار لكل قاعدة إخطار", + "edit-integration-msgs-per-tenant-rate-limit-title": "تعديل حدود معدل رسائل التكامل لكل مستأجر", + "edit-integration-msgs-per-device-rate-limit-title": "تعديل حدود معدل رسائل التكامل لكل جهاز", + "messages-per": "رسائل لكل", + "not-set": "غير مضبوط", + "number-of-messages": "عدد الرسائل", + "number-of-messages-required": "عدد الرسائل مطلوب.", + "number-of-messages-min": "القيمة الدنيا هي 1.", + "preview": "معاينة", + "per-seconds": "لكل ثانية", + "per-seconds-required": "معدل الزمن مطلوب.", + "per-seconds-min": "القيمة الدنيا هي 1.", + "rate-limits": "حدود المعدل", + "remove-limit": "إزالة الحد", + "transport-tenant-msg": "رسائل نقل المستأجر", + "transport-tenant-telemetry-msg": "رسائل تليمتري نقل المستأجر", + "transport-tenant-telemetry-data-points": "نقاط بيانات تليمتري نقل المستأجر", + "transport-device-msg": "رسائل نقل الجهاز", + "transport-device-telemetry-msg": "رسائل تليمتري نقل الجهاز", + "transport-device-telemetry-data-points": "نقاط بيانات تليمتري نقل الجهاز", + "sec": "ثانية" + } + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, =1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, =1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, =1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, =1 {1 day} other {# days} }", + "days": "أيام", + "hours": "ساعات", + "minutes": "دقائق", + "seconds": "ثواني", + "advanced": "متقدم", + "predefined": { + "yesterday": "الأمس", + "day-before-yesterday": "قبل يومين", + "this-day-last-week": "هذا اليوم من الأسبوع الماضي", + "previous-week": "الأسبوع السابق (الأحد - السبت)", + "previous-week-iso": "الأسبوع السابق (الاثنين - الأحد)", + "previous-month": "الشهر السابق", + "previous-quarter": "الربع السابق", + "previous-half-year": "نصف السنة السابق", + "previous-year": "السنة السابقة", + "current-hour": "الساعة الحالية", + "current-day": "اليوم الحالي", + "current-day-so-far": "حتى الآن في اليوم الحالي", + "current-week": "الأسبوع الحالي (الأحد - السبت)", + "current-week-iso": "الأسبوع الحالي (الاثنين - الأحد)", + "current-week-so-far": "حتى الآن في الأسبوع الحالي (الأحد - السبت)", + "current-week-iso-so-far": "حتى الآن في الأسبوع الحالي (الاثنين - الأحد)", + "current-month": "الشهر الحالي", + "current-month-so-far": "حتى الآن في الشهر الحالي", + "current-quarter": "الربع الحالي", + "current-quarter-so-far": "حتى الآن في الربع الحالي", + "current-half-year": "نصف السنة الحالي", + "current-half-year-so-far": "حتى الآن في نصف السنة الحالي", + "current-year": "السنة الحالية", + "current-year-so-far": "حتى الآن في السنة الحالية" + } + }, + "timeunit": { + "milliseconds": "ميلي ثانية", + "seconds": "ثواني", + "minutes": "دقائق", + "hours": "ساعات", + "days": "أيام" + }, + "timewindow": { + "timewindow": "نافذة الوقت", + "years": "{ years, plural, =1 { سنة } other {# سنوات } }", + "years-short": "{{ years }}س", + "months": "{ months, plural, =1 { شهر } other {# أشهر } }", + "months-short": "{{ months }}ش", + "weeks": "{ weeks, plural, =1 { أسبوع } other {# أسابيع } }", + "weeks-short": "{{ weeks }}أ", + "days": "{ days, plural, =1 { يوم } other {# أيام } }", + "days-short": "{{ days }}ي", + "hours": "{ hours, plural, =0 { ساعة } =1 {1 ساعة } other {# ساعات } }", + "hr": "{{ hr }} ساعة", + "hr-short": "{{ hr }}س", + "minutes": "{ minutes, plural, =0 { دقيقة } =1 {1 دقيقة } other {# دقائق } }", + "min": "{{ min }} دقيقة", + "min-short": "{{ min }}د", + "seconds": "{ seconds, plural, =0 { ثانية } =1 {1 ثانية } other {# ثواني } }", + "sec": "{{ sec }} ثانية", + "sec-short": "{{ sec }}ث", + "short": { + "days": "{ days, plural, =1 {1 يوم } other {# أيام } }", + "hours": "{ hours, plural, =1 {1 ساعة } other {# ساعات } }", + "minutes": "{{minutes}} دقيقة", + "seconds": "{{seconds}} ثانية" + }, + "realtime": "فوري", + "history": "تاريخ", + "last-prefix": "الأخير", + "period": "من {{ startTime }} إلى {{ endTime }}", + "edit": "تعديل نافذة الوقت", + "date-range": "نطاق التاريخ", + "for-all-time": "لكل الوقت", + "last": "الأخير", + "time-period": "فترة زمنية", + "hide": "إخفاء", + "interval": "فاصل", + "just-now": "الآن", + "just-now-lower": "الآن", + "ago": "منذ", + "style": "نمط نافذة الوقت", + "icon": "أيقونة", + "icon-position": "موقع الأيقونة", + "icon-position-left": "اليسار", + "icon-position-right": "اليمين", + "font": "الخط", + "color": "اللون", + "displayTypePrefix": "عرض البادئة الفوري/التاريخي", + "preview": "معاينة" + }, + "unit": { + "millimeter": "مليمتر", + "centimeter": "سنتيمتر", + "angstrom": "أنغستروم", + "nanometer": "نانومتر", + "micrometer": "ميكرومتر", + "meter": "متر", + "kilometer": "كيلومتر", + "inch": "بوصة", + "foot": "قدم", + "yard": "ياردة", + "mile": "ميل", + "nautical-mile": "الميل البحري", + "astronomical-unit": "الوحدة الفلكية", + "reciprocal-metre": "المتر المعكوس", + "meter-per-meter": "متر لكل متر", + "steradian": "ستيراديان", + "thou": "الحبة", + "barleycorn": "حبة الشعير", + "hand": "يد", + "chain": "سلسلة", + "furlong": "فارسخ", + "league": "ليغ", + "fathom": "قدم مربعة", + "cable": "كابل", + "link": "رابط", + "rod": "عصا", + "nanogram": "نانوغرام", + "microgram": "ميكروغرام", + "milligram": "مليغرام", + "gram": "جرام", + "kilogram": "كيلوجرام", + "tonne": "طن", + "ounce": "أونصة", + "pound": "رطل", + "stone": "حجر", + "hundredweight-count": "مئوية الوزن", + "short-tons": "أطنان قصيرة", + "dalton": "دالتون", + "grain": "حبة", + "drachm": "دراخم", + "quarter": "ربع", + "slug": "سلغ", + "carat": "قيراط", + "cubic-millimeter": "مليمتر مكعب", + "cubic-centimeter": "سنتيمتر مكعب", + "cubic-meter": "متر مكعب", + "cubic-kilometer": "كيلومتر مكعب", + "microliter": "ميكروليتر", + "milliliter": "مليلتر", + "liter": "لتر", + "hectoliter": "هكتوليتر", + "cubic-inch": "بوصة مكعبة", + "cubic-foot": "قدم مكعبة", + "cubic-yard": "ياردة مكعبة", + "fluid-ounce": "أونصة سائلة", + "pint": "باينت", + "quart": "كوارت", + "gallon": "جالون", + "oil-barrels": "برميل نفط", + "cubic-meter-per-kilogram": "متر مكعب لكل كيلوجرام", + "gill": "جل", + "hogshead": "هوغزهيد", + "teaspoon": "ملعقة شاي", + "tablespoon": "ملعقة طعام", + "cup": "كوب", + "celsius": "درجة مئوية", + "kelvin": "كلفن", + "rankine": "رانكين", + "fahrenheit": "فهرنهايت", + "percent": "بالمئة", + "meter-per-second": "متر في الثانية", + "kilometer-per-hour": "كيلومتر في الساعة", + "foot-per-second": "قدم في الثانية", + "mile-per-hour": "ميل في الساعة", + "knot": "عقدة", + "millimeters-per-minute": "مليمتر في الدقيقة", + "kilometer-per-hour-squared": "كيلومتر في الساعة مربع", + "foot-per-second-squared": "قدم في الثانية مربع", + "pascal": "باسكال", + "kilopascal": "كيلوباسكال", + "megapascal": "ميجاباسكال", + "gigapascal": "جيجاباسكال", + "millibar": "مليبار", + "bar": "بار", + "kilobar": "كيلوبار", + "newton": "نيوتن", + "newton-meter": "نيوتن متر", + "foot-pounds": "قدم-رطل", + "inch-pounds": "بوصة-رطل", + "newton-per-meter": "نيوتن لكل متر", + "atmospheres": "الغلاف الجوي", + "pounds-per-square-inch": "رطل لكل بوصة مربعة", + "torr": "تور", + "inches-of-mercury": "بوصة زئبق", + "pascal-per-square-meter": "باسكال لكل متر مربع", + "pound-per-square-inch": "رطل لكل بوصة مربعة", + "newton-per-square-meter": "نيوتن لكل متر مربع", + "kilogram-force-per-square-meter": "قوة كيلوجرام لكل متر مربع", + "pascal-per-square-centimeter": "باسكال لكل سنتيمتر مربع", + "ton-force-per-square-inch": "قوة الطن لكل بوصة مربعة", + "kilonewton-per-square-meter": "كيلونيوتن لكل متر مربع", + "newton-per-square-millimeter": "نيوتن لكل مليمتر مربع", + "microjoule": "ميكروجول", + "millijoule": "ميليجول", + "joule": "جول", + "kilojoule": "كيلوجول", + "megajoule": "ميجاجول", + "gigajoule": "جيجاجول", + "watt-hour": "واط-ساعة", + "kilowatt-hour": "كيلوواط-ساعة", + "electron-volts": "إلكترون فولت", + "joules-per-coulomb": "جول لكل كولومب", + "british-thermal-unit": "وحدة حرارية بريطانية", + "foot-pound": "قدم-رطل", + "calorie": "كالوري", + "small-calorie": "كالوري صغيرة", + "kilocalorie": "كيلوكالوري", + "joule-per-kelvin": "جول لكل كلفن", + "joule-per-kilogram-kelvin": "جول لكل كيلوجرام-كلفن", + "joule-per-kilogram": "جول لكل كيلوجرام", + "watt-per-meter-kelvin": "واط لكل متر-كلفن", + "joule-per-cubic-meter": "جول لكل متر مكعب", + "therm": "تيرم", + "electric-dipole-moment": "عزم الدب الكهربائي", + "magnetic-dipole-moment": "عزم الدب المغناطيسي", + "debye": "ديباي", + "coulomb-per-square-meter-per-volt": "كولوم لكل متر مربع لكل فولت", + "milliwatt": "ميليوات", + "microwatt": "ميكرووات", + "watt": "واط", + "kilowatt": "كيلوواط", + "megawatt": "ميغاواط", + "gigawatt": "جيغاواط", + "metric-horsepower": "قوة حصان متري", + "milliwatt-per-square-centimeter": "ميليوات لكل سنتيمتر مربع", + "watt-per-square-centimeter": "واط لكل سنتيمتر مربع", + "kilowatt-per-square-centimeter": "كيلوواط لكل سنتيمتر مربع", + "milliwatt-per-square-meter": "ميليوات لكل متر مربع", + "watt-per-square-meter": "واط لكل متر مربع", + "kilowatt-per-square-meter": "كيلوواط لكل متر مربع", + "watt-per-square-inch": "واط لكل بوصة مربعة", + "kilowatt-per-square-inch": "كيلوواط لكل بوصة مربعة", + "horsepower": "حصان", + "btu-per-hour": "وحدة حرارية بريطانية في الساعة", + "coulomb": "كولومب", + "millicoulomb": "ميليكولومب", + "microcoulomb": "ميكروكولومب", + "picocoulomb": "بيكوكولومب", + "coulomb-per-meter": "كولومب لكل متر", + "coulomb-per-cubic-meter": "كولومب لكل متر مكعب", + "coulomb-per-square-meter": "كولومب لكل متر مربع", + "square-millimeter": "مليمتر مربع", + "square-centimeter": "سنتيمتر مربع", + "square-meter": "متر مربع", + "hectare": "هكتار", + "square-kilometer": "كيلومتر مربع", + "square-inch": "بوصة مربعة", + "square-foot": "قدم مربعة", + "square-yard": "ياردة مربعة", + "acre": "فدان", + "square-mile": "ميل مربع", + "are": "فدان", + "barn": "بارن", + "circular-inch": "بوصة دائرية", + "milliampere-hour": "مللي أمبير-ساعة", + "milliampere-hour-tags": "تيار كهربائي، تدفق التيار، شحنة كهربائية، سعة التيار، تدفق الكهرباء، تدفق كهربائي، مللي أمبير-ساعة، مللي أمبير-ساعات، مللي أمبير-ساعة", + "ampere-hours": "أمبير-ساعة", + "ampere-hours-tags": "تيار كهربائي، تدفق التيار، شحنة كهربائية، سعة التيار، تدفق الكهرباء، تدفق كهربائي، أمبير، أمبير-ساعة، أمبير-ساعة", + "kiloampere-hours": "كيلوأمبير-ساعة", + "kiloampere-hours-tags": "تيار كهربائي، تدفق التيار، شحنة كهربائية، سعة التيار، تدفق الكهرباء، تدفق كهربائي، كيلوأمبير-ساعة، كيلوأمبير-ساعة، كيلو أمبير-ساعة", + "nanoampere": "نانوأمبير", + "nanoampere-tags": "تيار، أمبير، نانوأمبير، نانوأمبير", + "picoampere": "بيكوأمبير", + "picoampere-tags": "تيار، أمبير، بيكوأمبير، بيكوأمبير", + "microampere": "ميكروأمبير", + "microampere-tags": "تيار كهربائي، ميكروأمبير، ميكروأمبيرات، ميكرو أمبير", + "milliampere": "ملليأمبير", + "milliampere-tags": "تيار كهربائي، ملليأمبير، مللي أمبير، مللي أمبيرات", + "ampere": "أمبير", + "ampere-tags": "تيار كهربائي، تدفق التيار، تدفق الكهرباء، تدفق كهربائي، أمبير، أمبيرات، تيار كهربائي، أمبير", + "kiloamperes": "كيلوأمبيرات", + "kiloamperes-tags": "تيار كهربائي، تدفق التيار، كيلوأمبيرات، كيلو أمبير", + "microampere-per-square-centimeter": "ميكروأمبير لكل سنتيمتر مربع", + "microampere-per-square-centimeter-tags": "كثافة التيار، ميكروأمبير لكل سنتيمتر مربع، ميكروأمبير لكل سم²", + "ampere-per-square-meter": "أمبير لكل متر مربع", + "ampere-per-square-meter-tags": "كثافة التيار، التيار لكل وحدة مساحة، أمبير لكل متر مربع", + "ampere-per-meter": "أمبير لكل متر", + "ampere-per-meter-tags": "قوة المجال المغناطيسي، شدة المجال المغناطيسي، أمبير لكل متر", + "oersted": "أورستد", + "oersted-tags": "مجال مغناطيسي، أورستد", + "bohr-magneton": "مغناطون بور", + "bohr-magneton-tags": "فيزياء ذرية، عزم مغناطيسي، مغناطون بور، ميكرو بور", + "ampere-meter-squared": "أمبير-متر مربع", + "ampere-meter-squared-tags": "عزم مغناطيسي، عزم قطبي، أمبير-متر مربع", + "ampere-meter": "أمبير-متر", + "ampere-meter-tags": "مجال مغناطيسي، حلقة تيار، أمبير-متر", + "nanovolt": "نانوفولت", + "picovolt": "بيكوفولت", + "millivolts": "ملليفولت", + "microvolts": "ميكروفولت", + "volt": "فولت", + "kilovolts": "كيلوفولت", + "dbmV": "ديسيبل ملليفولت", + "dbm": "ديسيبل ميلليوات", + "volt-meter": "فولت-متر", + "kilovolt-meter": "كيلوفولت-متر", + "megavolt-meter": "ميغافولت-متر", + "microvolt-meter": "ميكروفولت-متر", + "millivolt-meter": "ملليفولت-متر", + "nanovolt-meter": "نانوفولت-متر", + "ohm": "أوم", + "microohm": "ميكروأوم", + "milliohm": "ملليأوم", + "kilohm": "كيلوأوم", + "megohm": "ميغاأوم", + "gigohm": "جيغاأوم", + "hertz": "هرتز", + "kilohertz": "كيلوهرتز", + "megahertz": "ميغاهرتز", + "gigahertz": "جيغاهرتز", + "rpm": "دورة في الدقيقة", + "candela-per-square-meter": "كانديلا لكل متر مربع", + "candela": "كانديلا", + "lumen": "لومن", + "lux": "لوكس", + "foot-candle": "قدم كانديلا", + "lumen-per-square-meter": "لومن لكل متر مربع", + "lux-second": "لوكس ثانية", + "lumen-second": "لومن ثانية", + "lumens-per-watt": "لومن لكل واط", + "absorbance": "امتصاصية", + "mole": "مول", + "nanomole": "نانومول", + "micromole": "ميكرومول", + "millimole": "ملليمول", + "kilomole": "كيلومول", + "mole-per-cubic-meter": "مول لكل متر مكعب", + "rssi": "RSSI", + "ppm": "أجزاء في المليون", + "ppb": "أجزاء في البليون", + "micrograms-per-cubic-meter": "ميكروغرام لكل متر مكعب", + "aqi": "مؤشر جودة الهواء", + "gram-per-cubic-meter": "جرام لكل متر مكعب", + "gram-per-kilogram": "الرطوبة النوعية", + "millimeters-per-second": "ملليمتر في الثانية", + "neper": "نيبر", + "bel": "بيل", + "decibel": "ديسيبل", + "meters-per-second-squared": "أمتار في الثانية مربع", + "becquerel": "بكريل", + "curie": "كيوري", + "gray": "غراي", + "sievert": "سيفيرت", + "roentgen": "رينجن", + "cps": "عدّات في الثانية", + "rad": "راد", + "rem": "ريم", + "dps": "انحلالات في الثانية", + "rutherford": "روثرفورد", + "coulombs-per-kilogram": "كولوم لكل كيلوغرام", + "becquerels-per-cubic-meter": "بيكريل لكل متر مكعب", + "curies-per-liter": "كيوري لكل ليتر", + "becquerels-per-second": "بيكريل لكل ثانية", + "curies-per-second": "كيوري لكل ثانية", + "gy-per-second": "غراي لكل ثانية", + "watt-per-steradian": "واط لكل ستيراديان", + "watt-per-square-metre-steradian": "واط لكل متر مربع ستيراديان", + "ph-level": "مستوى الحموضة", + "turbidity": "عكورة", + "mg-per-liter": "ملليغرام لكل ليتر", + "microsiemens-per-centimeter": "ميكروسيمنز لكل سنتيمتر", + "millisiemens-per-meter": "ملليسيمنز لكل متر", + "siemens-per-meter": "سيمنز لكل متر", + "kilogram-per-cubic-meter": "كيلوغرام لكل متر مكعب", + "gram-per-cubic-centimeter": "جرام لكل سنتيمتر مكعب", + "kilogram-per-square-meter": "كيلوغرام لكل متر مربع", + "milligram-per-milliliter": "ملليغرام لكل ملليلتر", + "milligram-per-cubic-meter": "ملليغرام لكل متر مكعب", + "pound-per-cubic-foot": "جنيه لكل قدم مكعب", + "ounces-per-cubic-inch": "أونصة لكل بوصة مكعبة", + "tons-per-cubic-yard": "طن لكل ياردة مكعبة", + "particle-density": "كثافة الجسيمات", + "kilometers-per-liter": "كيلومتر لكل ليتر", + "miles-per-gallon": "ميل لكل جالون", + "liters-per-100-km": "ليتر لكل 100 كيلومتر", + "gallons-per-mile": "جالون لكل ميل", + "liters-per-hour": "ليتر لكل ساعة", + "gallons-per-hour": "جالون لكل ساعة", + "beats-per-minute": "نبضة في الدقيقة", + "millimeters-of-mercury": "ملليمتر زئبق", + "milligrams-per-deciliter": "ملليغرام لكل ديسيليتر", + "g-force": "قوة الجاذبية", + "kilonewton": "كيلونيوتن", + "kilogram-force": "كيلوغرام-قوة", + "pound-force": "باوند-قوة", + "kilopound-force": "كيلوباوند-قوة", + "dyne": "دين", + "poundal": "باوندل", + "kip": "كيب", + "gal": "جال", + "gravity": "الجاذبية", + "hectopascal": "هكتوباسكال", + "atmosphere": "الغلاف الجوي", + "millibars": "مليبار", + "inch-of-mercury": "بوصة زئبق", + "richter-scale": "مقياس ريختر", + "second": "ثانية", + "minute": "دقيقة", + "hour": "ساعة", + "day": "يوم", + "week": "أسبوع", + "month": "شهر", + "year": "سنة", + "cubic-foot-per-minute": "قدم مكعب في الدقيقة", + "cubic-meters-per-hour": "متر مكعب في الساعة", + "cubic-meters-per-second": "متر مكعب في الثانية", + "liter-per-second": "لتر في الثانية", + "liter-per-minute": "لتر في الدقيقة", + "gallons-per-minute": "جالون في الدقيقة", + "cubic-foot-per-second": "قدم مكعب في الثانية", + "milliliters-per-minute": "ملليلتر في الدقيقة", + "bit": "بت", + "byte": "بايت", + "kilobyte": "كيلوبايت", + "megabyte": "ميجابايت", + "gigabyte": "غيغابايت", + "terabyte": "تيرابايت", + "petabyte": "بيتابايت", + "exabyte": "إكسابايت", + "zettabyte": "زيتابايت", + "yottabyte": "يوتابايت", + "bit-per-second": "بت في الثانية", + "kilobit-per-second": "كيلوبت في الثانية", + "megabit-per-second": "ميجابت في الثانية", + "gigabit-per-second": "غيغابت في الثانية", + "terabit-per-second": "تيرابت في الثانية", + "byte-per-second": "بايت في الثانية", + "kilobyte-per-second": "كيلوبايت في الثانية", + "megabyte-per-second": "ميجابايت في الثانية", + "gigabyte-per-second": "غيغابايت في الثانية", + "degree": "درجة", + "radian": "راديان", + "gradian": "غريد", + "mil": "ميل", + "revolution": "دورة", + "siemens": "سيمنز", + "millisiemens": "ميليسيمنز", + "microsiemens": "ميكروسيمنز", + "kilosiemens": "كيلوسيمنز", + "megasiemens": "ميغاسيمنز", + "gigasiemens": "جيجاسيمنز", + "farad": "فاراد", + "millifarad": "مليفاراد", + "microfarad": "ميكروفاراد", + "nanofarad": "نانوفاراد", + "picofarad": "بيكوفاراد", + "kilofarad": "كيلوفاراد", + "megafarad": "ميغافاراد", + "gigafarad": "جيجافاراد", + "terfarad": "تيرفاراد", + "farad-per-meter": "فاراد لكل متر", + "tesla": "تسلا", + "gauss": "جاوس", + "gauss-per-centimeter": "جاوس لكل سنتيمتر", + "gauss-square-centimeter": "جاوس-مربع سنتيمتر", + "henry": "هنري", + "henry-per-meter": "هنري لكل متر", + "kilogauss-square-centimeter": "كيلوغاوس-سنتيمتر مربع", + "microhenry": "مايكروهنري", + "microweber": "ميكرويبر", + "kilogauss": "كيلوجاوس", + "millitesla": "ميليتسلا", + "microtesla": "ميكروتسلا", + "nanotesla": "نانوتسلا", + "kilotesla": "كيلوتسلا", + "megatesla": "ميغاتسلا", + "millitesla-square-meters": "ميليتسلا متر مربع", + "millihenry": "ميلي هنري", + "gamma": "جاما", + "lambda": "لامبدا", + "square-meter-per-second": "متر مربع في الثانية", + "square-centimeter-per-second": "سنتيمتر مربع في الثانية", + "stoke": "ستوك", + "centistokes": "سنتيستوك", + "square-foot-per-second": "قدم مربع في الثانية", + "square-inch-per-second": "بوصة مربعة في الثانية", + "pascal-second": "باسكال ثانية", + "centipoise": "سنتيبويز", + "poise": "بويز", + "reynolds": "رينولدز", + "pound-per-foot-hour": "جنيه في القدم في الساعة", + "newton-second-per-square-meter": "نيوتن ثانية لكل متر مربع", + "dyne-second-per-square-centimeter": "دين ثانية لكل سنتيمتر مربع", + "kilogram-per-meter-second": "كيلوجرام في المتر في الثانية", + "tesla-square-meters": "تسلا متر مربع", + "maxwell": "ماكسويل", + "tesla-per-meter": "تسلا لكل متر", + "gauss-per-oersted": "جاوس لكل أورستد", + "kilogram-per-mole": "كيلوجرام لكل مول", + "gram-per-mole": "غرام لكل مول", + "milligram-per-mole": "ملليغرام لكل مول", + "joule-per-mole": "جول لكل مول", + "joule-per-mole-kelvin": "جول لكل مول كلفن", + "millivolts-per-meter": "مليفولت لكل متر", + "volts-per-meter": "فولت لكل متر", + "kilovolts-per-meter": "كيلوفولت لكل متر", + "radian-per-second": "راديان في الثانية", + "radian-per-second-squared": "راديان في الثانية مربعة", + "revolutions-per-minute-per-second": "دورة في الدقيقة في الثانية", + "revolutions-per-minute-per-second-squared": "دورة في الدقيقة في الثانية مربعة", + "deg-per-second": "درجة في الثانية", + "milliweber": "ميلي ويبر", + "degrees-brix": "درجات بريكس", + "nanohenry": "نانوهنري", + "tesla-meter-per-ampere": "تسلا متر في الأمبير", + "weber": "ويبر", + "katal": "كتال", + "katal-per-cubic-metre": "كتال لكل متر مكعب" + }, + "user": { + "all": "الكل", + "all-users": "جميع المستخدمين", + "groups": "المجموعات", + "user": "المستخدم", + "users": "المستخدمون", + "management": "إدارة المستخدمين", + "customer-users": "مستخدمو العملاء", + "tenant-admins": "مسؤولو المستأجر", + "sys-admin": "مسؤول النظام", + "tenant-admin": "مسؤول المستأجر", + "customer": "العميل", + "anonymous": "مجهول", + "add": "إضافة مستخدم", + "delete": "حذف مستخدم", + "add-user-text": "إضافة مستخدم جديد", + "no-users-text": "لم يتم العثور على مستخدمين", + "user-details": "تفاصيل المستخدم", + "delete-users": "حذف المستخدمين", + "delete-user-title": "هل أنت متأكد أنك تريد حذف المستخدم '{{userEmail}}'؟", + "delete-user-text": "كن حذرًا، بعد التأكيد ستصبح بيانات المستخدم وكل البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-users-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {مستخدم واحد} other {# مستخدمين} }؟", + "delete-users-action-title": "حذف { count, plural, =1 {مستخدم واحد} other {# مستخدمين} }", + "delete-users-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع المستخدمين المحددين وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد.", + "activation-email-sent-message": "تم إرسال بريد التنشيط بنجاح!", + "resend-activation": "إعادة إرسال التنشيط", + "email": "البريد الإلكتروني", + "email-required": "البريد الإلكتروني مطلوب.", + "invalid-email-format": "تنسيق البريد الإلكتروني غير صالح.", + "first-name": "الاسم الأول", + "last-name": "الاسم الأخير", + "description": "الوصف", + "default-dashboard": "لوحة المعلومات الافتراضية", + "always-fullscreen": "دائمًا كامل الشاشة", + "select-user": "اختيار المستخدم", + "no-users-matching": "لم يتم العثور على مستخدمين مطابقين لـ '{{entity}}'.", + "user-required": "المستخدم مطلوب", + "activation-method": "طريقة التنشيط", + "display-activation-link": "عرض رابط التنشيط", + "send-activation-mail": "إرسال بريد التنشيط", + "activation-link": "رابط تنشيط المستخدم", + "activation-link-text": "لتنشيط المستخدم، استخدم الرابط التالي: رابط التنشيط :", + "copy-activation-link": "نسخ رابط التنشيط", + "activation-link-copied-message": "تم نسخ رابط تنشيط المستخدم إلى الحافظة", + "details": "التفاصيل", + "login-as-tenant-admin": "تسجيل الدخول كمسؤول المستأجر", + "login-as-customer-user": "تسجيل الدخول كمستخدم العميل", + "select-group-to-add": "حدد المجموعة المستهدفة لإضافة المستخدمين المحددين", + "select-group-to-move": "حدد المجموعة المستهدفة لنقل المستخدمين المحددين", + "remove-users-from-group": "هل أنت متأكد أنك تريد إزالة { count, plural, =1 {مستخدم واحد} other {# مستخدمين} } من المجموعة '{{entityGroup}}'؟", + "group": "مجموعة المستخدمين", + "list-of-groups": "{ count, plural, =1 {مجموعة مستخدم واحدة} other {قائمة من # مجموعات المستخدمين} }", + "group-name-starts-with": "مجموعات المستخدمين التي تبدأ أسماؤها بـ '{{prefix}}'", + "search": "البحث عن المستخدمين", + "selected-users": "{ count, plural, =1 {مستخدم واحد} other {# مستخدمين} } محدد", + "disable-account": "تعطيل حساب المستخدم", + "enable-account": "تمكين حساب المستخدم", + "enable-account-message": "تم تمكين حساب المستخدم بنجاح!", + "disable-account-message": "تم تعطيل حساب المستخدم بنجاح!", + "copyId": "نسخ معرف المستخدم", + "idCopiedMessage": "تم نسخ معرف المستخدم إلى الحافظة", + "user-list": "قائمة المستخدمين", + "user-list-required": "قائمة المستخدمين مطلوبة" + }, + "value": { + "type": "نوع القيمة", + "string": "سلسلة نصية", + "string-value": "قيمة سلسلة نصية", + "string-value-required": "القيمة النصية مطلوبة", + "integer": "عدد صحيح", + "integer-value": "قيمة عدد صحيح", + "integer-value-required": "القيمة الصحيحة مطلوبة", + "invalid-integer-value": "قيمة عدد صحيح غير صالحة", + "double": "عدد عشري", + "double-value": "قيمة عدد عشري", + "double-value-required": "القيمة العشرية مطلوبة", + "boolean": "قيمة منطقية", + "boolean-value": "قيمة منطقية", + "false": "خطأ", + "true": "صحيح", + "long": "عدد طويل", + "json": "JSON", + "json-value": "قيمة JSON", + "json-value-invalid": "تنسيق قيمة JSON غير صالح", + "json-value-required": "القيمة JSON مطلوبة." + }, + "version-control": { + "version-control": "التحكم بالنسخ", + "management": "إدارة التحكم بالنسخ", + "search": "بحث النسخ", + "branch": "فرع", + "default": "افتراضي", + "select-branch": "اختر فرع", + "branch-required": "الفرع مطلوب", + "create-entity-version": "إنشاء نسخة الكيان", + "version-name": "اسم النسخة", + "version-name-required": "اسم النسخة مطلوب", + "author": "المؤلف", + "export-relations": "تصدير العلاقات", + "export-attributes": "تصدير الصفات", + "export-credentials": "تصدير بيانات الاعتماد", + "export-group-entities": "تصدير مجموعات الكيانات", + "export-roles": "تصدير الأدوار", + "entity-versions": "نسخ الكيان", + "versions": "النسخ", + "created-time": "وقت الإنشاء", + "version-id": "معرف النسخة", + "no-entity-versions-text": "لم يتم العثور على نسخ كيانات", + "no-versions-text": "لم يتم العثور على نسخ", + "copy-full-version-id": "نسخ معرف النسخة كامل", + "create-version": "إنشاء نسخة", + "creating-version": "جاري إنشاء النسخة... الرجاء الانتظار", + "nothing-to-commit": "لا يوجد تغييرات للتنفيذ", + "restore-version": "استعادة النسخة", + "restore-entity-from-version": "استعادة الكيان من النسخة '{{versionName}}'", + "restoring-entity-version": "جاري استعادة نسخة الكيان... الرجاء الانتظار", + "load-relations": "تحميل العلاقات", + "load-attributes": "تحميل الصفات", + "load-credentials": "تحميل بيانات الاعتماد", + "load-group-entities": "تحميل مجموعات الكيانات", + "load-roles": "تحميل الأدوار", + "compare-with-current": "مقارنة مع الحالي", + "diff-entity-with-version": "الفرق مع نسخة الكيان '{{versionName}}'", + "previous-difference": "الفرق السابق", + "next-difference": "الفرق التالي", + "current": "الحالي", + "differences": "{ count, plural, =1 {فرق واحد} other {# فروقات} }", + "create-entities-version": "إنشاء نسخة الكيانات", + "default-sync-strategy": "استراتيجية المزامنة الافتراضية", + "sync-strategy-merge": "دمج", + "sync-strategy-overwrite": "الكتابة فوق", + "entities-to-export": "الكيانات للتصدير", + "entities-to-restore": "الكيانات للاستعادة", + "sync-strategy": "استراتيجية المزامنة", + "all-entities": "جميع الكيانات", + "no-entities-to-export-prompt": "يرجى تحديد الكيانات للتصدير", + "no-entities-to-restore-prompt": "يرجى تحديد الكيانات للاستعادة", + "add-entity-type": "إضافة نوع الكيان", + "remove-all": "إزالة الكل", + "version-create-result": "{ added, plural, =0 {لم تتم إضافة كيانات} =1 {كيان واحد} other {# كيانات} } تمت إضافته.
{ modified, plural, =0 {لم تتم تعديل كيانات} =1 {كيان واحد} other {# كيانات} } تم تعديله.
{ removed, plural, =0 {لم تتم إزالة كيانات} =1 {كيان واحد} other {# كيانات} } تمت إزالته.", + "remove-other-entities": "إزالة الكيانات الأخرى", + "find-existing-entity-by-name": "البحث عن كيان موجود بالاسم", + "restore-entities-from-version": "استعادة الكيانات من النسخة '{{versionName}}'", + "restoring-entities-from-version": "جاري استعادة الكيانات... الرجاء الانتظار", + "no-entities-restored": "لم يتم استعادة كيانات", + "created": "{{created}} تم الإنشاء", + "updated": "{{updated}} تم التحديث", + "deleted": "{{deleted}} تم الحذف", + "groups-created": "{ created, plural, =1 {مجموعة واحدة} other {# مجموعات} } تم إنشاؤها", + "groups-updated": "{ updated, plural, =1 {مجموعة واحدة} other {# مجموعات} } تم تحديثها", + "groups-deleted": "{ deleted, plural, =1 {مجموعة واحدة} other {# مجموعات} } تم حذفها", + "remove-other-entities-confirm-text": "كن حذرًا! هذا سيحذف بشكل دائم جميع الكيانات الحالية
غير الموجودة في النسخة التي تريد استعادتها.

الرجاء كتابة \"إزالة الكيانات الأخرى\" للتأكيد.", + "auto-commit-to-branch": "التنفيذ التلقائي إلى الفرع {{ branch }}", + "default-create-entity-version-name": "تحديث {{entityName}}", + "sync-strategy-merge-hint": "ينشئ أو يحدث الكيانات المختارة في المستودع. جميع الكيانات الأخرى في المستودع لا يتم تعديلها.", + "sync-strategy-overwrite-hint": "ينشئ أو يحدث الكيانات المختارة في المستودع. جميع الكيانات الأخرى في المستودع يتم حذفها.", + "device-credentials-conflict": "فشل في تحميل الجهاز بالمعرف الخارجي {{entityId}}
بسبب وجود بيانات اعتماد متطابقة في قاعدة البيانات لجهاز آخر.
يرجى النظر في تعطيل إعداد تحميل بيانات الاعتماد في نموذج الاستعادة.", + "missing-referenced-entity": "فشل في تحميل {{sourceEntityTypeName}} بالمعرف الخارجي {{sourceEntityId}}
لأنه يشير إلى {{targetEntityTypeName}} مفقود بالمعرف {{targetEntityId}}.", + "add-entity-groups": "إضافة مجموعات الكيانات", + "entity-groups": "مجموعات الكيانات", + "integration-routing-key-conflict": "فشل في تحميل التكامل بالمعرف الخارجي {{entityId}}
بسبب وجود مفتاح تكامل متطابق في قاعدة البيانات لتكامل آخر.
يرجى النظر في تفعيل إعداد إنشاء مفتاح تكامل تلقائي في نموذج الاستعادة.", + "auto-generate-integration-key": "إنشاء مفتاح التكامل تلقائيًا", + "runtime-failed": "فشل: {{message}}", + "auto-commit-settings-read-only-hint": "ميزة التنفيذ التلقائي لا تعمل مع تفعيل خيار القراءة فقط في إعدادات المستودع." + }, + "widget": { + "widget-library": "مكتبة الأدوات", + "widget-bundle": "حزمة الأدوات", + "all-bundles": "جميع الحزم", + "select-widgets-bundle": "اختر حزمة الأدوات", + "widgets": "الأدوات", + "all-widgets": "جميع الأدوات", + "widget": "أداة", + "select-widget": "اختر أداة", + "no-widgets-matching": "لا توجد أدوات مطابقة لـ '{{entity}}'.", + "no-widgets": "لا توجد أدوات بعد", + "no-widgets-text": "لم يتم العثور على أدوات", + "management": "إدارة الأدوات", + "editor": "محرر الأدوات", + "confirm-to-exit-editor-html": "لديك إعدادات أداة غير محفوظة.
هل أنت متأكد أنك تريد مغادرة هذه الصفحة؟", + "widget-type-not-found": "مشكلة في تحميل تكوين الأداة.
ربما تم إزالة نوع الأداة المرتبط.", + "widget-type-load-error": "لم يتم تحميل الأداة بسبب الأخطاء التالية:", + "remove": "إزالة الأداة", + "delete": "حذف الأداة", + "edit": "تعديل الأداة", + "remove-widget-title": "هل أنت متأكد أنك تريد إزالة الأداة '{{widgetTitle}}'؟", + "remove-widget-text": "بعد التأكيد، ستصبح الأداة وجميع البيانات ذات الصلة غير قابلة للاسترجاع.", + "timeseries": "سلاسل زمنية", + "search-data": "بحث البيانات", + "no-data-found": "لم يتم العثور على بيانات", + "latest": "آخر القيم", + "rpc": "أداة التحكم", + "alarm": "أداة التنبيه", + "static": "أداة ثابتة", + "timeseries-short": "سلسلة", + "latest-short": "آخر", + "rpc-short": "تحكم", + "alarm-short": "تنبيه", + "static-short": "ثابت", + "select-widget-type": "اختر نوع الأداة", + "missing-widget-title-error": "يجب تحديد عنوان الأداة!", + "widget-saved": "تم حفظ الأداة", + "unable-to-save-widget-error": "تعذر حفظ الأداة! الأداة تحتوي على أخطاء!", + "save": "حفظ الأداة", + "saveAs": "حفظ الأداة باسم", + "move": "نقل الأداة", + "save-widget-as": "حفظ الأداة باسم", + "save-widget-as-text": "الرجاء إدخال عنوان جديد للأداة", + "toggle-fullscreen": "تبديل وضع ملء الشاشة", + "run": "تشغيل الأداة", + "widget-title": "عنوان الأداة", + "title": "العنوان", + "title-required": "عنوان الأداة مطلوب.", + "title-max-length": "يجب أن يكون العنوان أقل من 256", + "system": "نظام", + "type": "نوع الأداة", + "resources": "الموارد", + "resource-url": "رابط JavaScript/CSS", + "resource-is-module": "هو وحدة", + "remove-resource": "إزالة المورد", + "add-resource": "إضافة مورد", + "html": "HTML", + "tidy": "تنظيم", + "css": "CSS", + "settings-schema": "مخطط الإعدادات", + "datakey-settings-schema": "مخطط إعدادات مفتاح البيانات", + "latest-datakey-settings-schema": "أحدث مخطط لإعدادات مفتاح البيانات", + "widget-settings": "إعدادات الأداة", + "description": "الوصف", + "tags": "العلامات", + "image-preview": "معاينة الصورة", + "settings-form-selector": "محدد نموذج الإعدادات", + "data-key-settings-form-selector": "محدد نموذج إعدادات مفتاح البيانات", + "latest-data-key-settings-form-selector": "محدد نموذج أحدث إعدادات مفتاح البيانات", + "all": "الكل", + "actual": "فعلي", + "deprecated": "مهجور", + "has-basic-mode": "يحتوي على وضع أساسي", + "basic-mode-form-selector": "محدد نموذج الوضع الأساسي", + "basic-mode": "أساسي", + "advanced-mode": "متقدم", + "javascript": "جافاسكربت", + "js": "JS", + "delete-widget-title": "هل أنت متأكد أنك تريد حذف الأداة '{{widgetName}}'؟", + "delete-widget-text": "بعد التأكيد، ستصبح الأداة وجميع البيانات ذات الصلة غير قابلة للاسترجاع.", + "delete-widgets-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {أداة واحدة} other {# أدوات} }؟", + "delete-widgets-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع الأدوات المحددة وجميع البيانات ذات الصلة غير قابلة للاسترجاع.", + "delete-widget": "حذف الأداة", + "widget-template-load-failed-error": "فشل في تحميل قالب الأداة!", + "details": "تفاصيل", + "widget-details": "تفاصيل الأداة", + "add": "إضافة أداة", + "add-existing-widget": "إضافة أداة موجودة", + "add-new-widget": "إضافة أداة جديدة", + "search-widgets": "بحث الأدوات", + "selected-widgets": "{ count, plural, =1 {أداة واحدة} other {# أدوات} } مختارة", + "undo": "تراجع عن تغييرات الأداة", + "export": "تصدير الأداة", + "export-widgets": "تصدير الأدوات", + "export-data": "تصدير بيانات الأداة", + "export-to-csv": "تصدير البيانات إلى CSV...", + "export-to-excel": "تصدير البيانات إلى XLS...", + "export-to-excel-xlsx": "تصدير البيانات إلى XLSX...", + "import": "استيراد أداة", + "no-data": "لا توجد بيانات لعرضها على الأداة", + "data-overflow": "الأداة تعرض {{count}} من أصل {{total}} كيان", + "alarm-data-overflow": "الأداة تعرض تنبيهات لـ {{allowedEntities}} كيانات (الحد الأقصى المسموح) من أصل {{totalEntities}} كيان", + "search": "بحث عن أداة", + "filter": "نوع فلتر الأداة", + "loading-widgets": "جاري تحميل الأدوات...", + "widget-template-error": "قالب HTML الأداة غير صالح." + }, + "widget-action": { + "header-button": "زر رأس الودجة", + "open-dashboard-state": "انتقال إلى حالة لوحة تحكم جديدة", + "update-dashboard-state": "تحديث حالة لوحة التحكم الحالية", + "open-dashboard": "انتقال إلى لوحة تحكم أخرى", + "custom": "إجراء مخصص", + "custom-pretty": "إجراء مخصص (مع قالب HTML)", + "custom-pretty-error-title": "خطأ في الحوار المخصص", + "custom-pretty-template-error": "قالب الحوار المخصص غير صالح.", + "custom-pretty-controller-error": "حدث خطأ أثناء تقييم وظيفة الحوار المخصصة.", + "mobile-action": "إجراء على الهاتف المحمول", + "target-dashboard-state": "حالة لوحة التحكم المستهدفة", + "target-dashboard-state-required": "حالة لوحة التحكم المستهدفة مطلوبة", + "set-entity-from-widget": "تعيين كيان من الودجة", + "target-dashboard": "لوحة تحكم مستهدفة", + "open-right-layout": "فتح تخطيط لوحة التحكم اليميني (عرض الجوال)", + "state-display-type": "خيار عرض حالة لوحة التحكم", + "open-normal": "عادي", + "open-in-separate-dialog": "فتح في حوار منفصل", + "open-in-popover": "فتح في منبثق", + "dialog-title": "عنوان الحوار", + "dialog-hide-dashboard-toolbar": "إخفاء شريط أدوات لوحة التحكم في الحوار", + "dialog-width": "عرض الحوار بالنسبة لعرض منطقة العرض بالنسبة المئوية", + "dialog-height": "ارتفاع الحوار بالنسبة لارتفاع منطقة العرض بالنسبة المئوية", + "dialog-size-range-error": "يجب أن تكون قيمة نسبة حجم الحوار في نطاق من 1 إلى 100.", + "popover-preferred-placement": "الترتيب المفضل للمنبثق", + "popover-placement-top": "أعلى", + "popover-placement-topLeft": "أعلى اليسار", + "popover-placement-topRight": "أعلى اليمين", + "popover-placement-right": "يمين", + "popover-placement-rightTop": "أعلى اليمين", + "popover-placement-rightBottom": "أسفل اليمين", + "popover-placement-bottom": "أسفل", + "popover-placement-bottomLeft": "أسفل اليسار", + "popover-placement-bottomRight": "أسفل اليمين", + "popover-placement-left": "يسار", + "popover-placement-leftTop": "أعلى اليسار", + "popover-placement-leftBottom": "أسفل اليسار", + "popover-hide-on-click-outside": "إخفاء المنبثق عند النقر خارجه", + "popover-hide-dashboard-toolbar": "إخفاء شريط أدوات لوحة التحكم في المنبثق", + "popover-width": "عرض المنبثق بوحدات المتصفح (على سبيل المثال 100px، 25vw)", + "popover-height": "ارتفاع المنبثق بوحدات المتصفح (على سبيل المثال 100px، 25vh)", + "popover-style": "نمط المنبثق", + "open-new-browser-tab": "فتح في علامة تبويب متصفح جديدة", + "mobile": { + "action-type": "نوع إجراء الجوال", + "action-type-required": "نوع إجراء الجوال مطلوب", + "take-picture-from-gallery": "التقاط صورة من المعرض", + "take-photo": "التقاط صورة", + "map-direction": "فتح اتجاهات الخريطة", + "map-location": "فتح موقع الخريطة", + "scan-qr-code": "مسح رمز الاستجابة السريعة (QR)", + "make-phone-call": "إجراء مكالمة هاتفية", + "get-location": "الحصول على موقع الهاتف", + "take-screenshot": "التقاط لقطة شاشة" + } + }, + "widgets-bundle": { + "current": "الحزمة الحالية", + "widgets-bundles": "حزم الودجات", + "widgets-bundle-widgets": "ودجات حزم الودجات", + "add": "إضافة حزمة الودجات", + "delete": "حذف حزمة الودجات", + "title": "العنوان", + "title-required": "العنوان مطلوب.", + "title-max-length": "يجب أن يكون العنوان أقل من 256 حرفًا", + "description": "الوصف", + "image-preview": "معاينة الصورة", + "order": "الترتيب", + "add-widgets-bundle-text": "إضافة حزمة جديدة من الودجات", + "no-widgets-bundles-text": "لم يتم العثور على حزم الودجات", + "empty": "حزمة الودجات فارغة", + "details": "التفاصيل", + "widgets-bundle-details": "تفاصيل حزمة الودجات", + "delete-widgets-bundle-title": "هل أنت متأكد أنك تريد حذف حزمة الودجات '{{widgetsBundleTitle}}'؟", + "delete-widgets-bundle-text": "كن حذرًا، بعد التأكيد ستصبح حزمة الودجات وجميع البيانات ذات الصلة غير قابلة للاسترداد.", + "delete-widgets-bundles-title": "هل أنت متأكد أنك تريد حذف { count, plural, =1 {حزمة واحدة من الودجات} other {# حزمة من الودجات} }؟", + "delete-widgets-bundles-action-title": "حذف { count, plural, =1 {حزمة واحدة من الودجات} other {# حزمة من الودجات} }", + "delete-widgets-bundles-text": "كن حذرًا، بعد التأكيد سيتم إزالة جميع حزم الودجات المحددة وستصبح جميع البيانات ذات الصلة غير قابلة للاسترداد.", + "no-widgets-bundles-matching": "لم يتم العثور على حزم الودجات المطابقة لـ '{{widgetsBundle}}'.", + "widgets-bundle-required": "حزمة الودجات مطلوبة.", + "system": "النظام", + "import": "استيراد حزمة الودجات", + "export": "تصدير حزمة الودجات", + "export-widgets-bundle-widgets-prompt": "تضمين ودجات الحزمة في البيانات المصدرة (وإلا سيتم تصدير معرفات الودجات المرجعية فقط)", + "export-failed-error": "تعذر تصدير حزمة الودجات: {{error}}", + "create-new-widgets-bundle": "إنشاء حزمة جديدة من الودجات", + "widgets-bundle-file": "ملف حزمة الودجات", + "invalid-widgets-bundle-file-error": "تعذر استيراد حزمة الودجات: هيكل بيانات حزمة الودجات غير صالح.", + "search": "بحث في حزم الودجات", + "selected-widgets-bundles": "{ count, plural, =1 {1 حزمة من الودجات} other {# حزمة من الودجات} } محددة", + "open-widgets-bundle": "فتح حزمة الودجات", + "loading-widgets-bundles": "جاري تحميل حزم الودجات..." + }, + "widget-config": { + "data": "البيانات", + "settings": "الإعدادات", + "advanced": "متقدم", + "appearance": "المظهر", + "widget-card": "بطاقة الودجة", + "mobile": "الجوال", + "title": "العنوان", + "title-tooltip": "تلميح العنوان", + "general-settings": "الإعدادات العامة", + "display-title": "عرض عنوان الودجة", + "card-title": "عنوان البطاقة", + "drop-shadow": "ظل الانسياب", + "enable-fullscreen": "تمكين الشاشة الكاملة", + "enable-data-export": "تمكين تصدير البيانات", + "data-export": "تصدير البيانات", + "background-color": "لون الخلفية", + "text-color": "لون النص", + "border-radius": "نصف قطر الحدود", + "padding": "التباعد الداخلي", + "margin": "الهامش", + "widget-style": "نمط الودجة", + "widget-css": "CSS الخاص بالودجة", + "title-style": "نمط العنوان", + "mobile-mode-settings": "إعدادات الوضع الجوال", + "order": "الترتيب", + "height": "الارتفاع", + "mobile-hide": "إخفاء الودجة في الوضع الجوال", + "desktop-hide": "إخفاء الودجة في الوضع سطح المكتب", + "units": "رمز خاص لإظهاره بجانب القيمة", + "units-by-default": "الوحدات افتراضيًا", + "decimals": "عدد الأرقام بعد الفاصلة العشرية", + "decimals-by-default": "الأرقام العشرية افتراضيًا", + "default-data-key-parameter-hint": "ينطبق هذا المعلم على جميع قيم الودجة ما لم يتم تجاوزه بواسطة تكوين مفتاح البيانات", + "units-short": "الوحدات", + "decimals-short": "الأرقام العشرية", + "decimals-suffix": "الأرقام العشرية", + "timewindow": "نافذة الوقت", + "use-dashboard-timewindow": "استخدام نافذة الوقت للوحة التحكم", + "use-widget-timewindow": "استخدام نافذة الوقت للودجة", + "display-timewindow": "عرض نافذة الوقت", + "legend": "وسيلة تعليمية", + "display-legend": "عرض الوسيلة التعليمية", + "datasources": "مصادر البيانات", + "datasource": "مصدر البيانات", + "maximum-datasources": "مسموح بأقصى { count, plural, =1 {مصدر بيانات واحد فقط.} other {# مصادر بيانات فقط مسموحة} }", + "timeseries-key-error": "يجب تحديد مفتاح بيانات سلاسل زمنية واحد على الأقل", + "datasource-type": "النوع", + "datasource-parameters": "المعلمات", + "remove-datasource": "إزالة مصدر البيانات", + "add-datasource": "إضافة مصدر البيانات", + "target-device": "الجهاز المستهدف", + "alarm-source": "مصدر التنبيه", + "actions": "الإجراءات", + "action": "الإجراء", + "add-action": "إضافة إجراء", + "search-actions": "بحث عن الإجراءات", + "no-actions-text": "لم يتم العثور على إجراءات", + "action-source": "مصدر الإجراء", + "action-source-required": "مصدر الإجراء مطلوب.", + "action-name": "الاسم", + "action-name-required": "اسم الإجراء مطلوب.", + "action-name-not-unique": "يوجد إجراء آخر بنفس الاسم بالفعل.
يجب أن يكون اسم الإجراء فريدًا ضمن نفس مصدر الإجراء.", + "action-icon": "الرمز", + "show-hide-action-using-function": "إظهار/إخفاء الإجراء باستخدام الدالة", + "action-type": "النوع", + "action-type-required": "نوع الإجراء مطلوب.", + "edit-action": "تحرير الإجراء", + "delete-action": "حذف الإجراء", + "delete-action-title": "حذف إجراء الودجة", + "delete-action-text": "هل أنت متأكد أنك تريد حذف إجراء الودجة بالاسم '{{actionName}}'؟", + "title-icon": "رمز العنوان", + "display-icon": "عرض رمز العنوان", + "card-icon": "رمز البطاقة", + "icon": "الرمز", + "icon-color": "لون الرمز", + "icon-size": "حجم الرمز", + "advanced-settings": "الإعدادات المتقدمة", + "data-settings": "إعدادات البيانات", + "limits": "الحدود", + "no-data-display-message": "رسالة بديلة \"لا توجد بيانات لعرضها\"", + "data-page-size": "أقصى عدد للكيانات لكل مصدر بيانات", + "settings-component-not-found": "لم يتم العثور على مكون نموذج الإعدادات للمحدد '{{selector}}'", + "preview": "معاينة", + "set": "تعيين", + "set-message": "رسالة تعيين", + "advanced-title-style": "نمط العنوان المتقدم", + "card-style": "نمط البطاقة", + "text": "النص", + "background": "الخلفية", + "advanced-widget-style": "نمط الودجة المتقدم", + "card-buttons": "أزرار البطاقة", + "show-card-buttons": "عرض أزرار البطاقة", + "card-border-radius": "نصف قطر حدود البطاقة", + "card-appearance": "مظهر البطاقة", + "color": "اللون", + "tooltip": "تلميح", + "units-required": "الوحدة مطلوبة." + }, + "widget-type": { + "import": "استيراد نوع الودجة", + "export": "تصدير نوع الودجة", + "export-failed-error": "تعذر تصدير الودجة: {{error}}", + "widget-file": "ملف الودجة", + "invalid-widget-file-error": "تعذر استيراد الودجة: هيكل بيانات الودجة غير صالح." + }, + "self-registration": { + "self-registration": "التسجيل الذاتي", + "self-registration-url": "رابط التسجيل الذاتي", + "captcha-version": "إصدار رمز التحقق", + "captcha-action": "اسم السجل الرمزي لـ reCAPTCHA", + "captcha-site-key": "مفتاح موقع reCAPTCHA", + "captcha-secret-key": "المفتاح السري لـ reCAPTCHA", + "notification-email": "البريد الإلكتروني للإشعار", + "notification-email-invalid": "تنسيق البريد الإلكتروني غير صالح.", + "notification-email-required": "البريد الإلكتروني للإشعار مطلوب.", + "privacy-policy-text": "نص سياسة الخصوصية", + "terms-of-use-text": "نص شروط الاستخدام", + "text-message-page": "رسالة نصية لصفحة التسجيل", + "enable-mobile-self-registration": "تمكين التسجيل الذاتي من التطبيق المحمول", + "mobile-self-registration-title": "إعدادات التسجيل الذاتي للتطبيق المحمول", + "mobile-package": "حزمة التطبيق", + "mobile-package-placeholder": "مثال: my.example.app", + "mobile-package-hint": "لنظام Android: معرف التطبيق الفريد الخاص بك. لنظام iOS: معرف حزمة المنتج.", + "mobile-package-required": "حزمة التطبيق مطلوبة", + "mobile-app-secret": "التطبيق السري", + "invalid-mobile-app-secret": "يجب أن يحتوي التطبيق السري على أحرف أبجدية وأرقام فقط ويجب أن يكون بين 16 و 2048 حرفًا.", + "copy-mobile-app-secret": "نسخ التطبيق السري", + "mobile-app-scheme": "رابط التطبيق", + "mobile-app-scheme-placeholder": "مثال: customscheme", + "mobile-app-scheme-required": "رابط التطبيق مطلوب", + "mobile-app-host": "اسم المضيف لرابط التطبيق", + "mobile-app-host-placeholder": "مثال: my.app.host", + "mobile-app-host-required": "اسم المضيف لرابط التطبيق مطلوب", + "show-privacy-policy": "عرض سياسة الخصوصية", + "show-terms-of-use": "عرض شروط الاستخدام", + "domain-settings": "إعدادات النطاق", + "general-settings": "الإعدادات العامة" + }, + "solution-template": { + "solution-template": "قالب الحل", + "solution-templates": "قوالب الحل", + "management": "إدارة قوالب الحل", + "details": "التفاصيل", + "install": "تثبيت", + "level": "المستوى", + "install-title": "تم تثبيت قالب الحل بنجاح", + "install-failed-title": "فشل تثبيت قالب الحل", + "instructions": "التعليمات", + "goto-main-dashboard": "الانتقال إلى لوحة التحكم الرئيسية", + "delete": "حذف", + "delete-solution-title": "هل أنت متأكد أنك تريد حذف الحل '{{solutionTitle}}'؟", + "delete-solution-text": "كن حذرًا، بعد التأكيد سيتم جعل الحل وجميع البيانات المتعلقة به لا يمكن استردادها.", + "installing": "جارٍ تثبيت قالب الحل..." + }, + "markdown": { + "edit": "تعديل", + "preview": "معاينة", + "copy-code": "انقر للنسخ", + "copied": "تم النسخ!" + }, + "white-labeling": { + "white-labeling": "تسمية بيضاء", + "white-labeling-general": "تسمية بيضاء عامة", + "login-white-labeling": "تسمية بيضاء لتسجيل الدخول", + "general": "عام", + "login": "تسجيل الدخول", + "preview": "معاينة", + "app-title": "عنوان التطبيق", + "favicon": "رمز الموقع", + "favicon-description": "*.ico، *.gif أو *.png صورة بحجم أقصى {{kbSize}} كيلوبايت.", + "favicon-size-error": "صورة الموقع كبيرة جدًا. أقصى حجم مسموح به لصور الموقع {{kbSize}} كيلوبايت.", + "favicon-type-error": "تنسيق ملف صورة الموقع غير صالح. يتم قبول صور ICO، GIF أو PNG فقط.", + "drop-favicon-image": "اسحب صورة رمز الموقع أو انقر لتحديد ملف للتحميل.", + "drop-favicon-image-or": "اسحب وأسقط صورة رمز الموقع أو", + "no-favicon-image": "لم يتم اختيار أي رمز", + "logo": "شعار", + "logo-description": "أي صورة بحجم أقصى {{kbSize}} كيلوبايت.", + "logo-size-error": "صورة الشعار كبيرة جدًا. الحجم الأقصى المسموح به لصورة الشعار {{kbSize}} كيلوبايت.", + "logo-type-error": "تنسيق ملف الشعار غير صالح. يتم قبول الصور فقط.", + "drop-logo-image": "اسحب صورة الشعار أو انقر لتحديد ملف للتحميل.", + "drop-logo-image-or": "اسحب وأسقط صورة الشعار أو", + "no-logo-image": "لم يتم اختيار أي شعار", + "logo-height": "ارتفاع الشعار، بكسل", + "primary-palette": "الباليت الأساسي", + "accent-palette": "الباليت المثير", + "customize-palette": "تخصيص", + "advanced-css": "CSS المتقدم", + "edit-palette": "تحرير الباليت", + "save-palette": "حفظ الباليت", + "primary-background": "خلفية أساسية", + "secondary-background": "خلفية ثانوية", + "hue1": "درجة لون 1", + "hue2": "درجة لون 2", + "hue3": "درجة لون 3", + "page-background-color": "لون خلفية الصفحة", + "dark-foreground": "أمامي داكن", + "domain-name": "اسم النطاق", + "base-url": "عنوان URL الأساسي", + "prohibit-different-url": "منع استخدام اسم مضيف من رؤوس الطلبات العميل", + "prohibit-different-url-hint": "يجب تمكين هذا الإعداد لبيئات الإنتاج. قد يتسبب في مشاكل أمان عند التعطيل", + "help-link-base-url": "عناوين مساعدة الروابط الأساسية", + "ui-help-base-url": "عنوان URL الأساسي للمساعدة في واجهة المستخدم", + "enable-help-links": "تمكين روابط المساعدة", + "error-verification-url": "لا يجب أن يحتوي اسم النطاق على الرموز '/' و ':' . مثال: thingsboard.io", + "show-platform-name-version": "عرض اسم المنصة والإصدار", + "platform-name": "اسم المنصة", + "platform-version": "إصدار المنصة", + "version-mask": "{{name}} v.{{version}}", + "position": { + "label": "موقع اسم المنصة والإصدار", + "under-logo": "تحت الشعار", + "bottom": "أسفل نموذج تسجيل الدخول" + } + }, + "widgets": { + "background": { + "background": "الخلفية", + "background-settings": "إعدادات الخلفية", + "background-type-image": "صورة", + "background-type-color": "لون", + "image-url": "عنوان URL للصورة", + "overlay": "تراكب", + "enable-overlay": "تمكين التراكب", + "blur": "ضبابية", + "preview": "معاينة" + }, + "battery-level": { + "layout": "التخطيط", + "layout-vertical-solid": "عمودي. صلب", + "layout-horizontal-solid": "أفقي. صلب", + "layout-vertical-divided": "عمودي. مقسم", + "layout-horizontal-divided": "أفقي. مقسم", + "icon": "أيقونة", + "value": "القيمة", + "auto-scale": "تحجيم تلقائي", + "battery-level-color": "لون مستوى البطارية", + "battery-shape-color": "لون شكل البطارية", + "battery-level-card-style": "نمط بطاقة مستوى البطارية", + "sections-count": "عدد الأقسام" + }, + "signal-strength": { + "value": "القيمة", + "last-update": "آخر تحديث", + "no-signal": "لا إشارة", + "layout": "التخطيط", + "layout-wifi": "واي فاي", + "layout-cellular-bar": "شريط الخلية", + "icon": "أيقونة", + "date": "التاريخ", + "active-bars-color": "لون الشرائط النشطة للإشارة", + "inactive-bars-color": "لون الشرائط غير النشطة للإشارة", + "tooltip": "تلميح", + "background-color": "لون الخلفية", + "background-blur": "ضبابية الخلفية", + "signal-strength-card-style": "نمط بطاقة قوة الإشارة" + }, + "chart": { + "common-settings": "الإعدادات الشائعة", + "enable-stacking-mode": "تمكين وضع التراص", + "selection": "تحديد نطاق الوقت", + "enable-selection-mode": "تمكين وضع التحديد", + "line-shadow-size": "حجم ظل الخط", + "display-smooth-lines": "عرض الخطوط الناعمة (المنحنية)", + "default-bar-width": "العرض الافتراضي للعمود للبيانات غير المجمعة (بالميلي ثانية)", + "bar-alignment": "محاذاة العمود", + "bar-alignment-left": "يسار", + "bar-alignment-right": "يمين", + "bar-alignment-center": "وسط", + "default-font": "الخط الافتراضي", + "default-font-size": "حجم الخط الافتراضي", + "default-font-color": "لون الخط الافتراضي", + "thresholds-line-width": "عرض الخط الافتراضي لجميع الحدود", + "tooltip-settings": "إعدادات التلميح", + "tooltip": "تلميح", + "show-tooltip": "عرض التلميح", + "hover-individual-points": "تحويل مؤشرات فردية", + "show-cumulative-values": "عرض القيم التراكمية في وضع التراص", + "hide-zero-false-values": "إخفاء القيم الصفرية / الكاذبة من التلميح", + "tooltip-value-format-function": "وظيفة تنسيق قيم التلميح", + "grid-settings": "إعدادات الشبكة", + "show-vertical-lines": "عرض الخطوط الرأسية", + "show-horizontal-lines": "عرض الخطوط الأفقية", + "grid-outline-border-width": "عرض الحدود / الشبكة (بالبكسل)", + "primary-color": "اللون الأساسي", + "background-color": "لون الخلفية", + "ticks-color": "لون العلامات", + "xaxis-settings": "إعدادات المحور الأفقي", + "axis-title": "عنوان المحور", + "xaxis-tick-labels-settings": "إعدادات تسميات العلامات على المحور الأفقي", + "show-tick-labels": "عرض تسميات العلامات", + "yaxis-settings": "إعدادات المحور الرأسي", + "min-scale-value": "القيمة الدنيا على المقياس", + "max-scale-value": "القيمة القصوى على المقياس", + "yaxis-tick-labels-settings": "إعدادات تسميات العلامات على المحور الرأسي", + "tick-step-size": "الخطوة بين العلامات", + "number-of-decimals": "عدد الأرقام العشرية للعرض", + "ticks-formatter-function": "وظيفة تنسيق العلامات", + "comparison-settings": "إعدادات المقارنة", + "enable-comparison": "تمكين المقارنة", + "time-for-comparison": "فترة المقارنة", + "time-for-comparison-previous-interval": "الفاصل الزمني السابق (الافتراضي)", + "time-for-comparison-days": "قبل يوم", + "time-for-comparison-weeks": "قبل أسبوع", + "time-for-comparison-months": "قبل شهر", + "time-for-comparison-years": "قبل سنة", + "time-for-comparison-custom-interval": "الفاصل الزمني المخصص", + "custom-interval-value": "قيمة الفاصل الزمني المخصصة (بالميلي ثانية)", + "comparison-x-axis-settings": "إعدادات محور المقارنة الأفقي", + "axis-position": "موضع المحور", + "axis-position-top": "أعلى (الافتراضي)", + "axis-position-bottom": "أسفل", + "custom-legend-settings": "إعدادات الأسطورة المخصصة", + "enable-custom-legend": "تمكين الأسطورة المخصصة (سيتيح لك هذا استخدام قيم السمات / السلاسل الزمنية في تسميات المفاتيح)", + "key-name": "اسم المفتاح", + "key-name-required": "اسم المفتاح مطلوب", + "key-type": "نوع المفتاح", + "key-type-attribute": "سمة", + "key-type-timeseries": "سلسلة زمنية", + "label-keys-list": "قائمة المفاتيح لاستخدامها في التسميات", + "no-label-keys": "لا توجد مفاتيح مكونة", + "add-label-key": "إضافة مفتاح جديد", + "line-width": "عرض الخط", + "color": "اللون", + "data-is-hidden-by-default": "البيانات مخفية افتراضيًا", + "disable-data-hiding": "تعطيل إخفاء البيانات", + "remove-from-legend": "إزالة مفتاح البيانات من الأسطورة", + "exclude-from-stacking": "استبعاد من التراص (متاح في \"التراص\" فقط)", + "line-settings": "إعدادات الخط", + "show-line": "عرض الخط", + "fill-line": "ملء الخط", + "fill-line-opacity": "شفافية الملء", + "points-settings": "إعدادات النقاط", + "show-points": "عرض النقاط", + "points-line-width": "عرض الخط للنقاط", + "points-radius": "نصف قطر النقاط", + "point-shape": "شكل النقطة", + "point-shape-circle": "دائرة", + "point-shape-cross": "صليب", + "point-shape-diamond": "ماسة", + "point-shape-square": "مربع", + "point-shape-triangle": "مثلث", + "point-shape-custom": "وظيفة مخصصة", + "point-shape-draw-function": "وظيفة رسم شكل النقطة", + "show-separate-axis": "عرض محور منفصل", + "axis-position-left": "يسار", + "axis-position-right": "يمين", + "thresholds": "الحدود", + "no-thresholds": "لا توجد حدود مكونة", + "add-threshold": "إضافة حد", + "show-values-for-comparison": "عرض القيم التاريخية للمقارنة", + "comparison-values-label": "تسمية القيم التاريخية", + "comparison-line-color": "لون الخط للمقارنة", + "threshold-settings": "إعدادات الحدود", + "use-as-threshold": "استخدام قيمة المفتاح كحد", + "threshold-line-width": "عرض الخط للحد", + "threshold-color": "لون الحد", + "common-pie-settings": "إعدادات البيتزا الشائعة", + "radius": "النصف", + "inner-radius": "النصف الداخلي", + "tilt": "ميل", + "common-pie-settings-range-error": "يجب أن تكون القيمة في النطاق من 0 إلى 1", + "stroke-settings": "إعدادات السكتة الدماغية", + "width-pixels": "العرض (بالبكسل)", + "show-labels": "عرض التسميات", + "animation-settings": "إعدادات الرسوم المتحركة", + "animated-pie": "تمكين رسوم متحركة للبيتزا (تجريبي)", + "border-settings": "إعدادات الحدود", + "border-width": "عرض الحدود", + "border-color": "لون الحدود", + "legend-settings": "إعدادات الأسطورة", + "display-legend": "عرض الأسطورة", + "labels-font-color": "لون خط التسميات", + "series": "سلاسل", + "add-series": "إضافة سلسلة", + "series-settings": "إعدادات السلاسل", + "remove-series": "إزالة السلسلة", + "no-series": "لا توجد سلاسل مكونة", + "no-series-error": "يجب تحديد سلسلة واحدة على الأقل", + "chart-appearance": "مظهر الرسم البياني", + "vertical-grid-lines": "خطوط الشبكة العمودية", + "horizontal-grid-lines": "خطوط الشبكة الأفقية", + "chart-background": "خلفية الرسم البياني", + "grid-lines-color": "لون خطوط الشبكة", + "border": "حدود", + "axis": "محور", + "vertical-axis": "المحور العمودي", + "ticks": "العلامات", + "horizontal-axis": "المحور الأفقي" + }, + "color": { + "color-settings": "إعدادات الألوان", + "color-type-constant": "ثابت", + "color-type-range": "نطاق", + "color-type-function": "وظيفة", + "color": "اللون", + "value-range": "نطاق القيمة", + "from": "من", + "to": "إلى", + "color-function": "وظيفة اللون", + "copy-color-settings-from": "نسخ إعدادات الألوان من" + }, + "dashboard-state": { + "dashboard-state-settings": "إعدادات حالة لوحة المعلومات", + "dashboard-state": "معرف حالة لوحة المعلومات", + "autofill-state-layout": "ملء تخطيط الحالة تلقائيًا بالارتفاع الافتراضي", + "default-margin": "هامش الودجت الافتراضي", + "default-background-color": "لون الخلفية الافتراضي", + "sync-parent-state-params": "مزامنة معلمات الحالة مع لوحة المعلومات الأم" + }, + "date-range-navigator": { + "date-range-picker-settings": "إعدادات مُحدّد نطاق التاريخ", + "hide-date-range-picker": "إخفاء مُحدّد نطاق التاريخ", + "picker-one-panel": "مُحدّد نطاق التاريخ بلوحة واحدة", + "picker-auto-confirm": "تأكيد تلقائي لمُحدّد نطاق التاريخ", + "picker-show-template": "عرض القالب لمُحدّد نطاق التاريخ", + "first-day-of-week": "أول يوم في الأسبوع", + "interval-settings": "إعدادات الفترة", + "hide-interval": "إخفاء الفترة", + "initial-interval": "الفترة الابتدائية", + "interval-hour": "ساعة", + "interval-day": "يوم", + "interval-week": "أسبوع", + "interval-two-weeks": "أسبوعين", + "interval-month": "شهر", + "interval-three-months": "3 أشهر", + "interval-six-months": "6 أشهر", + "step-settings": "إعدادات الخطوة", + "hide-step-size": "إخفاء حجم الخطوة", + "initial-step-size": "حجم الخطوة الابتدائي", + "hide-labels": "إخفاء التسميات", + "use-session-storage": "استخدام تخزين الجلسة", + "localizationMap": { + "Sun": "الأحد", + "Mon": "الإثنين", + "Tue": "الثلاثاء", + "Wed": "الأربعاء", + "Thu": "الخميس", + "Fri": "الجمعة", + "Sat": "السبت", + "Jan": "يناير", + "Feb": "فبراير", + "Mar": "مارس", + "Apr": "أبريل", + "May": "مايو", + "Jun": "يونيو", + "Jul": "يوليو", + "Aug": "أغسطس", + "Sep": "سبتمبر", + "Oct": "أكتوبر", + "Nov": "نوفمبر", + "Dec": "ديسمبر", + "January": "يناير", + "February": "فبراير", + "March": "مارس", + "April": "أبريل", + "June": "يونيو", + "July": "يوليو", + "August": "أغسطس", + "September": "سبتمبر", + "October": "أكتوبر", + "November": "نوفمبر", + "December": "ديسمبر", + "Custom Date Range": "نطاق تاريخ مخصص", + "Date Range Template": "قالب نطاق التاريخ", + "Today": "اليوم", + "Yesterday": "أمس", + "This Week": "هذا الأسبوع", + "Last Week": "الأسبوع الماضي", + "This Month": "هذا الشهر", + "Last Month": "الشهر الماضي", + "Year": "سنة", + "This Year": "هذه السنة", + "Last Year": "السنة الماضية", + "Date picker": "مُحدّد التاريخ", + "Hour": "ساعة", + "Day": "يوم", + "Week": "أسبوع", + "2 weeks": "أسبوعين", + "Month": "شهر", + "3 months": "3 أشهر", + "6 months": "6 أشهر", + "Custom interval": "فاصل زمني مخصص", + "Interval": "الفاصل الزمني", + "Step size": "حجم الخطوة", + "Ok": "حسناً" + } + }, + "doughnut": { + "total": "الإجمالي", + "layout": "تخطيط", + "layout-default": "افتراضي", + "layout-with-total": "مع الإجمالي", + "auto-scale": "تحجيم تلقائي", + "clockwise-layout": "تخطيط باتجاه العقارب", + "sort-series": "فرز السلاسل حسب التسمية", + "central-total-value": "قيمة الإجمالي المركزية", + "legend-position-top": "أعلى", + "legend-position-bottom": "أسفل", + "legend-position-left": "يسار", + "legend-position-right": "يمين", + "legend-label": "التسمية", + "legend-value": "القيمة", + "tooltip": "تلميح", + "tooltip-value": "القيمة", + "tooltip-value-type-absolute": "مطلق", + "tooltip-value-type-percentage": "نسبة مئوية", + "tooltip-background-color": "لون الخلفية", + "tooltip-background-blur": "تمويه الخلفية", + "doughnut-card-style": "نمط بطاقة الدونات" + }, + "entities-hierarchy": { + "hierarchy-data-settings": "إعدادات بيانات التسلسل الهرمي", + "relations-query-function": "وظيفة استعلام العلاقات بين العقد", + "has-children-function": "وظيفة التحقق من وجود الأطفال في العقد", + "node-state-settings": "إعدادات حالة العقد", + "node-opened-function": "وظيفة فتح العقد الافتراضية", + "node-disabled-function": "وظيفة تعطيل العقد", + "display-settings": "إعدادات العرض", + "node-icon-function": "وظيفة أيقونة العقد", + "node-text-function": "وظيفة نص العقد", + "sort-settings": "إعدادات الفرز", + "nodes-sort-function": "وظيفة فرز العقد" + }, + "edge": { + "display-default-title": "عرض العنوان الافتراضي" + }, + "gateway": { + "general-settings": "الإعدادات العامة", + "widget-title": "عنوان الودجة", + "default-archive-file-name": "اسم ملف الأرشيف الافتراضي", + "device-type-for-new-gateway": "نوع الجهاز للبوابة الجديدة", + "messages-settings": "إعدادات الرسائل", + "save-config-success-message": "رسالة نصية حول حفظ إعدادات بوابة بنجاح", + "device-name-exists-message": "رسالة نصية عندما يكون الجهاز بالاسم المدخل موجودًا بالفعل", + "gateway-title": "نموذج البوابة", + "read-only": "للقراءة فقط", + "events-title": "عنوان نموذج أحداث البوابة", + "events-filter": "مرشحات الأحداث", + "event-key-contains": "مفتاح الحدث يحتوي على...", + "show-connector": "إظهار للموصل", + "connector-state-param-key": "مفتاح المعلمة الحالية للموصل", + "status": "الحالة", + "message": "الرسالة", + "created-time": "وقت الإنشاء" + }, + "gauge": { + "default-color": "اللون الافتراضي", + "radial-gauge-settings": "إعدادات العداد الدائري", + "ticks-settings": "إعدادات العلامات", + "min-value": "القيمة الدنيا", + "max-value": "القيمة القصوى", + "min-value-short": "الدنيا", + "max-value-short": "القصوى", + "start-ticks-angle": "زاوية بداية العلامات", + "ticks-angle": "زاوية العلامات", + "major-ticks": "العلامات الرئيسية", + "major-ticks-count": "عدد العلامات الرئيسية", + "major-ticks-color": "لون العلامات الرئيسية", + "minor-ticks": "العلامات الثانوية", + "minor-ticks-count": "عدد العلامات الثانوية", + "minor-ticks-color": "لون العلامات الثانوية", + "tick-numbers-font": "خط أرقام العلامات", + "unit-title-settings": "إعدادات عنوان الوحدة", + "show-unit-title": "عرض عنوان الوحدة", + "unit-title": "عنوان الوحدة", + "title-font": "خط العنوان", + "units-settings": "إعدادات الوحدات", + "units-font": "خط الوحدات", + "value-box-settings": "إعدادات مربع القيمة", + "show-value-box": "عرض مربع القيمة", + "value-box": "مربع القيمة", + "value-int": "عدد الأرقام للجزء الصحيح من القيمة", + "value-text": "نص القيمة", + "value-text-shadow": "ظل نص القيمة", + "value-font": "خط نص القيمة", + "rect-stroke-color-start": "لون الإطار المستطيل - بداية التدرج", + "rect-stroke-color-end": "لون الإطار المستطيل - نهاية التدرج", + "background-color": "لون الخلفية", + "shadow-color": "لون الظل", + "value-box-rect-stroke-color": "لون إطار مربع القيمة", + "value-box-rect-stroke-color-end": "لون إطار مربع القيمة - نهاية التدرج", + "value-box-background-color": "لون خلفية مربع القيمة", + "value-box-shadow-color": "لون ظل مربع القيمة", + "plate-settings": "إعدادات اللوحة", + "show-plate-border": "حدود اللوحة", + "plate-color": "لون اللوحة", + "needle-settings": "إعدادات الإبرة", + "needle-circle-size": "حجم دائرة الإبرة", + "needle-color": "لون الإبرة", + "needle-color-start": "لون الإبرة - بداية التدرج", + "needle-color-end": "لون الإبرة - نهاية التدرج", + "needle-color-shadow-up": "لون ظل النصف العلوي من الإبرة", + "needle-color-shadow-down": "لون الظل المسقط", + "highlights-settings": "إعدادات التمييز", + "highlights-width": "عرض التمييز", + "highlights": "التمييزات", + "highlight-from": "من", + "highlight-to": "إلى", + "highlight-color": "اللون", + "no-highlights": "لا يوجد تمييزات مُعدة", + "add-highlight": "إضافة تمييز", + "animation-settings": "إعدادات الحركة", + "enable-animation": "تمكين الحركة", + "animation-duration-rule": "مدة الحركة والقاعدة", + "animation-duration": "مدة الحركة", + "animation-rule": "قاعدة الحركة", + "animation-linear": "خطي", + "animation-quad": "رباعي", + "animation-quint": "خماسي", + "animation-cycle": "دورة", + "animation-bounce": "ارتداد", + "animation-elastic": "مرن", + "animation-dequad": "عكس رباعي", + "animation-dequint": "عكس خماسي", + "animation-decycle": "عكس دورة", + "animation-debounce": "عكس ارتداد", + "animation-delastic": "عكس مرن", + "linear-gauge-settings": "إعدادات العداد الخطي", + "bar-stroke": "ضربة الشريط", + "bar-stroke-width": "عرض ضربة الشريط", + "bar-stroke-color": "لون ضربة الشريط", + "bar-background-color": "لون خلفية الشريط - بداية التدرج", + "bar-background-color-end": "لون خلفية الشريط - نهاية التدرج", + "progress-bar-color": "لون شريط التقدم", + "progress-bar": "شريط التقدم", + "progress-bar-color-start": "لون شريط التقدم - بداية التدرج", + "progress-bar-color-end": "لون شريط التقدم - نهاية التدرج", + "major-ticks-names": "أسماء العلامات الرئيسية", + "show-stroke-ticks": "عرض ضربات العلامات", + "major-ticks-font": "خط العلامات الرئيسية", + "border-color": "لون الحدود", + "border-width": "عرض الحدود", + "needle-circle": "دائرة الإبرة", + "needle-circle-color": "لون دائرة الإبرة", + "animation-target": "هدف الحركة", + "animation-target-needle": "الإبرة", + "animation-target-plate": "اللوحة", + "common-settings": "إعدادات العداد الشائعة", + "gauge-type": "نوع العداد", + "gauge-type-arc": "قوس", + "gauge-type-donut": "دونات", + "gauge-type-horizontal-bar": "شريط أفقي", + "gauge-type-vertical-bar": "شريط عمودي", + "donut-start-angle": "الزاوية التي تبدأ منها", + "bar-settings": "إعدادات شريط العداد", + "relative-bar-width": "عرض الشريط النسبي", + "neon-glow-brightness": "سطوع تأثير الوهج النيوني، (0-100)، 0 - تعطيل التأثير", + "stripes-thickness": "سمك الخطوط، 0 - بدون خطوط", + "rounded-line-cap": "عرض نهايات الخطوط المستديرة", + "bar-color-settings": "إعدادات لون الشريط", + "use-precise-level-color-values": "استخدام مستويات الألوان الدقيقة", + "bar-colors": "ألوان الشريط، من الأدنى إلى الأعلى", + "color": "اللون", + "no-bar-colors": "لم يتم تكوين ألوان للشريط", + "add-bar-color": "إضافة لون للشريط", + "from": "من", + "to": "إلى", + "fixed-level-colors": "ألوان الشريط باستخدام قيم الحدود", + "gauge-title-settings": "إعدادات عنوان العداد", + "show-gauge-title": "عرض عنوان العداد", + "gauge-title": "عنوان العداد", + "gauge-title-font": "خط عنوان العداد", + "unit-title-and-timestamp-settings": "إعدادات عنوان الوحدة والطابع الزمني", + "show-timestamp": "عرض طابع الوقت", + "timestamp-format": "تنسيق الطابع الزمني", + "label-font": "خط التسمية أسفل القيمة", + "value-settings": "إعدادات القيمة", + "show-value": "عرض نص القيمة", + "min-max-settings": "إعدادات تسميات الحد الأدنى/الأقصى", + "show-min-max": "عرض القيم الدنيا والقصوى", + "min-max-font": "خط تسميات الحد الأدنى والأقصى", + "show-ticks": "عرض العلامات", + "tick-width": "عرض العلامة", + "tick-color": "لون العلامة", + "tick-values": "قيم العلامات", + "no-tick-values": "لا توجد قيم للعلامات مُكونة", + "add-tick-value": "إضافة قيمة علامة", + "gauge-appearance": "مظهر العداد", + "units-title": "عنوان الوحدات", + "value": "القيمة", + "ticks": "العلامات", + "arrow-and-scale-color": "لون السهم والمقياس الافتراضي", + "scale-settings": "إعدادات المقياس", + "scale": "المقياس", + "scale-color": "ألوان المقياس", + "compass-appearance": "مظهر البوصلة", + "labels": "التسميات", + "label-style": "أسلوب التسمية" + }, + "gpio": { + "pin": "الدبوس", + "label": "التسمية", + "row": "الصف", + "column": "العمود", + "color": "اللون", + "panel-settings": "إعدادات اللوحة", + "background-color": "لون الخلفية", + "gpio-switches": "مفاتيح GPIO", + "no-gpio-switches": "لم يتم تكوين مفاتيح GPIO", + "add-gpio-switch": "إضافة مفتاح GPIO", + "gpio-status-request": "طلب حالة GPIO", + "method-name": "اسم الطريقة", + "method-body": "جسم الطريقة", + "gpio-status-change-request": "طلب تغيير حالة GPIO", + "parse-gpio-status-function": "تحليل وظيفة حالة GPIO", + "gpio-leds": "مصابيح GPIO", + "no-gpio-leds": "لم يتم تكوين مصابيح GPIO", + "add-gpio-led": "إضافة مصباح LED لـ GPIO" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "لا يمكن استخدام معلمة السمة في هذه الأداة", + "blocked-location": "تم حظر الموقع الجغرافي في متصفحك", + "claim-device": "ادعاء الجهاز", + "claim-failed": "فشل في ادعاء الجهاز!", + "claim-not-found": "لم يتم العثور على الجهاز!", + "claim-successful": "تم ادعاء الجهاز بنجاح!", + "date": "التاريخ", + "device-name": "اسم الجهاز", + "device-name-required": "اسم الجهاز مطلوب", + "discard-changes": "تجاهل التغييرات", + "entity-attribute-required": "سمة الكيان مطلوبة", + "entity-coordinate-required": "كلا الحقلين، خط العرض وخط الطول، مطلوبان", + "entity-timeseries-required": "سلسلة زمنية للكيان مطلوبة", + "get-location": "الحصول على الموقع الحالي", + "invalid-date": "تاريخ غير صالح", + "latitude": "خط العرض", + "longitude": "خط الطول", + "min-value-error": "القيمة الدنيا هي {{value}}", + "max-value-error": "القيمة القصوى هي {{value}}", + "not-allowed-entity": "الكيان المختار لا يمكن أن يكون له سمات مشتركة", + "no-attribute-selected": "لم يتم اختيار سمة", + "no-datakey-selected": "لم يتم اختيار مفتاح بيانات", + "no-coordinate-specified": "لم يتم تحديد مفتاح بيانات لخط العرض/الطول", + "no-entity-selected": "لم يتم اختيار كيان", + "no-image": "لا توجد صورة", + "no-support-geolocation": "متصفحك لا يدعم الموقع الجغرافي", + "no-support-web-camera": "متصفحك لا يدعم الكاميرات", + "enable-https-use-widget": "الرجاء تفعيل HTTPS لاستخدام هذه الأداة", + "no-found-your-camera": "لا يمكن العثور على كاميرتك", + "no-permission-camera": "تم رفض الإذن من قبل المستخدم / هذا الموقع لا يملك إذن استخدام الكاميرا", + "no-timeseries-selected": "لم يتم اختيار سلسلة زمنية", + "secret-key": "المفتاح السري", + "secret-key-required": "المفتاح السري مطلوب", + "switch-attribute-value": "تبديل قيمة سمة الكيان", + "switch-camera": "تبديل الكاميرا", + "switch-timeseries-value": "تبديل قيمة سلسلة زمنية للكيان", + "take-photo": "التقاط صورة", + "time": "الوقت", + "timeseries-not-allowed": "لا يمكن استخدام معلمة سلسلة زمنية في هذه الأداة", + "update-failed": "فشل التحديث", + "update-successful": "تم التحديث بنجاح", + "update-attribute": "تحديث السمة", + "update-timeseries": "تحديث السلسلة الزمنية", + "value": "القيمة", + "general-settings": "الإعدادات العامة", + "widget-title": "عنوان الأداة", + "claim-button-label": "تسمية زر الادعاء", + "show-secret-key-field": "عرض حقل إدخال 'المفتاح السري'", + "labels-settings": "إعدادات التسميات", + "show-labels": "عرض التسميات", + "device-name-label": "تسمية حقل إدخال اسم الجهاز", + "secret-key-label": "تسمية حقل إدخال المفتاح السري", + "messages-settings": "إعدادات الرسائل", + "claim-device-success-message": "رسالة نصية لادعاء الجهاز بنجاح", + "claim-device-not-found-message": "رسالة نصية عند عدم العثور على الجهاز", + "claim-device-failed-message": "رسالة نصية لفشل ادعاء الجهاز", + "claim-device-name-required-message": "رسالة خطأ 'اسم الجهاز مطلوب'", + "claim-device-secret-key-required-message": "رسالة خطأ 'المفتاح السري مطلوب'", + "relations-settings": "إعدادات العلاقات", + "relate-device-to-state-entity": "ربط الجهاز بكيان الحالة الحالي", + "relate-direction": "اتجاه الربط", + "relate-direction-from": "من", + "relate-direction-to": "إلى", + "relation-type": "نوع العلاقة", + "show-label": "عرض التسمية", + "label": "التسمية", + "required": "مطلوب", + "required-error-message": "رسالة خطأ 'مطلوب'", + "show-result-message": "عرض رسالة النتيجة", + "integer-field-settings": "إعدادات حقل العدد الصحيح", + "min-value": "القيمة الدنيا", + "max-value": "القيمة القصوى", + "double-field-settings": "إعدادات حقل العدد النقطي", + "text-field-settings": "إعدادات حقل النص", + "min-length": "الطول الأدنى", + "max-length": "الطول الأقصى", + "checkbox-settings": "إعدادات مربع الاختيار", + "true-label": "تسمية المحدد", + "false-label": "تسمية غير المحدد", + "image-input-settings": "إعدادات إدخال الصورة", + "display-preview": "عرض المعاينة", + "display-clear-button": "عرض زر الإزالة", + "display-apply-button": "عرض زر التطبيق", + "display-discard-button": "عرض زر التجاهل", + "datetime-field-settings": "إعدادات حقل التاريخ/الوقت", + "display-time-input": "عرض إدخال الوقت", + "latitude-key-name": "اسم مفتاح خط العرض", + "longitude-key-name": "اسم مفتاح خط الطول", + "show-get-location-button": "عرض زر 'الحصول على الموقع الحالي'", + "use-high-accuracy": "استخدام دقة عالية", + "location-fields-settings": "إعدادات حقول الموقع", + "latitude-label": "تسمية خط العرض", + "longitude-label": "تسمية خط الطول", + "input-fields-alignment": "محاذاة حقول الإدخال", + "input-fields-alignment-column": "عمود (افتراضي)", + "input-fields-alignment-row": "صف", + "layout": "التخطيط", + "row-gap": "الفجوة بين الصفوف بالبكسل", + "column-gap": "الفجوة بين الأعمدة بالبكسل", + "latitude-field-required": "حقل خط العرض مطلوب", + "longitude-field-required": "حقل خط الطول مطلوب", + "attribute-settings": "إعدادات السمة", + "widget-mode": "وضع الأداة", + "widget-mode-update-attribute": "تحديث السمة", + "widget-mode-update-timeseries": "تحديث السلسلة الزمنية", + "attribute-scope": "نطاق السمة", + "attribute-scope-server": "سمة الخادم", + "attribute-scope-shared": "سمة مشتركة", + "value-required": "القيمة مطلوبة", + "image-settings": "إعدادات الصورة", + "image-format": "تنسيق الصورة", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "جودة الصورة التي تستخدم الضغط الخاسر مثل jpeg و webp", + "max-image-width": "العرض الأقصى للصورة", + "max-image-height": "الارتفاع الأقصى للصورة", + "action-buttons": "أزرار العمل", + "show-action-buttons": "عرض أزرار العمل", + "update-all-values": "تحديث جميع القيم، ليس فقط المعدلة", + "save-button-label": "تسمية زر 'حفظ'", + "reset-button-label": "تسمية زر 'تراجع'", + "group-settings": "إعدادات المجموعة", + "show-group-title": "عرض عنوان لمجموعة من الحقول، المتعلقة بكيانات مختلفة", + "group-title": "عنوان المجموعة", + "fields-alignment": "محاذاة الحقول", + "fields-alignment-row": "صف (افتراضي)", + "fields-alignment-column": "عمود", + "fields-in-row": "عدد الحقول في الصف", + "option-value": "القيمة (اكتب 'null' لإنشاء خيار فارغ)", + "option-label": "التسمية", + "hide-input-field": "إخفاء حقل الإدخال", + "datakey-type": "نوع مفتاح البيانات", + "datakey-type-server": "سمة الخادم (افتراضي)", + "datakey-type-shared": "سمة مشتركة", + "datakey-type-timeseries": "سلسلة زمنية", + "datakey-value-type": "نوع قيمة مفتاح البيانات", + "datakey-value-type-string": "نص", + "datakey-value-type-double": "عدد نقطي", + "datakey-value-type-integer": "عدد صحيح", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "منطقي (مربع اختيار)", + "datakey-value-type-boolean-switch": "منطقي (مفتاح)", + "datakey-value-type-date-time": "تاريخ ووقت", + "datakey-value-type-date": "تاريخ", + "datakey-value-type-time": "وقت", + "datakey-value-type-select": "اختيار", + "datakey-value-type-color": "لون", + "value-is-required": "القيمة مطلوبة", + "ability-to-edit-attribute": "القدرة على تعديل السمة", + "ability-to-edit-attribute-editable": "قابل للتعديل (افتراضي)", + "ability-to-edit-attribute-disabled": "معطل", + "ability-to-edit-attribute-readonly": "للقراءة فقط", + "disable-on-datakey-name": "تعطيل عند قيمة كاذبة لمفتاح بيانات آخر (حدد اسم مفتاح البيانات)", + "field-appearance": "مظهر الحقل", + "appearance-fill": "ملء", + "appearance-outline": "مخطط", + "subscript-sizing": "تحجيم النص الفرعي", + "subscript-sizing-fixed": "ثابت", + "subscript-sizing-dynamic": "ديناميكي", + "slide-toggle-settings": "إعدادات التبديل السلس", + "slide-toggle-label-position": "موضع تسمية التبديل السلس", + "slide-toggle-label-position-after": "بعد", + "slide-toggle-label-position-before": "قبل", + "select-options": "خيارات الاختيار", + "no-select-options": "لا توجد خيارات اختيار مُكونة", + "add-select-option": "إضافة خيار اختيار", + "numeric-field-settings": "إعدادات الحقل الرقمي", + "step-interval": "الفاصل الزمني بين القيم", + "error-messages": "رسائل الخطأ", + "min-value-error-message": "رسالة خطأ 'القيمة الدنيا'", + "max-value-error-message": "رسالة خطأ 'القيمة القصوى'", + "invalid-date-error-message": "رسالة خطأ 'تاريخ غير صالح'", + "invalid-JSON-error-message": "رسالة خطأ 'JSON غير صالح'", + "icon-settings": "إعدادات الأيقونة", + "dialog-editor-settings": "إعدادات محرر الحوار", + "use-custom-icon": "استخدام أيقونة مخصصة", + "input-cell-icon": "أيقونة لعرضها قبل خلية الإدخال", + "value-conversion-settings": "إعدادات تحويل القيمة", + "get-value-settings": "إعدادات الحصول على القيمة", + "use-get-value-function": "استخدام وظيفة getValue", + "get-value-function": "وظيفة getValue", + "set-value-settings": "إعدادات تعيين القيمة", + "use-set-value-function": "استخدام وظيفة setValue", + "set-value-function": "وظيفة setValue", + "json-invalid": "القيمة JSON لها تنسيق غير صالح", + "title": "العنوان", + "cancel-button-label": "تسمية زر 'إلغاء'" + }, + "invalid-qr-code-text": "نص غير صالح لرمز الاستجابة السريعة. يجب أن يكون الإدخال من نوع سلسلة", + "qr-code": { + "use-qr-code-text-function": "استخدام وظيفة نص رمز الاستجابة السريعة", + "qr-code-text-pattern": "نمط نص رمز الاستجابة السريعة (مثل '${entityName} | ${keyName} - بعض النص.')", + "qr-code-text-pattern-hint": "يستخدم نمط نص رمز الاستجابة السريعة قيمة المفتاح الأول الموجود في الكيانات في الاسم المستعار للكيان.", + "qr-code-text-pattern-required": "نمط نص رمز الاستجابة السريعة مطلوب.", + "qr-code-text-function": "وظيفة نص رمز الاستجابة السريعة" + }, + "label-widget": { + "label-pattern": "نمط", + "label-pattern-hint": "تلميح: على سبيل المثال، 'نص ${keyName} وحدات.' أو ${#<key index>} وحدات'", + "label-pattern-required": "النمط مطلوب", + "label-position": "الموضع (نسبي إلى الخلفية بالنسبة المئوية)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "لون الخلفية", + "font-settings": "إعدادات الخط", + "background-image": "صورة الخلفية", + "labels": "التسميات", + "no-labels": "لم يتم تكوين تسميات", + "add-label": "إضافة تسمية" + }, + "navigation": { + "title": "العنوان", + "navigation-path": "مسار التنقل", + "filter-type": "نوع التصفية", + "filter-type-all": "جميع العناصر", + "filter-type-include": "تضمين العناصر", + "filter-type-exclude": "استبعاد العناصر", + "items": "العناصر", + "enter-urls-to-filter": "أدخل عناوين URL للتصفية..." + }, + "persistent-table": { + "rpc-id": "معرف RPC", + "message-type": "نوع الرسالة", + "method": "الطريقة", + "params": "المعلمات", + "created-time": "وقت الإنشاء", + "expiration-time": "وقت الانتهاء", + "retries": "محاولات متكررة", + "status": "الحالة", + "filter": "تصفية", + "refresh": "تحديث", + "add": "إضافة طلب RPC", + "details": "تفاصيل", + "delete": "حذف", + "delete-request-title": "حذف طلب RPC دائم", + "delete-request-text": "هل أنت متأكد من رغبتك في حذف الطلب؟", + "details-title": "تفاصيل معرف RPC: ", + "additional-info": "معلومات إضافية", + "response": "الاستجابة", + "any-status": "أي حالة", + "rpc-status-list": "قائمة حالة RPC", + "no-request-prompt": "لا يوجد طلب لعرضه", + "send-request": "إرسال الطلب", + "add-title": "إنشاء طلب RPC دائم", + "method-error": "الطريقة مطلوبة.", + "timeout-error": "الحد الأدنى لقيمة الوقت المنقضي هو 5000 (5 ثوان).", + "white-space-error": "غير مسموح بالمسافات البيضاء.", + "rpc-status": { + "QUEUED": "في الانتظار", + "SENT": "تم الإرسال", + "DELIVERED": "تم التوصيل", + "SUCCESSFUL": "ناجح", + "TIMEOUT": "انتهى الوقت", + "EXPIRED": "منتهي الصلاحية", + "FAILED": "فشل" + }, + "rpc-search-status-all": "الكل", + "message-types": { + "false": "اثنين الطريقة", + "true": "طريقة واحدة" + }, + "general-settings": "إعدادات عامة", + "enable-filter": "تمكين التصفية", + "enable-sticky-header": "عرض العنوان أثناء التمرير", + "enable-sticky-action": "عرض عمود الإجراءات أثناء التمرير", + "display-request-details": "عرض تفاصيل الطلب", + "allow-send-request": "السماح بإرسال طلب RPC", + "allow-delete-request": "السماح بحذف الطلب", + "columns-settings": "إعدادات الأعمدة", + "display-columns": "الأعمدة لعرضها", + "column": "العمود", + "no-columns-found": "لم يتم العثور على أعمدة", + "no-columns-matching": "'{{column}}' غير موجود." + }, + "range-chart": { + "chart": "رسم بياني", + "data-zoom": "تكبير البيانات", + "range-colors": "ألوان النطاق", + "out-of-range-color": "لون خارج النطاق", + "fill-area": "ملء المنطقة", + "legend-label": "تسمية العنوان", + "tooltip": "تلميح", + "tooltip-value": "القيمة", + "tooltip-date": "التاريخ", + "tooltip-background-color": "لون الخلفية", + "tooltip-background-blur": "ضبابية الخلفية", + "range-chart-card-style": "نمط بطاقة رسم النطاق" + }, + "rpc": { + "value-settings": "إعدادات القيمة", + "initial-value": "القيمة الأولية", + "retrieve-value-settings": "استرجاع إعدادات قيمة التشغيل/الإيقاف", + "retrieve-value-method": "استرجاع القيمة باستخدام طريقة", + "retrieve-value-method-none": "لا تسترجع", + "retrieve-value-method-rpc": "استدعاء طريقة RPC لاسترجاع القيمة", + "retrieve-value-method-attribute": "الاشتراك للحصول على سمة", + "retrieve-value-method-timeseries": "الاشتراك للحصول على سلسلة زمنية", + "attribute-value-key": "مفتاح السمة", + "timeseries-value-key": "مفتاح السلسلة الزمنية", + "get-value-method": "طريقة RPC للحصول على القيمة", + "parse-value-function": "وظيفة تحليل القيمة", + "update-value-settings": "تحديث إعدادات القيمة", + "set-value-method": "طريقة RPC لتعيين القيمة", + "convert-value-function": "وظيفة تحويل القيمة", + "rpc-settings": "إعدادات RPC", + "request-timeout": "مهلة طلب RPC (مللي ثانية)", + "persistent-rpc-settings": "إعدادات RPC الدائمة", + "request-persistent": "طلب RPC دائم", + "persistent-polling-interval": "الفاصل الزمني للاستطلاع (مللي ثانية) للحصول على استجابة أمر RPC الدائم", + "common-settings": "إعدادات مشتركة", + "switch-title": "عنوان المفتاح", + "show-on-off-labels": "إظهار تسميات التشغيل/الإيقاف", + "slide-toggle-label": "تسمية التبديل الانزلاقي", + "label-position": "موقع التسمية", + "label-position-before": "قبل", + "label-position-after": "بعد", + "slider-color": "لون المنزلق", + "slider-color-primary": "أساسي", + "slider-color-accent": "لهجة", + "slider-color-warn": "تحذير", + "button-style": "أسلوب الزر", + "button-raised": "زر بارز", + "button-primary": "لون أساسي", + "button-background-color": "لون خلفية الزر", + "button-text-color": "لون نص الزر", + "widget-title": "عنوان الأداة", + "button-label": "تسمية الزر", + "device-attribute-scope": "نطاق سمة الجهاز", + "server-attribute": "سمة الخادم", + "shared-attribute": "سمة مشتركة", + "device-attribute-parameters": "معاملات سمة الجهاز", + "is-one-way-command": "هو أمر اتجاه واحد", + "rpc-method": "طريقة RPC", + "rpc-method-params": "معاملات طريقة RPC", + "show-rpc-error": "إظهار خطأ تنفيذ أمر RPC", + "led-title": "عنوان LED", + "led-color": "لون LED", + "check-status-settings": "إعدادات التحقق من الحالة", + "perform-rpc-status-check": "تنفيذ التحقق من حالة جهاز RPC", + "retrieve-led-status-value-method": "استرجاع قيمة حالة LED باستخدام طريقة", + "led-status-value-attribute": "سمة الجهاز التي تحتوي على قيمة حالة LED", + "led-status-value-timeseries": "سلسلة زمنية للجهاز التي تحتوي على قيمة حالة LED", + "check-status-method": "طريقة التحقق من حالة الجهاز RPC", + "parse-led-status-value-function": "وظيفة تحليل قيمة حالة LED", + "knob-title": "عنوان المقبض", + "min-value": "القيمة الدنيا", + "max-value": "القيمة القصوى" + }, + "maps": { + "select-entity": "اختر كيان", + "select-entity-hint": "تلميح: بعد الاختيار، انقر على الخريطة لتعيين الموقع", + "tooltips": { + "placeMarker": "انقر لوضع '{{entityName}}' كيان", + "firstVertex": "مضلع لـ '{{entityName}}': انقر لوضع النقطة الأولى", + "firstVertex-cut": "انقر لوضع النقطة الأولى", + "continueLine": "مضلع لـ '{{entityName}}': انقر للاستمرار في الرسم", + "continueLine-cut": "انقر للاستمرار في الرسم", + "finishLine": "انقر على أي علامة موجودة للإنهاء", + "finishPoly": "مضلع لـ '{{entityName}}': انقر على العلامة الأولى للإنهاء والحفظ", + "finishPoly-cut": "انقر على العلامة الأولى للإنهاء والحفظ", + "finishRect": "مضلع لـ '{{entityName}}': انقر للإنهاء والحفظ", + "startCircle": "دائرة لـ '{{entityName}}': انقر لوضع مركز الدائرة", + "finishCircle": "دائرة لـ '{{entityName}}': انقر لإنهاء الدائرة", + "placeCircleMarker": "انقر لوضع علامة دائرية" + }, + "actions": { + "finish": "إنهاء", + "cancel": "إلغاء", + "removeLastVertex": "إزالة آخر نقطة" + }, + "buttonTitles": { + "drawMarkerButton": "وضع كيان", + "drawPolyButton": "إنشاء مضلع", + "drawLineButton": "إنشاء خط متعدد", + "drawCircleButton": "إنشاء دائرة", + "drawRectButton": "إنشاء مستطيل", + "editButton": "وضع التحرير", + "dragButton": "وضع السحب والإفلات", + "cutButton": "قطع منطقة المضلع", + "deleteButton": "إزالة", + "drawCircleMarkerButton": "إنشاء علامة دائرية", + "rotateButton": "تدوير المضلع" + }, + "map-provider-settings": "إعدادات مزود الخريطة", + "map-provider": "مزود الخريطة", + "map-provider-google": "خرائط Google", + "map-provider-openstreet": "خرائط OpenStreet", + "map-provider-here": "خرائط HERE", + "map-provider-image": "خريطة الصورة", + "map-provider-tencent": "خرائط Tencent", + "openstreet-provider": "مزود خريطة OpenStreet", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (الافتراضي)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "استخدام مزود مخصص", + "custom-provider-tile-url": "URL بلاط المزود المخصص", + "google-maps-api-key": "مفتاح API لخرائط Google", + "default-map-type": "نوع الخريطة الافتراضي", + "google-map-type-roadmap": "خريطة الطرق", + "google-map-type-satelite": "القمر الصناعي", + "google-map-type-hybrid": "هجين", + "google-map-type-terrain": "التضاريس", + "map-layer": "طبقة الخريطة", + "here-map-normal-day": "HERE.normalDay (الافتراضي)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "الاعتمادات", + "here-app-id": "معرف تطبيق HERE", + "here-app-code": "كود تطبيق HERE", + "here-api-key": "مفتاح API لـ HERE", + "here-use-new-version-api-3": "استخدم الإصدار 3 من API", + "tencent-maps-api-key": "مفتاح API لخرائط Tencent", + "tencent-map-type-roadmap": "خريطة الطرق", + "tencent-map-type-satelite": "القمر الصناعي", + "tencent-map-type-hybrid": "هجين", + "image-map-background": "خلفية خريطة الصورة", + "image-map-background-from-entity-attribute": "أخذ خلفية خريطة الصورة من سمة الكيان", + "image-url-source-entity-alias": "اسم مستعار لكيان مصدر URL الصورة", + "image-url-source-entity-attribute": "سمة كيان مصدر URL الصورة", + "common-map-settings": "إعدادات الخريطة المشتركة", + "x-pos-key-name": "اسم مفتاح الموقع X", + "y-pos-key-name": "اسم مفتاح الموقع Y", + "latitude-key-name": "اسم مفتاح خط العرض", + "longitude-key-name": "اسم مفتاح خط الطول", + "default-map-zoom-level": "مستوى تكبير/تصغير الخريطة الافتراضي (0 - 20)", + "default-map-center-position": "موضع مركز الخريطة الافتراضي (0,0)", + "disable-scroll-zooming": "تعطيل التكبير/التصغير بالتمرير", + "disable-double-click-zooming": "تعطيل التكبير بالنقر المزدوج", + "disable-zoom-control-buttons": "تعطيل أزرار التحكم في التكبير/التصغير", + "fit-map-bounds": "تعديل حدود الخريطة لتغطية جميع العلامات", + "use-default-map-center-position": "استخدام موضع مركز الخريطة الافتراضي", + "entities-limit": "حد الكيانات للتحميل", + "markers-settings": "إعدادات العلامات", + "marker-offset-x": "إزاحة العلامة X نسبيًا إلى الموقع مضروبًا في عرض العلامة", + "marker-offset-y": "إزاحة العلامة Y نسبيًا إلى الموقع مضروبًا في ارتفاع العلامة", + "position-function": "وظيفة تحويل الموقع، يجب أن تعيد الإحداثيات x,y كعدد مزدوج من 0 إلى 1 لكل منهما", + "draggable-marker": "علامة قابلة للسحب", + "label": "التسمية", + "show-label": "إظهار التسمية", + "use-label-function": "استخدام وظيفة التسمية", + "label-pattern": "التسمية (أمثلة النمط: '${entityName}', '${entityName}: (نص ${keyName} وحدات.)')", + "label-function": "وظيفة التسمية", + "tooltip": "التلميح", + "show-tooltip": "إظهار التلميح", + "show-tooltip-action": "الإجراء لعرض التلميح", + "show-tooltip-action-click": "إظهار التلميح عند النقر (الافتراضي)", + "show-tooltip-action-hover": "إظهار التلميح عند التمرير", + "auto-close-tooltips": "إغلاق التلميحات تلقائيًا", + "use-tooltip-function": "استخدام وظيفة التلميح", + "tooltip-pattern": "التلميح (على سبيل المثال 'نص ${keyName} وحدات.' أو نص الرابط')", + "tooltip-function": "وظيفة التلميح", + "tooltip-offset-x": "إزاحة التلميح X نسبيًا إلى مرساة العلامة مضروبًا في عرض العلامة", + "tooltip-offset-y": "إزاحة التلميح Y نسبيًا إلى مرساة العلامة مضروبًا في ارتفاع العلامة", + "color": "اللون", + "use-color-function": "استخدام وظيفة اللون", + "color-function": "وظيفة اللون", + "marker-image": "صورة العلامة", + "use-marker-image-function": "استخدام وظيفة صورة العلامة", + "custom-marker-image": "صورة علامة مخصصة", + "custom-marker-image-size": "حجم صورة العلامة المخصصة (بكسل)", + "marker-image-function": "وظيفة صورة العلامة", + "marker-images": "صور العلامات", + "polygon-settings": "إعدادات المضلع", + "show-polygon": "عرض المضلع", + "polygon-key-name": "اسم مفتاح المضلع", + "enable-polygon-edit": "تمكين تحرير المضلع", + "polygon-label": "تسمية المضلع", + "show-polygon-label": "عرض تسمية المضلع", + "use-polygon-label-function": "استخدام وظيفة تسمية المضلع", + "polygon-label-pattern": "تسمية المضلع (أمثلة النمط: '${entityName}', '${entityName}: (نص ${keyName} وحدات.)')", + "polygon-label-function": "وظيفة تسمية المضلع", + "polygon-tooltip": "تلميح المضلع", + "show-polygon-tooltip": "عرض تلميح المضلع", + "auto-close-polygon-tooltips": "إغلاق تلميحات المضلع تلقائيًا", + "use-polygon-tooltip-function": "استخدام وظيفة تلميح المضلع", + "polygon-tooltip-pattern": "التلميح (على سبيل المثال 'نص ${keyName} وحدات.' أو نص الرابط')", + "polygon-tooltip-function": "وظيفة تلميح المضلع", + "polygon-color": "لون المضلع", + "polygon-opacity": "شفافية المضلع", + "use-polygon-color-function": "استخدام وظيفة لون المضلع", + "polygon-color-function": "وظيفة لون المضلع", + "polygon-stroke": "ضربة المضلع", + "stroke-color": "لون الضربة", + "stroke-opacity": "شفافية الضربة", + "stroke-weight": "وزن الضربة", + "use-polygon-stroke-color-function": "استخدام وظيفة لون ضربة المضلع", + "polygon-stroke-color-function": "وظيفة لون ضربة المضلع", + "circle-settings": "إعدادات الدائرة", + "show-circle": "عرض الدائرة", + "circle-key-name": "اسم مفتاح الدائرة", + "enable-circle-edit": "تمكين تحرير الدائرة", + "circle-label": "تسمية الدائرة", + "show-circle-label": "عرض تسمية الدائرة", + "use-circle-label-function": "استخدام وظيفة تسمية الدائرة", + "circle-label-pattern": "تسمية الدائرة (أمثلة النمط: '${entityName}', '${entityName}: (نص ${keyName} وحدات.)')", + "circle-label-function": "وظيفة تسمية الدائرة", + "circle-tooltip": "تلميح الدائرة", + "show-circle-tooltip": "عرض تلميح الدائرة", + "auto-close-circle-tooltips": "إغلاق تلميحات الدائرة تلقائيًا", + "use-circle-tooltip-function": "استخدام وظيفة تلميح الدائرة", + "circle-tooltip-pattern": "التلميح (على سبيل المثال 'نص ${keyName} وحدات.' أو نص الرابط')", + "circle-tooltip-function": "وظيفة تلميح الدائرة", + "circle-fill-color": "لون ملء الدائرة", + "circle-fill-color-opacity": "شفافية لون ملء الدائرة", + "use-circle-fill-color-function": "استخدام وظيفة لون ملء الدائرة", + "circle-fill-color-function": "وظيفة لون ملء الدائرة", + "circle-stroke": "ضربة الدائرة", + "use-circle-stroke-color-function": "استخدام وظيفة لون ضربة الدائرة", + "circle-stroke-color-function": "وظيفة لون ضربة الدائرة", + "markers-clustering-settings": "إعدادات تجميع العلامات", + "use-map-markers-clustering": "استخدام تجميع العلامات على الخريطة", + "zoom-on-cluster-click": "التكبير عند النقر على تجمع", + "max-cluster-zoom": "أقصى مستوى تكبير عندما تكون العلامة جزءًا من تجمع (0 - 18)", + "max-cluster-radius-pixels": "القطر الأقصى الذي سيغطيه تجمع في البكسل", + "cluster-zoom-animation": "إظهار تحريك العلامات عند التكبير", + "show-markers-bounds-on-cluster-mouse-over": "عرض حدود العلامات عند تحويل الماوس فوق تجمع", + "spiderfy-max-zoom-level": "تشبك عند أقصى مستوى تكبير (لرؤية جميع علامات التجمع)", + "load-optimization": "تحسين التحميل", + "cluster-chunked-loading": "استخدام قطع لإضافة العلامات بحيث لا يتجمد الصفحة", + "cluster-markers-lazy-load": "استخدام التحميل الكسلي لإضافة العلامات", + "editor-settings": "إعدادات المحرر", + "enable-snapping": "تمكين الالتقاط للرؤوس الأخرى لرسم دقيق", + "init-draggable-mode": "تهيئة الخريطة في وضع السحب", + "hide-all-edit-buttons": "إخفاء جميع أزرار التحكم في التحرير", + "hide-draw-buttons": "إخفاء أزرار الرسم", + "hide-edit-buttons": "إخفاء أزرار التحرير", + "hide-remove-button": "إخفاء زر الإزالة", + "route-map-settings": "إعدادات خريطة المسار", + "trip-animation-settings": "إعدادات تحريك الرحلة", + "normalization-step": "خطوة التوحيد للبيانات (مللي ثانية)", + "tooltip-background-color": "لون خلفية التلميح", + "tooltip-font-color": "لون خط التلميح", + "tooltip-opacity": "شفافية التلميح (0-1)", + "auto-close-tooltip": "إغلاق التلميح تلقائيًا", + "rotation-angle": "تعيين زاوية دوران إضافية للعلامة (درجة)", + "path-settings": "إعدادات المسار", + "path-color": "لون المسار", + "use-path-color-function": "استخدام وظيفة لون المسار", + "path-color-function": "وظيفة لون المسار", + "path-decorator": "زخرفة المسار", + "use-path-decorator": "استخدام زخرفة المسار", + "decorator-symbol": "رمز الزخرفة", + "decorator-symbol-arrow-head": "سهم", + "decorator-symbol-dash": "شرطة", + "decorator-symbol-size": "حجم رمز الزخرفة (بكسل)", + "use-path-decorator-custom-color": "استخدام لون مخصص لزخرفة المسار", + "decorator-custom-color": "لون مخصص للزخرفة", + "decorator-offset": "إزاحة الزخرفة", + "end-decorator-offset": "إزاحة نهاية الزخرفة", + "decorator-repeat": "تكرار الزخرفة", + "points-settings": "إعدادات النقاط", + "show-points": "إظهار النقاط", + "point-color": "لون النقطة", + "point-size": "حجم النقطة (بكسل)", + "use-point-color-function": "استخدام وظيفة لون النقطة", + "point-color-function": "وظيفة لون النقطة", + "use-point-as-anchor": "استخدام النقطة كمرجع", + "point-as-anchor-function": "وظيفة النقطة كمرجع", + "independent-point-tooltip": "تلميح النقطة المستقل", + "clustering-markers": "تجميع العلامات", + "use-icon-create-function": "استخدام وظيفة ألوان العلامات", + "marker-color-function": "وظيفة لون العلامة" + }, + "markdown": { + "use-markdown-text-function": "استخدام وظيفة قيم التنسيق البسيط/HTML", + "markdown-text-function": "وظيفة قيم التنسيق البسيط/HTML", + "markdown-text-pattern": "نمط التنسيق البسيط/HTML (تنسيق بسيط أو HTML مع متغيرات، مثلا '${entityName} أو ${keyName} - بعض النص.')", + "apply-default-markdown-style": "تطبيق نمط التنسيق البسيط الافتراضي", + "markdown-css": "CSS التنسيق البسيط/HTML" + }, + "simple-card": { + "label": "التسمية", + "label-position": "موضع التسمية", + "label-position-left": "يسار", + "label-position-top": "أعلى" + }, + "value-card": { + "layout": "التخطيط", + "layout-square": "مربع", + "layout-vertical": "عمودي", + "layout-centered": "مركز", + "layout-simplified": "مبسط", + "layout-horizontal": "أفقي", + "layout-horizontal-reversed": "أفقي معكوس", + "label": "التسمية", + "icon": "الأيقونة", + "value": "القيمة", + "date": "التاريخ", + "value-card-style": "نمط بطاقة القيمة", + "auto-scale": "تحجيم تلقائي" + }, + "liquid-level-card": { + "layout-simple": "بسيط", + "layout-percentage": "نسبة مئوية", + "layout-absolute": "مطلق", + "layout": "تخطيط", + "background-overlay": "تراكب خلفية القيمة", + "total-volume": "الحجم الإجمالي", + "tank": "خزان", + "shape": "الشكل", + "datasource-units": "وحدات المصدر", + "widget-units": "وحدات الودجة", + "decimals": "الأعشار", + "liquid": "سائل", + "liquid-color": "لون السائل", + "value": "القيمة", + "value-font": "خط القيمة", + "level": "المستوى", + "last-update": "آخر تحديث", + "shape-by-attribute": "تعيين شكل الخزان بواسطة اسم الخاصية", + "tooltip-background": "لون الخلفية", + "background-blur": "ضبابية الخلفية", + "tank-color": "لون الخزان", + "static": "ثابت", + "see-examples": "رؤية الأمثلة", + "attribute": "الخاصية", + "shape-type": "النوع", + "v-oval": "بيضاوي رأسي", + "v-cylinder": "أسطواني رأسي", + "v-capsule": "كبسولة رأسية", + "rectangle": "مستطيل", + "h-oval": "بيضاوي أفقي", + "h-ellipse": "قطع ناقص أفقي", + "h-dish-ends": "أطراف طبقية أفقية", + "h-cylinder": "أسطواني أفقي", + "h-capsule": "كبسولة أفقية", + "h-elliptical_2_1": "ناقصة 2:1 أفقية", + "icon": "أيقونة البطاقة", + "title": "عنوان البطاقة", + "units": "الوحدات", + "color-and-font": "اللون والخط", + "shape-attribute-name": "اسم الخاصية", + "total-volume-required": "الحجم الإجمالي مطلوب.", + "attribute-name-required": "اسم الخاصية مطلوب.", + "attribute-key-not-set": "مفتاح الخاصية '{{attributeName}}' غير مُعيَّن", + "attribute-key-invalid": "مفتاح الخاصية '{{attributeName}}' غير صالح" + }, + "aggregated-value-card": { + "subtitle": "العنوان الفرعي", + "chart": "رسم بياني", + "values": "القيم", + "value-appearance": "مظهر القيمة", + "position": "الموضع", + "position-center": "الوسط", + "position-right-top": "أعلى اليمين", + "position-right-bottom": "أسفل اليمين", + "position-left-top": "أعلى اليسار", + "position-left-bottom": "أسفل اليسار", + "font": "الخط", + "color": "اللون", + "arrow": "السهم", + "display-up-down-arrow": "عرض السهم الصعودي/الهابط", + "add-value": "إضافة قيمة", + "remove-value": "إزالة القيمة", + "no-values": "لا توجد قيم مُكونة", + "aggregation": "التجميع", + "aggregated-value-card-style": "نمط بطاقة القيمة المجمعة", + "auto-scale": "تحجيم تلقائي" + }, + "value-chart-card": { + "layout": "التخطيط", + "layout-left": "اليسار", + "layout-right": "اليمين", + "auto-scale": "تحجيم تلقائي", + "icon": "الأيقونة", + "value": "القيمة", + "chart": "الرسم البياني", + "value-chart-card-style": "نمط بطاقة القيمة والرسم البياني" + }, + "progress-bar": { + "layout": "التخطيط", + "layout-default": "الافتراضي", + "layout-simplified": "المبسط", + "auto-scale": "تحجيم تلقائي", + "icon": "الأيقونة", + "value": "القيمة", + "range": "النطاق", + "min": "الحد الأدنى", + "max": "الحد الأقصى", + "range-ticks": "علامات النطاق", + "bar": "الشريط", + "bar-color": "لون الشريط", + "bar-background": "خلفية الشريط", + "progress-bar-card-style": "نمط بطاقة شريط التقدم" + }, + "alarm-count": { + "alarm-count-card-style": "نمط بطاقة عدد التنبيهات" + }, + "entity-count": { + "entity-count-card-style": "نمط بطاقة عدد الكيانات" + }, + "count": { + "layout": "التخطيط", + "layout-column": "عمود", + "layout-row": "صف", + "label": "التسمية", + "icon": "الأيقونة", + "icon-background": "خلفية الأيقونة", + "value": "القيمة", + "chevron": "السهم", + "auto-scale": "تحجيم تلقائي" + }, + "table": { + "common-table-settings": "إعدادات الجدول الشائعة", + "enable-search": "تمكين البحث", + "enable-sticky-header": "عرض العنوان دائمًا", + "enable-sticky-action": "عرض عمود الإجراءات دائمًا", + "hidden-cell-button-display-mode": "وضع عرض أزرار الخلية المخفية", + "show-empty-space-hidden-action": "عرض المساحة الفارغة بدلاً من إجراء الزر الخفي", + "dont-reserve-space-hidden-action": "عدم احتياط مساحة لأزرار الإجراءات المخفية", + "display-timestamp": "الطابع الزمني", + "display-pagination": "عرض الترقيم", + "default-page-size": "حجم الصفحة الافتراضي", + "use-entity-label-tab-name": "استخدام تسمية الكيان في اسم التبويب", + "hide-empty-lines": "إخفاء الأسطر الفارغة", + "row-style": "نمط الصف", + "use-row-style-function": "استخدام وظيفة نمط الصف", + "row-style-function": "وظيفة نمط الصف", + "cell-style": "نمط الخلية", + "use-cell-style-function": "استخدام وظيفة نمط الخلية", + "cell-style-function": "وظيفة نمط الخلية", + "cell-content": "محتوى الخلية", + "use-cell-content-function": "استخدام وظيفة محتوى الخلية", + "cell-content-function": "وظيفة محتوى الخلية", + "show-latest-data-column": "عرض أحدث عمود بيانات", + "latest-data-column-order": "ترتيب أحدث عمود بيانات", + "entities-table-title": "عنوان جدول الكيانات", + "enable-select-column-display": "تمكين اختيار الأعمدة للعرض", + "display-entity-name": "عرض عمود اسم الكيان", + "entity-name-column-title": "عنوان عمود اسم الكيان", + "display-entity-label": "عرض عمود تسمية الكيان", + "entity-label-column-title": "عنوان عمود تسمية الكيان", + "display-entity-type": "عرض عمود نوع الكيان", + "default-sort-order": "الترتيب الافتراضي", + "custom-title": "عنوان رأس مخصص", + "column-width": "عرض العمود (بكسل أو ٪)", + "default-column-visibility": "رؤية العمود الافتراضية", + "column-visibility-visible": "مرئي", + "column-visibility-hidden": "مخفي", + "column-visibility-hidden-mobile": "مخفي في وضع الجوال", + "column-selection-to-display": "اختيار العمود في 'الأعمدة المعروضة'", + "column-selection-to-display-enabled": "ممكن", + "column-selection-to-display-disabled": "معطل", + "column-export-option": "تضمين العمود في التصدير", + "column-export-option-always": "دائمًا", + "column-export-option-only-visible": "فقط إذا كان العمود مرئيًا", + "column-export-option-never": "أبدًا", + "alarms-table-title": "عنوان جدول التنبيهات", + "enable-alarms-selection": "تمكين تحديد التنبيهات", + "enable-alarms-search": "تمكين البحث في التنبيهات", + "enable-alarm-filter": "تمكين مرشح التنبيه", + "display-alarm-details": "عرض تفاصيل التنبيه", + "allow-alarms-ack": "السماح بالاعتراف بالتنبيهات", + "allow-alarms-clear": "السماح بمسح التنبيهات", + "display-alarm-activity": "عرض نشاط التنبيه", + "allow-alarms-assign": "السماح بتعيين التنبيهات", + "blob-entities-table-title": "عنوان جدول كائنات التدفق", + "display-created-time": "عرض عمود الوقت الذي تم إنشاؤه", + "display-type": "عرض عمود نوع", + "display-customer": "عرض عمود العميل", + "no-data-display-message": "رسالة بديلة \"لا توجد بيانات لعرضها\"", + "force-default-blob-entity-type": "فرض نوع كائن التدفق الافتراضي", + "scheduler-events-table-title": "عنوان جدول أحداث المجدول", + "force-default-event-type": "فرض نوع الحدث الافتراضي", + "columns": "الأعمدة", + "column-settings": "إعدادات العمود", + "remove-column": "إزالة العمود", + "add-column": "إضافة عمود", + "no-columns": "لا توجد أعمدة مكونة", + "columns-to-display": "الأعمدة المعروضة", + "table-header": "رأس الجدول", + "header-buttons": "أزرار الرأس", + "table-buttons": "أزرار الجدول", + "pagination": "ترقيم الصفحات", + "rows": "الصفوف", + "timeseries-column-error": "يجب تحديد عمود واحد على الأقل للسلسلة الزمنية", + "alarm-column-error": "يجب تحديد عمود واحد على الأقل للتنبيه", + "table-tabs": "ألسنة الجدول", + "show-cell-actions-menu-mobile": "عرض قائمة الإجراءات في خلية الجدول في وضع الجوال" + }, + "wind-speed-direction": { + "layout": "التخطيط", + "layout-default": "الافتراضي", + "layout-advanced": "المتقدم", + "layout-simplified": "المبسط", + "values": "القيم", + "wind-direction": "اتجاه الرياح", + "center-value": "القيمة المركزية", + "icon": "الأيقونة", + "arrow": "السهم", + "ticks": "العلامات", + "labels-type": "نوع العلامات", + "directional-names": "أسماء الاتجاهات", + "degrees": "الدرجات", + "major-ticks": "علامات رئيسية", + "minor-ticks": "علامات فرعية", + "wind-speed-direction-card-style": "نمط بطاقة سرعة الرياح واتجاهها", + "ticks-color": "لون العلامات", + "ticks-labels-type": "نوع علامات العلامات", + "arrow-color": "لون السهم" + }, + "scheduler": { + "enabled-scheduler-event-views": "عروض أحداث المجدول الممكنة", + "enabled-scheduler-event-views-both": "الكل", + "enabled-scheduler-event-views-list": "عرض القائمة فقط", + "enabled-scheduler-event-views-calendar": "عرض التقويم فقط", + "display-name": "عرض الاسم", + "type-name": "اسم النوع", + "display-originator-select": "عرض اختيار الكيان المنشئ", + "display-message-type-select": "عرض اختيار نوع الرسالة", + "display-metadata-table": "عرض جدول بيانات الرسالة", + "configuration-html-template": "قالب HTML التكوين", + "custom-event-types": "أنواع الأحداث المخصصة", + "no-custom-event-types": "لم يتم تكوين أنواع أحداث مخصصة", + "add-custom-event-type": "إضافة نوع حدث مخصص" + }, + "value-source": { + "value-source": "مصدر القيمة", + "predefined-value": "القيمة المحددة مسبقًا", + "entity-attribute": "سمة الكيان", + "value": "القيمة", + "source-entity-alias": "اسم الكيان المصدر", + "source-entity-attribute": "سمة الكيان المصدرة" + }, + "widget-font": { + "font-settings": "إعدادات الخط", + "font-family": "عائلة الخط", + "size": "الحجم", + "relative-font-size": "الحجم النسبي للخط (بالنسبة المئوية)", + "font-style": "النمط", + "font-style-normal": "عادي", + "font-style-italic": "مائل", + "font-style-oblique": "مائل", + "font-weight": "الوزن", + "font-weight-normal": "عادي", + "font-weight-bold": "عريض", + "font-weight-bolder": "أكثر سمكا", + "font-weight-lighter": "أخف", + "color": "اللون", + "shadow-color": "لون الظل", + "preview": "معاينة", + "line-height": "ارتفاع السطر", + "auto": "تلقائي" + }, + "home": { + "no-data-available": "لا تتوفر بيانات" + }, + "system-info": { + "cpu": "وحدة المعالجة المركزية", + "ram": "الذاكرة العشوائية", + "disk": "القرص", + "cpu-warning-text": "ارتفاع استخدام وحدة المعالجة المركزية. لتجنب فشل النظام، قم بتحسين أداء النظام.", + "cpu-critical-text": "استخدام وحدة المعالجة المركزية عالٍ للغاية. لتجنب فشل النظام، قم بتحسين أداء النظام.", + "ram-warning-text": "انخفاض الذاكرة العشوائية المتاحة. لتجنب فشل النظام، قم بتحسين أداء النظام أو زيادة حجم الذاكرة العشوائية.", + "ram-critical-text": "انخفاض حرج في الذاكرة العشوائية المتاحة. لتجنب فشل النظام، قم بتحسين أداء النظام أو زيادة حجم الذاكرة العشوائية.", + "disk-warning-text": "انخفاض مساحة القرص المتاحة. لتجنب فقدان البيانات، قم بتحرير المساحة أو زيادة مساحة القرص.", + "disk-critical-text": "انخفاض حرج في مساحة القرص المتاحة. لتجنب فقدان البيانات، قم بتحرير المساحة أو زيادة مساحة القرص." + }, + "cluster-info": { + "service-id": "معرف الخدمة", + "service-type": "نوع الخدمة", + "no-data": "لا تتوفر بيانات" + }, + "transport-messages": { + "title": "رسائل النقل", + "info": "كل الرسائل التي تأتي من الأجهزة" + }, + "activity": { + "title": "النشاط" + }, + "documentation": { + "title": "التوثيق", + "add-link": "إضافة رابط", + "add-link-title": "إضافة رابط التوثيق", + "name": "الاسم", + "name-required": "الاسم مطلوب.", + "link": "الرابط", + "link-required": "الرابط مطلوب.", + "columns": "الأعمدة" + }, + "quick-links": { + "title": "روابط سريعة", + "add-link": "إضافة رابط", + "add-link-title": "إضافة رابط سريع", + "quick-link": "رابط سريع", + "quick-link-required": "الرابط السريع مطلوب.", + "no-links-matching": "لا توجد روابط تطابق '{{name}}'.", + "columns": "الأعمدة" + }, + "recent-dashboards": { + "title": "لوحات التحكم", + "last": "آخر مشاهدة", + "starred": "المفضلة", + "name": "الاسم", + "last-viewed": "آخر مشاهدة", + "no-last-viewed-dashboards": "لا توجد لوحات تحكم مشاهدة في الآونة الأخيرة بعد" + }, + "configured-features": { + "title": "الميزات المُكوَّنة", + "info": "حالة الميزات التي تتطلب تكوينًا", + "white-labeling-feature": "العلامة البيضاء", + "email-feature": "البريد الإلكتروني", + "sms-feature": "رسالة نصية قصيرة (SMS)", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "التحقق الثنائي العامل (2FA)", + "feature-configured": "الميزة مكونة.\nانقر لإعدادها", + "feature-not-configured": "الميزة غير مكونة.\nانقر لإعدادها" + }, + "version-info": { + "title": "الإصدار", + "contact-us": "اتصل بنا", + "current-version": "الإصدار الحالي", + "current": "الحالي", + "available-version": "الإصدار المتاح", + "available": "متاح", + "upgrade": "الترقية", + "version-is-up-to-date": "الإصدار محدَّث" + }, + "license-info": { + "title": "معلومات الترخيص", + "view-all": "عرض الكل", + "license-portal": "بوابة الترخيص", + "devices-info": "{count} / { max, plural, =0 {∞} other {#} } أجهزة", + "assets-info": "{count} / { max, plural, =0 {∞} other {#} } أصول", + "dashboards-info": "{count} / { max, plural, =0 {∞} other {#} } لوحات التحكم", + "integrations-info": "{count} / { max, plural, =0 {∞} other {#} } التكاملات", + "community-support": "دعم المجتمع", + "white-labeling": "العلامة البيضاء", + "unlimited-datapoints-and-messages": "نقاط البيانات والرسائل غير محدودة", + "unlimited-api-calls": "استدعاءات API غير محدودة" + }, + "usage-info": { + "title": "استخدام", + "entities": "الكيانات", + "api-calls": "استدعاءات API" + }, + "functions": { + "title": "الوظائف", + "pe-feature-tooltip": "فقط على ThingsBoard\nالنسخة المهنية", + "switch-to-pe": "التبديل إلى النسخة المهنية", + "alarms": "التنبيهات", + "dashboards": "لوحات التحكم", + "entities-and-relations": "الكيانات والعلاقات", + "profiles": "الملفات الشخصية", + "advanced-features": "الميزات المتقدمة", + "notification-center": "مركز الإشعارات", + "api-usage": "استخدام واجهة البرمجة", + "customers": "العملاء", + "customers-hierarchy": "تسلسل العملاء", + "roles-and-permissions": "الأدوار والأذونات", + "groups": "المجموعات", + "integrations": "التكاملات", + "solution-templates": "قوالب الحلول", + "scheduler": "المجدول", + "white-labeling": "العلامة البيضاء" + }, + "devices": { + "view-docs": "عرض الوثائق", + "inactive": "غير نشط", + "active": "نشط", + "total": "الإجمالي" + }, + "alarms": { + "critical": "حرجة", + "assigned-to-me": "معيَّنة لي", + "total": "الإجمالي" + }, + "solution-templates": { + "title": "قوالب الحلول", + "prototype-plan": "خطة النموذج الأولي", + "startup-plan": "خطة البداية" + }, + "getting-started": { + "get-started": "البدء", + "finish": "انتهاء", + "done-welcome-title": "مرحبًا بكم على متن السفينة", + "done-welcome-text": "لقد قمت بعمل رائع!", + "sys-admin": { + "step1": { + "title": "إنشاء مستأجر ومسؤول المستأجر", + "content": "

المستأجر هو فرد أو منظمة يمتلك أو ينتج الأجهزة والأصول. يمكن أن يكون للمستأجر مستخدمون مسؤولون متعددون للمستأجر وعملاء وأجهزة وأصول متعددة.

يمكن لمسؤول المستأجر إنشاء وإدارة الأجهزة والأصول والعملاء ولوحات التحكم داخل حساب المستأجر.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-create-tenant": "كيفية إنشاء مستأجر ومسؤول المستأجر" + }, + "step2": { + "title": "تكوين الميزة: خادم البريد", + "content": "

تكوين خادم البريد أمر أساسي لتنشيط المستخدمين، واسترداد كلمات المرور، وتوصيل إشعارات التنبيه.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-mail-server": "كيفية تكوين خادم البريد" + }, + "step3": { + "title": "تكوين الميزة: مزود خدمة SMS", + "content": "

قم بتكوين مزودي الخدمة SMS لإخطار العملاء بالتنبيهات عبر الرسائل القصيرة (SMS).

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-sms-provider": "كيفية تكوين مزود خدمة SMS" + }, + "step4": { + "title": "تكوين الميزة: العلامة البيضاء", + "content": "

قم بتخصيص شعار شركتك أو منتجك ومخطط الألوان بسهولة دون برمجة ودون إعادة تشغيل الخدمة.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-white-labeling": "كيفية تكوين العلامة البيضاء" + }, + "step5": { + "title": "تكوين الميزة: 2FA", + "content": "

تحسين أمان حسابات المنصة مع المصادقة ثنائية العامل.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-2fa": "كيفية تكوين 2FA" + }, + "step6": { + "title": "تكوين الميزة: OAuth 2", + "content": "

بسهولة تبسيط تسجيل الدخول لمستخدمي المستأجر والعملاء باستخدام وظيفة الاستمارة الواحدة عبر OAuth 2.0.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-oauth2": "كيفية تكوين OAuth 2" + }, + "step7": { + "title": "تكوين الميزة: Slack", + "content": "

توزيع الإشعارات إلى مستخدمي المستأجر والعملاء عبر Slack وفقًا لقواعد الإشعار التي تعيِّنها.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-notifications": "كيفية تكوين Slack" + } + }, + "tenant-admin": { + "step1": { + "title": "إنشاء جهاز", + "content": "

دعونا نقوم بتوفير أول جهاز لك على المنصة عبر واجهة المستخدم. اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-create-device": "كيفية إنشاء جهاز" + }, + "step2": { + "title": "الاتصال بالجهاز", + "content-before": "

للاتصال بالجهاز، تحتاج إلى الحصول على بيانات اعتماد الجهاز. نوصي باستخدام بيانات الاعتماد الافتراضية المولدة تلقائيًا وهي رمز الوصول لهذا الدليل.

  • انتقل إلى جدول الجهاز
  • انقر على صف الجهاز لفتح تفاصيل الجهاز
  • اضغط على الزر \"نسخ رمز الوصول\"

استخدم الأوامر البسيطة لنشر البيانات عبر HTTP. لا تنسى استبدال $ACCESS_TOKEN برمز الوصول لجهازك:

", + "ubuntu": { + "install-curl": "تثبيت cURL لنظام Ubuntu:" + }, + "macos": { + "install-curl": "تثبيت cURL لنظام MacOS:" + }, + "windows": { + "install-curl": "بدءًا من Windows 10 b17063، يتوفر cURL افتراضيًا." + }, + "replace-access-token": "استبدل $ACCESS_TOKEN برمز الوصول لجهازك:", + "content-after": "

يمكنك أيضًا استخدام بروتوكولات أخرى مثل MQTT، CoAP، إلخ.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-connect-device": "كيفية الاتصال بالجهاز" + }, + "step3": { + "title": "إنشاء لوحة تحكم", + "content": "

إنشاء لوحة تحكم لتصور البيانات من الكيانات مثل الأصول والأجهزة، إلخ.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-create-dashboard": "كيفية إنشاء لوحة تحكم" + }, + "step4": { + "title": "تكوين قواعد التنبيه", + "alarm-rules": "قواعد التنبيه", + "content": "

لنقم برفع تنبيه عندما تصل درجة الحرارة إلى 25 درجة مئوية. اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-configure-alarm-rules": "كيفية تكوين قواعد التنبيه" + }, + "step5": { + "title": "إنشاء تنبيه", + "content-before": "

لتشغيل التنبيه، قم بإرسال قيمة تلميم جديدة تبلغ 26 درجة مئوية أو أعلى.

", + "replace-access-token": "استبدل $ACCESS_TOKEN برمز الوصول لجهازك:", + "content-after": "

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-create-alarm": "كيفية إنشاء تنبيه" + }, + "step6": { + "title": "إنشاء عميل ومشاركة لوحة التحكم", + "content": "

من خلال إنشاء لوحات تحكم للمستخدمين النهائيين، يمكن للمستخدمين العملاء رؤية أجهزتهم فقط، وسيتم إخفاء البيانات من عميل آخر.

اتبع الوثائق حول كيفية القيام بذلك:

", + "how-to-create-customer-and-share-dashboard": "كيفية إنشاء عميل ومشاركة لوحة التحكم" + } + } + } + }, + "icon": { + "icon": "أيقونة", + "icons": "الأيقونات", + "select-icon": "اختر أيقونة", + "material-icons": "أيقونات مادية", + "show-all": "عرض جميع الأيقونات", + "search-icon": "بحث عن أيقونة", + "no-icons-found": "لم يتم العثور على أيقونات لـ '{{iconSearch}}'" + }, + "subscription": { + "entity-limit-text": "ومع ذلك، يمكنك ترقية خطة اشتراكك لزيادة حدودك.", + "upgrade-your-plan": "ترقية خطة الاشتراك", + "white-labeling-feature": "ميزة التسمية البيضاء", + "white-labeling-text-full": "قم بإعادة تسمية واجهة ويب منصة ThingsBoard باستخدام شعار شركتك أو منتجك ونظام ألوانك في 2 دقيقة.

إزالة عبارة 'Powered By' في تذييل لوحات القيادة.
لا تحتاج إلى برمجة أو إعادة تشغيل الخدمة. اسمح لعملائك أيضًا بتسمية واجهتهم بالأبيض.", + "enable-white-labeling": "قم بتمكين ميزة التسمية البيضاء الآن من خلال ترقية خطة اشتراكك!", + "read-more": "اقرأ المزيد", + "white-labeling-video-text": "انظر إلى البرنامج التعليمي الفيديو أدناه لمعرفة كيفية عمل هذه الميزة!" + }, + "subscription-error": { + "title": "انتهاك الاشتراك", + "warning-title": "تحذير بخصوص الاشتراك", + "upgrade-subscription-plan": "الرجاء ترقية خطة اشتراكك", + "upgrade-subscription-plan-to-install-solution-template": "من أجل تثبيت الحل {{solutionTemplateName}} يجب عليك ترقية اشتراكك على الأقل إلى خطة {{planName}}!", + "limit-reached": { + "device-count": "لقد وصلت إلى الحد الأقصى للأجهزة ({{value}}) المسموح بها بخطة الاشتراك الخاصة بك!", + "asset-count": "لقد وصلت إلى الحد الأقصى للأصول ({{value}}) المسموح بها بخطة الاشتراك الخاصة بك!" + }, + "feature-disabled": { + "white-labeling": "ميزة التسمية البيضاء غير مسموح بها بواسطة خطة الاشتراك الخاصة بك!" + } + }, + "phone-input": { + "phone-input-label": "رقم الهاتف", + "phone-input-required": "رقم الهاتف مطلوب", + "phone-input-validation": "رقم الهاتف غير صالح أو غير ممكن", + "phone-input-pattern": "رقم الهاتف غير صالح. يجب أن يكون بتنسيق E.164، مثل {{phoneNumber}}", + "phone-input-hint": "رقم الهاتف بتنسيق E.164، مثل {{phoneNumber}}" + }, + "custom": { + "widget-action": { + "action-cell-button": "زر العملية في الخلية", + "row-click": "عند النقر على الصف", + "polygon-click": "عند النقر على المضلع", + "marker-click": "عند النقر على العلامة", + "circle-click": "عند النقر على الدائرة", + "tooltip-tag-action": "إجراء الوسم التوضيحي", + "node-selected": "عند تحديد العقدة", + "element-click": "عند النقر على عنصر HTML", + "pie-slice-click": "عند النقر على قطاع البيتزا", + "row-double-click": "عند النقر المزدوج على الصف", + "card-click": "عند النقر على البطاقة" + } + }, + "paginator": { + "items-per-page": "العناصر في كل صفحة:", + "first-page-label": "الصفحة الأولى", + "last-page-label": "الصفحة الأخيرة", + "next-page-label": "الصفحة التالية", + "previous-page-label": "الصفحة السابقة", + "items-per-page-separator": "من" + }, + "language": { + "language": "اللغة" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 288db8c9d2..17c42f85db 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6934,6 +6934,7 @@ "language": { "language": "Language", "locales": { + "ar_AR": "اَلْعَرَبِيَّةُ", "ca_ES": "Catalan", "cs_CZ": "Česky", "da_DK": "Dansk", From b20b6006e9b79e06267e077e1c36cb58e2e9c00a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 1 Mar 2024 14:14:25 +0200 Subject: [PATCH 014/147] Notifications to sysadmin on housekeeper task processing failure --- .../install/ThingsboardInstallService.java | 7 +- .../DefaultHousekeeperService.java | 19 ++++-- .../AlarmsUnassignTaskProcessor.java | 4 +- .../AttributesDeletionTaskProcessor.java | 4 +- .../EntitiesDeletionTaskProcessor.java | 4 +- .../EntityAlarmsDeletionTaskProcessor.java | 4 +- .../EventsDeletionTaskProcessor.java | 4 +- .../processor/HousekeeperTaskProcessor.java | 4 +- .../TelemetryDeletionTaskProcessor.java | 4 +- .../stats/HousekeeperStatsService.java | 3 +- .../DefaultSystemDataLoaderService.java | 37 ++++++----- .../install/SystemDataLoaderService.java | 2 +- ...TaskProcessingFailureTriggerProcessor.java | 53 +++++++++++++++ .../NotificationTemplateService.java | 2 + .../AlarmsUnassignHousekeeperTask.java | 2 +- .../EntitiesDeletionHousekeeperTask.java | 9 ++- .../data/housekeeper}/HousekeeperTask.java | 8 ++- .../data/housekeeper/HousekeeperTaskType.java | 34 ++++++++++ .../data/notification/NotificationType.java | 3 +- ...TaskProcessingFailureNotificationInfo.java | 61 +++++++++++++++++ .../trigger/TaskProcessingFailureTrigger.java | 65 +++++++++++++++++++ .../config/NotificationRuleTriggerConfig.java | 1 + .../config/NotificationRuleTriggerType.java | 3 +- ...gFailureNotificationRuleTriggerConfig.java | 21 +++--- .../dao/housekeeper/CleanUpService.java | 28 +++++--- .../dao/housekeeper/HousekeeperService.java | 6 +- .../DefaultNotificationSettingsService.java | 24 +++---- .../DefaultNotificationTemplateService.java | 5 ++ .../notification/DefaultNotifications.java | 14 ++++ .../notification/NotificationTemplateDao.java | 2 + .../JpaNotificationTemplateDao.java | 5 ++ .../NotificationTemplateRepository.java | 5 ++ .../rule-notification-dialog.component.html | 13 ++++ .../rule-notification-dialog.component.ts | 13 +++- .../template-notification-dialog.component.ts | 3 +- .../app/shared/models/notification.models.ts | 18 +++-- .../notification/task_processing_failure.md | 46 +++++++++++++ .../assets/locale/locale.constant-en_US.json | 5 +- 38 files changed, 458 insertions(+), 87 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/TaskProcessingFailureTriggerProcessor.java rename {dao/src/main/java/org/thingsboard/server/dao/housekeeper/data => common/data/src/main/java/org/thingsboard/server/common/data/housekeeper}/AlarmsUnassignHousekeeperTask.java (95%) rename {dao/src/main/java/org/thingsboard/server/dao/housekeeper/data => common/data/src/main/java/org/thingsboard/server/common/data/housekeeper}/EntitiesDeletionHousekeeperTask.java (83%) rename {dao/src/main/java/org/thingsboard/server/dao/housekeeper/data => common/data/src/main/java/org/thingsboard/server/common/data/housekeeper}/HousekeeperTask.java (91%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/info/TaskProcessingFailureNotificationInfo.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java rename dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java => common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java (61%) create mode 100644 ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 44d81902a8..a79ed50eb7 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -110,7 +110,7 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.5.1 to 3.6.0 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.5.1"); dataUpdateService.updateData("3.5.1"); - systemDataLoaderService.updateDefaultNotificationConfigs(); + systemDataLoaderService.updateDefaultNotificationConfigs(true); case "3.6.0": log.info("Upgrading ThingsBoard from version 3.6.0 to 3.6.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.0"); @@ -126,7 +126,10 @@ public class ThingsboardInstallService { case "3.6.2": log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); - systemDataLoaderService.updateDefaultNotificationConfigs(); + systemDataLoaderService.updateDefaultNotificationConfigs(true); + case "3.6.3": + log.info("Upgrading ThingsBoard from version 3.6.3 to 3.6.4 ..."); + systemDataLoaderService.updateDefaultNotificationConfigs(false); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index c9054c30fa..56123af64d 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -21,10 +21,12 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.notification.rule.trigger.TaskProcessingFailureTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.housekeeper.HousekeeperService; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -60,6 +62,7 @@ public class DefaultHousekeeperService implements HousekeeperService { private final TbQueueProducer> producer; private final HousekeeperReprocessingService reprocessingService; private final HousekeeperStatsService statsService; + private final NotificationRuleProcessor notificationRuleProcessor; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); private final ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); @@ -75,11 +78,13 @@ public class DefaultHousekeeperService implements HousekeeperService { TbCoreQueueFactory queueFactory, TbQueueProducerProvider producerProvider, HousekeeperStatsService statsService, + NotificationRuleProcessor notificationRuleProcessor, @Lazy List> taskProcessors) { this.consumer = queueFactory.createHousekeeperMsgConsumer(); this.producer = producerProvider.getHousekeeperMsgProducer(); this.reprocessingService = reprocessingService; this.statsService = statsService; + this.notificationRuleProcessor = notificationRuleProcessor; this.taskProcessors = taskProcessors.stream().collect(Collectors.toMap(HousekeeperTaskProcessor::getTaskType, p -> p)); } @@ -155,14 +160,14 @@ public class DefaultHousekeeperService implements HousekeeperService { task.getTaskType(), msg.getTask().getAttempt(), task, error); reprocessingService.submitForReprocessing(msg, error); statsService.reportFailure(task.getTaskType(), msg); + notificationRuleProcessor.process(TaskProcessingFailureTrigger.builder() + .task(task) + .error(error) + .attempt(msg.getTask().getAttempt()) + .build()); } } - @Override - public void submitTask(HousekeeperTask task) { - submitTask(task.getEntityId().getId(), task); - } - @Override public void submitTask(UUID key, HousekeeperTask task) { log.trace("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java index 68fd05deec..9a8517903f 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java @@ -20,8 +20,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; -import org.thingsboard.server.dao.housekeeper.data.AlarmsUnassignHousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.AlarmsUnassignHousekeeperTask; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java index 0467e50aeb..c8ff5306a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java @@ -19,8 +19,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; @Component @RequiredArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java index da7af3c42b..8028a76199 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java @@ -19,8 +19,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; -import org.thingsboard.server.dao.housekeeper.data.EntitiesDeletionHousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.EntitiesDeletionHousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; @Component @RequiredArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java index a91f2262f9..974a95eb08 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java @@ -18,8 +18,8 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; @Component @RequiredArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java index 481f9f1bbf..551c8ce7e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java @@ -18,8 +18,8 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; @Component @RequiredArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java index 7daa99540e..79e87fcc91 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.housekeeper.processor; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; public interface HousekeeperTaskProcessor { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java index e7da740261..c7f37ed091 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -20,8 +20,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java index ab4f699ed1..3f6e88a299 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -23,8 +23,7 @@ import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import java.util.ArrayList; diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index b20a548d90..45ae1280b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -548,7 +548,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { } else { ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value) - , System.currentTimeMillis()))); + , System.currentTimeMillis()))); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); } } @@ -692,23 +692,26 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override @SneakyThrows - public void updateDefaultNotificationConfigs() { - PageDataIterable tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500); - ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4)); - log.info("Updating default edge failure notification configs for all tenants"); - AtomicInteger count = new AtomicInteger(); - for (TenantId tenantId : tenants) { - executor.submit(() -> { - notificationSettingsService.updateDefaultNotificationConfigs(tenantId); - int n = count.incrementAndGet(); - if (n % 500 == 0) { - log.info("{} tenants processed", n); - } - }); - } - executor.shutdown(); - executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + public void updateDefaultNotificationConfigs(boolean updateTenants) { + log.info("Updating notification configs..."); notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID); + + if (updateTenants) { + PageDataIterable tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500); + ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4)); + AtomicInteger count = new AtomicInteger(); + for (TenantId tenantId : tenants) { + executor.submit(() -> { + notificationSettingsService.updateDefaultNotificationConfigs(tenantId); + int n = count.incrementAndGet(); + if (n % 500 == 0) { + log.info("{} tenants processed", n); + } + }); + } + executor.shutdown(); + executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index eeac1b6aa4..7c05ff620f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -35,6 +35,6 @@ public interface SystemDataLoaderService { void createDefaultNotificationConfigs(); - void updateDefaultNotificationConfigs(); + void updateDefaultNotificationConfigs(boolean updateTenants); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/TaskProcessingFailureTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/TaskProcessingFailureTriggerProcessor.java new file mode 100644 index 0000000000..7754690215 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/TaskProcessingFailureTriggerProcessor.java @@ -0,0 +1,53 @@ +/** + * 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.notification.rule.trigger; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.notification.info.TaskProcessingFailureNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.TaskProcessingFailureTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig; + +@Service +public class TaskProcessingFailureTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(TaskProcessingFailureTrigger trigger, TaskProcessingFailureNotificationRuleTriggerConfig triggerConfig) { + return true; + } + + @Override + public TaskProcessingFailureNotificationInfo constructNotificationInfo(TaskProcessingFailureTrigger trigger) { + HousekeeperTask task = trigger.getTask(); + return TaskProcessingFailureNotificationInfo.builder() + .tenantId(task.getTenantId()) + .entityId(task.getEntityId()) + .taskType(task.getTaskType()) + .taskDescription(task.getDescription()) + .error(StringUtils.truncate(ExceptionUtils.getStackTrace(trigger.getError()), 1024)) + .attempt(trigger.getAttempt()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java index 1f4019063b..664f976904 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java @@ -32,6 +32,8 @@ public interface NotificationTemplateService { PageData findNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes, PageLink pageLink); + int countNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes); + void deleteNotificationTemplateById(TenantId tenantId, NotificationTemplateId id); void deleteNotificationTemplatesByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java similarity index 95% rename from dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java rename to common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java index 8761229e89..af4e746987 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/AlarmsUnassignHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.housekeeper.data; +package org.thingsboard.server.common.data.housekeeper; import lombok.AccessLevel; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java similarity index 83% rename from dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java rename to common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java index e20f3ffe0c..515703991c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/EntitiesDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.housekeeper.data; +package org.thingsboard.server.common.data.housekeeper; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; @@ -34,4 +35,10 @@ public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { this.entityType = entityType; } + @JsonIgnore + @Override + public String getDescription() { + return entityType.getNormalName().toLowerCase() + "s deletion"; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java rename to common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index 6b63d43d43..44598d511c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.housekeeper.data; +package org.thingsboard.server.common.data.housekeeper; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -76,4 +77,9 @@ public class HousekeeperTask implements Serializable { return new EntitiesDeletionHousekeeperTask(tenantId, entityType); } + @JsonIgnore + public String getDescription() { + return taskType.getDescription() + " for " + entityId.getEntityType().getNormalName().toLowerCase() + " " + entityId.getId(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java new file mode 100644 index 0000000000..4f30f94a45 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java @@ -0,0 +1,34 @@ +/** + * 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.housekeeper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum HousekeeperTaskType { + + DELETE_ENTITIES("entities deletion"), + DELETE_ATTRIBUTES("attributes deletion"), + DELETE_TELEMETRY("telemetry deletion"), + DELETE_EVENTS("events deletion"), + UNASSIGN_ALARMS("alarms unassigning"), + DELETE_ENTITY_ALARMS("entity alarms deletion"); + + private final String description; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java index 5a8fd87933..ec66a3d53a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java @@ -30,6 +30,7 @@ public enum NotificationType { RULE_NODE, RATE_LIMITS, EDGE_CONNECTION, - EDGE_COMMUNICATION_FAILURE + EDGE_COMMUNICATION_FAILURE, + TASK_PROCESSING_FAILURE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/TaskProcessingFailureNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/TaskProcessingFailureNotificationInfo.java new file mode 100644 index 0000000000..cbee861c04 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/TaskProcessingFailureNotificationInfo.java @@ -0,0 +1,61 @@ +/** + * 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.notification.info; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Map; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TaskProcessingFailureNotificationInfo implements RuleOriginatedNotificationInfo { + + private TenantId tenantId; + private EntityId entityId; + private HousekeeperTaskType taskType; + private String taskDescription; + private String error; + private int attempt; + + @Override + public Map getTemplateData() { + return mapOf( + "tenantId", tenantId.toString(), + "entityType", entityId.getEntityType().getNormalName(), + "entityId", entityId.getId().toString(), + "taskType", taskType.getDescription(), + "taskDescription", taskDescription, + "error", error, + "attempt", String.valueOf(attempt) + ); + } + + @Override + public TenantId getAffectedTenantId() { + return tenantId; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java new file mode 100644 index 0000000000..9ccfac6aff --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java @@ -0,0 +1,65 @@ +/** + * 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.notification.rule.trigger; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; + +import java.util.concurrent.TimeUnit; + +@Data +@Builder +public class TaskProcessingFailureTrigger implements NotificationRuleTrigger { + + private final HousekeeperTask task; + private final int attempt; + private final Throwable error; + + @Override + public NotificationRuleTriggerType getType() { + return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE; + } + + @Override + public TenantId getTenantId() { + return task.getTenantId(); + } + + @Override + public EntityId getOriginatorEntityId() { + return task.getEntityId(); + } + + @Override + public boolean deduplicate() { + return true; + } + + @Override + public String getDeduplicationKey() { + return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), task.getTaskType().toString()); + } + + @Override + public long getDefaultDeduplicationDuration() { + return TimeUnit.MINUTES.toMillis(30); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java index 15a5e59255..1621ec8120 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java @@ -38,6 +38,7 @@ import java.io.Serializable; @Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"), @Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"), @Type(value = EdgeCommunicationFailureNotificationRuleTriggerConfig.class, name = "EDGE_COMMUNICATION_FAILURE"), + @Type(value = TaskProcessingFailureNotificationRuleTriggerConfig.class, name = "TASK_PROCESSING_FAILURE") }) public interface NotificationRuleTriggerConfig extends Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java index 8469fac752..8846ae284d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java @@ -31,7 +31,8 @@ public enum NotificationRuleTriggerType { NEW_PLATFORM_VERSION(false), ENTITIES_LIMIT(false), API_USAGE_LIMIT(false), - RATE_LIMITS(false); + RATE_LIMITS(false), + TASK_PROCESSING_FAILURE(false); private final boolean tenantLevel; diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java similarity index 61% rename from dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java index 51fb9ab926..73f0517238 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/data/HousekeeperTaskType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java @@ -13,13 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.housekeeper.data; +package org.thingsboard.server.common.data.notification.rule.trigger.config; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TaskProcessingFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE; + } -public enum HousekeeperTaskType { - DELETE_ENTITIES, - DELETE_ATTRIBUTES, - DELETE_TELEMETRY, // maybe divide into latest and ts kv history? - DELETE_EVENTS, - UNASSIGN_ALARMS, - DELETE_ENTITY_ALARMS } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 5bdcd341b8..b81b6dc5e6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -21,12 +21,13 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; import org.thingsboard.server.dao.relation.RelationService; +import java.util.Optional; import java.util.UUID; @Component @@ -34,7 +35,7 @@ import java.util.UUID; @Slf4j public class CleanUpService { - private final HousekeeperService housekeeperService; + private final Optional housekeeperService; private final RelationService relationService; @TransactionalEventListener(fallbackExecution = true) @@ -44,24 +45,31 @@ public class CleanUpService { log.trace("[{}] Handling entity deletion event: {}", tenantId, event); cleanUpRelatedData(tenantId, entityId); if (entityId.getEntityType() == EntityType.USER) { - housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); + housekeeperService.ifPresent(housekeeperService -> { + housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); + }); } } public void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { // todo: skipped entities list relationService.deleteEntityRelations(tenantId, entityId); - housekeeperService.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); + housekeeperService.ifPresent(housekeeperService -> { + housekeeperService.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); + housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); + }); } public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { UUID tasksKey = UUID.randomUUID(); // so that all tasks are pushed to single partition to be processed synchronously - for (EntityType entityType : entityTypes) { - housekeeperService.submitTask(tasksKey, HousekeeperTask.deleteEntities(tenantId, entityType)); - } + // todo: just use tenantId as key in the impl + housekeeperService.ifPresent(housekeeperService -> { + for (EntityType entityType : entityTypes) { + housekeeperService.submitTask(tasksKey, HousekeeperTask.deleteEntities(tenantId, entityType)); + } + }); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java index 5e55318953..1bbbb25df1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.dao.housekeeper; -import org.thingsboard.server.dao.housekeeper.data.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import java.util.UUID; public interface HousekeeperService { - void submitTask(HousekeeperTask task); + default void submitTask(HousekeeperTask task) { + submitTask(task.getEntityId().getId(), task); + } // tasks with the same key will be pushed to the same partition and thus processed synchronously void submitTask(UUID key, HousekeeperTask task); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 1bdc835b29..a37d37e849 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -173,17 +173,15 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForTenant, affectedTenantAdmins.getId()); - defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForTenant, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForTenant, affectedTenantAdmins.getId()); - defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId()); - defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, tenantAdmins.getId()); return; } @@ -206,19 +204,19 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS @Override public void updateDefaultNotificationConfigs(TenantId tenantId) { if (tenantId.isSysTenantId()) { - if (notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, - List.of(NotificationType.RATE_LIMITS), new PageLink(1)).getTotalElements() > 0) { - return; - } - NotificationTarget sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.SYSTEM_ADMINISTRATORS).stream() .findFirst().orElseGet(() -> createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators")); NotificationTarget affectedTenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.AFFECTED_TENANT_ADMINISTRATORS).stream() .findFirst().orElseGet(() -> createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), "")); - defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId()); - defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId()); - defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId()); + if (!isNotificationConfigured(tenantId, NotificationType.RATE_LIMITS)) { + defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId()); + } + if (!isNotificationConfigured(tenantId, NotificationType.TASK_PROCESSING_FAILURE)) { + defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, sysAdmins.getId()); + } } else { var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE); var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes( @@ -252,6 +250,10 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS } } + private boolean isNotificationConfigured(TenantId tenantId, NotificationType... notificationTypes) { + return notificationTemplateService.countNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, List.of(notificationTypes)) > 0; + } + private NotificationTarget createTarget(TenantId tenantId, String name, UsersFilter filter, String description) { NotificationTarget target = new NotificationTarget(); target.setTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index 6cf8239f47..bec6c3d61c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -69,6 +69,11 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im return notificationTemplateDao.findByTenantIdAndNotificationTypesAndPageLink(tenantId, notificationTypes, pageLink); } + @Override + public int countNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes) { + return notificationTemplateDao.countByTenantIdAndNotificationTypes(tenantId, notificationTypes); + } + @Override public void deleteNotificationTemplateById(TenantId tenantId, NotificationTemplateId id) { if (notificationRequestDao.existsByTenantIdAndStatusAndTemplateId(tenantId, NotificationRequestStatus.SCHEDULED, id)) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index fd17618bd1..4f617c2b9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Notif import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; @@ -358,6 +359,19 @@ public class DefaultNotifications { .build()) .build(); + public static final DefaultNotification taskProcessingFailure = DefaultNotification.builder() + .name("Task processing failure notification") + .type(NotificationType.TASK_PROCESSING_FAILURE) + .subject("Failed to process ${taskType}") + .text("Failed to process ${taskDescription} for tenant ${tenantId}: ${error}") + .icon("warning").color(YELLOW_COLOR) + .rule(DefaultRule.builder() + .name("Task processing failure") + .triggerConfig(TaskProcessingFailureNotificationRuleTriggerConfig.builder().build()) + .description("Send notification to system admins when task processing fails") + .build()) + .build(); + public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder() .name("JWT Signing Key issue notification") .type(NotificationType.GENERAL) diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java index d9a80ce3f5..136bc4d472 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java @@ -30,6 +30,8 @@ public interface NotificationTemplateDao extends Dao, Expo PageData findByTenantIdAndNotificationTypesAndPageLink(TenantId tenantId, List notificationTypes, PageLink pageLink); + int countByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes); + void removeByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java index 8ee6c3df3a..8950ab1e35 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java @@ -53,6 +53,11 @@ public class JpaNotificationTemplateDao extends JpaAbstractDao notificationTypes) { + return notificationTemplateRepository.countByTenantIdAndNotificationTypes(tenantId.getId(), notificationTypes); + } + @Override public void removeByTenantId(TenantId tenantId) { notificationTemplateRepository.deleteByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java index b3e416a491..bb45c10d06 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java @@ -42,6 +42,11 @@ public interface NotificationTemplateRepository extends JpaRepository notificationTypes); + @Transactional @Modifying @Query("DELETE FROM NotificationTemplateEntity t WHERE t.tenantId = :tenantId") diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index 5b477d0345..f5a2ddba17 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -581,6 +581,19 @@ + + + {{ 'notification.task-processing-failure-trigger-settings' | translate }} +
+
+ + notification.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts index 89488b7517..bc6935b4e7 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts @@ -101,6 +101,7 @@ export class RuleNotificationDialogComponent extends rateLimitsTemplateForm: FormGroup; edgeCommunicationFailureTemplateForm: FormGroup; edgeConnectionTemplateForm: FormGroup; + taskProcessingFailureTemplateForm: FormGroup; triggerType = TriggerType; triggerTypes: TriggerType[]; @@ -337,6 +338,12 @@ export class RuleNotificationDialogComponent extends }) }); + this.taskProcessingFailureTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + taskTypes: [] + }) + }); + this.triggerTypeFormsMap = new Map([ [TriggerType.ALARM, this.alarmTemplateForm], [TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm], @@ -349,7 +356,8 @@ export class RuleNotificationDialogComponent extends [TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm], [TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm], [TriggerType.EDGE_COMMUNICATION_FAILURE, this.edgeCommunicationFailureTemplateForm], - [TriggerType.EDGE_CONNECTION, this.edgeConnectionTemplateForm] + [TriggerType.EDGE_CONNECTION, this.edgeConnectionTemplateForm], + [TriggerType.TASK_PROCESSING_FAILURE, this.taskProcessingFailureTemplateForm] ]); if (data.isAdd || data.isCopy) { @@ -488,7 +496,8 @@ export class RuleNotificationDialogComponent extends TriggerType.ENTITIES_LIMIT, TriggerType.API_USAGE_LIMIT, TriggerType.NEW_PLATFORM_VERSION, - TriggerType.RATE_LIMITS + TriggerType.RATE_LIMITS, + TriggerType.TASK_PROCESSING_FAILURE ]); if (this.isSysAdmin()) { diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts index c0ae4f1abb..ccbcb6bf22 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts @@ -179,7 +179,8 @@ export class TemplateNotificationDialogComponent NotificationType.ENTITIES_LIMIT, NotificationType.API_USAGE_LIMIT, NotificationType.NEW_PLATFORM_VERSION, - NotificationType.RATE_LIMITS + NotificationType.RATE_LIMITS, + NotificationType.TASK_PROCESSING_FAILURE ]); if (this.isSysAdmin()) { diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 2cc7d8547b..0c5e1c5a90 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -523,7 +523,8 @@ export enum NotificationType { RULE_NODE = 'RULE_NODE', RATE_LIMITS = 'RATE_LIMITS', EDGE_CONNECTION = 'EDGE_CONNECTION', - EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE' + EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE', + TASK_PROCESSING_FAILURE = 'TASK_PROCESSING_FAILURE' } export const NotificationTypeIcons = new Map([ @@ -535,6 +536,7 @@ export const NotificationTypeIcons = new Map([ [NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'], [NotificationType.ENTITIES_LIMIT, 'data_thresholding'], [NotificationType.API_USAGE_LIMIT, 'insert_chart'], + [NotificationType.TASK_PROCESSING_FAILURE, 'warning'] ]); export const AlarmSeverityNotificationColors = new Map( @@ -646,7 +648,13 @@ export const NotificationTemplateTypeTranslateMap = new Map([ @@ -676,7 +685,8 @@ export const TriggerTypeTranslationMap = new Map([ [TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'], [TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'], [TriggerType.EDGE_CONNECTION, 'notification.trigger.edge-connection'], - [TriggerType.EDGE_COMMUNICATION_FAILURE, 'notification.trigger.edge-communication-failure'] + [TriggerType.EDGE_COMMUNICATION_FAILURE, 'notification.trigger.edge-communication-failure'], + [TriggerType.TASK_PROCESSING_FAILURE, 'notification.trigger.task-processing-failure'] ]); export interface NotificationUserSettings { diff --git a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md new file mode 100644 index 0000000000..0bcf107298 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md @@ -0,0 +1,46 @@ +#### Task processing failure notification templatization + +
+
+ +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `taskType` - the task type, e.g. 'telemetry deletion'; +* `taskDescription` - the task description, e.g. 'telemetry deletion for device c4d93dc0-63a1-11ee-aa6d-f7cbc0a71325'; +* `error` - the error stacktrace +* `tenantId` - the tenant id; +* `entityType` - the type of the entity to which the task is related; +* `entityId` - the id of the entity to which the task is related; +* `attempt` - the number of attempts processing the task + +Parameter names must be wrapped using `${...}`. For example: `${entityType}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${entityType:upperCase}` +* `lowerCase`, for example - `${entityType:lowerCase}` +* `capitalize`, for example - `${entityType:capitalize}` + +
+ +##### Examples + +Let's assume that telemetry deletion failed for some device. +The following template: + +```text +Failed to process ${taskType} for ${entityType:lowerCase} ${entityId} +{:copy-code} +``` + +will be transformed to: + +```text +Failed to process telemetry deletion for device c4d93dc0-63a1-11ee-aa6d-f7cbc0a71325 +``` + +
+
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0428087620..f94a2fffd3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3241,6 +3241,7 @@ "api-usage-trigger-settings": "API usage trigger settings", "new-platform-version-trigger-settings": "New platform version trigger settings", "rate-limits-trigger-settings": "Exceeded rate limits trigger settings", + "task-processing-failure-trigger-settings": "Task processing failure trigger settings", "at-least-one-should-be-selected": "At least one should be selected", "basic-settings": "Basic settings", "button-text": "Button text", @@ -3450,7 +3451,8 @@ "new-platform-version": "New platform version", "rate-limits": "Exceeded rate limits", "edge-communication-failure": "Edge communication failure", - "edge-connection": "Edge connection" + "edge-connection": "Edge connection", + "task-processing-failure": "Task processing failure" }, "templates": "Templates", "notification-templates": "Notifications / Templates", @@ -3473,6 +3475,7 @@ "rate-limits": "Exceeded rate limits", "edge-connection": "Edge connection", "edge-communication-failure": "Edge communication failure", + "task-processing-failure": "Task processing failure", "trigger": "Trigger", "trigger-required": "Trigger is required" }, From b82bceceb4142e31167293871a304d2f63cbda89 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 7 Mar 2024 17:01:33 +0200 Subject: [PATCH 015/147] Housekeeper: tests and refactoring --- .../DefaultHousekeeperService.java | 20 +- .../EntitiesDeletionTaskProcessor.java | 7 +- .../EntityAlarmsDeletionTaskProcessor.java | 2 + .../DefaultExportableEntitiesService.java | 3 - .../housekeeper/HousekeeperServiceTest.java | 303 ++++++++++++++++++ .../entity/DefaultEntityServiceRegistry.java | 4 +- .../dao/housekeeper/CleanUpService.java | 22 +- .../dao/housekeeper/HousekeeperService.java | 9 +- .../server/dao/rule/BaseRuleChainService.java | 8 +- 9 files changed, 344 insertions(+), 34 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 56123af64d..63e8bb6a75 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -42,7 +42,6 @@ import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; import javax.annotation.PreDestroy; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -58,11 +57,12 @@ public class DefaultHousekeeperService implements HousekeeperService { private final Map> taskProcessors; - private final TbQueueConsumer> consumer; - private final TbQueueProducer> producer; private final HousekeeperReprocessingService reprocessingService; private final HousekeeperStatsService statsService; private final NotificationRuleProcessor notificationRuleProcessor; + private final TbQueueConsumer> consumer; + private final TbQueueProducer> producer; + private final TopicPartitionInfo submitTpi; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); private final ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); @@ -80,11 +80,12 @@ public class DefaultHousekeeperService implements HousekeeperService { HousekeeperStatsService statsService, NotificationRuleProcessor notificationRuleProcessor, @Lazy List> taskProcessors) { - this.consumer = queueFactory.createHousekeeperMsgConsumer(); - this.producer = producerProvider.getHousekeeperMsgProducer(); this.reprocessingService = reprocessingService; this.statsService = statsService; this.notificationRuleProcessor = notificationRuleProcessor; + this.consumer = queueFactory.createHousekeeperMsgConsumer(); + this.producer = producerProvider.getHousekeeperMsgProducer(); + this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); this.taskProcessors = taskProcessors.stream().collect(Collectors.toMap(HousekeeperTaskProcessor::getTaskType, p -> p)); } @@ -169,10 +170,13 @@ public class DefaultHousekeeperService implements HousekeeperService { } @Override - public void submitTask(UUID key, HousekeeperTask task) { + public void submitTask(HousekeeperTask task) { log.trace("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); - TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - producer.send(tpi, new TbProtoQueueMsg<>(key, ToHousekeeperServiceMsg.newBuilder() + /* + * using msg key as entity id so that msgs related to certain entity are pushed to same partition, + * e.g. on tenant deletion (entity id is tenant id), we need to clean up tenant entities in certain order + * */ + producer.send(submitTpi, new TbProtoQueueMsg<>(task.getEntityId().getId(), ToHousekeeperServiceMsg.newBuilder() .setTask(HousekeeperTaskProto.newBuilder() .setValue(JacksonUtil.toString(task)) .setTs(task.getTs()) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java index 8028a76199..a16a5f09bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java @@ -17,10 +17,10 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.entity.EntityDaoService; -import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.common.data.housekeeper.EntitiesDeletionHousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; @Component @RequiredArgsConstructor @@ -31,9 +31,6 @@ public class EntitiesDeletionTaskProcessor implements HousekeeperTaskProcessor void removeById(TenantId tenantId, I id) { EntityType entityType = id.getEntityType(); EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); - if (entityService == null) { - throw new IllegalArgumentException("Unsupported entity type " + entityType); - } entityService.deleteEntity(tenantId, id); } diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java new file mode 100644 index 0000000000..b132cf34fd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -0,0 +1,303 @@ +/** + * 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.housekeeper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; +import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.event.LifecycleEvent; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.housekeeper.HousekeeperService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@TestPropertySource(properties = { + "transport.http.enabled=true" +}) +public class HousekeeperServiceTest extends AbstractControllerTest { + + @SpyBean + private HousekeeperService housekeeperService; + @Autowired + private EventService eventService; + @Autowired + private TimeseriesService timeseriesService; + @Autowired + private AttributesService attributesService; + @Autowired + private RuleChainService ruleChainService; + @Autowired + private AlarmService alarmService; + + private TenantId tenantId; + + private static final String TELEMETRY_KEY = "telemetry1"; + private static final String ATTRIBUTE_KEY = "_attribute1"; + private static final String KV_VALUE = "ewfewfwef"; + + @Before + public void setUp() throws Exception { + loginTenantAdmin(); + this.tenantId = super.tenantId; + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void whenDeviceIsDeleted_thenCleanUpRelatedData() throws Exception { + Device device = createDevice("test", "test"); + createRelatedData(device.getId()); + + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoRelatedData(device.getId()); + }); + } + + @Test + public void whenRuleChainIsDeleted_thenCleanUpRelatedData() throws Exception { + RuleChainMetaData ruleChainMetaData = createRuleChain(); + RuleChainId ruleChainId = ruleChainMetaData.getRuleChainId(); + RuleNodeId ruleNode1Id = ruleChainMetaData.getNodes().get(0).getId(); + RuleNodeId ruleNode2Id = ruleChainMetaData.getNodes().get(1).getId(); + createRelatedData(ruleChainId); + createRelatedData(ruleNode1Id); + createRelatedData(ruleNode2Id); + + doDelete("/api/ruleChain/" + ruleChainId).andExpect(status().isOk()); + + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoRelatedData(ruleNode1Id); + verifyNoRelatedData(ruleNode2Id); + verifyNoRelatedData(ruleChainId); + }); + } + + @Test + public void whenUserIsDeleted_thenCleanUpRelatedData() throws Exception { + Device device = createDevice("test", "test"); + UserId userId = customerUserId; + createRelatedData(userId); + Alarm alarm = Alarm.builder() + .type("test") + .tenantId(tenantId) + .originator(device.getId()) + .severity(AlarmSeverity.MAJOR) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + alarm = doPost("/api/alarm/" + alarm.getId() + "/assign/" + userId, "", Alarm.class); + assertThat(alarm.getAssigneeId()).isEqualTo(userId); + assertThat(alarmService.findAlarmIdsByAssigneeId(tenantId, userId, new PageLink(100)).getData()).isNotEmpty(); + + doDelete("/api/user/" + userId).andExpect(status().isOk()); + + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoRelatedData(userId); + }); + assertThat(alarmService.findAlarmById(tenantId, alarm.getId()).getAssigneeId()).isNull(); + } + + @Test + public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception { + loginDifferentTenant(); + tenantId = differentTenantId; + + createRelatedData(tenantId); + + Device device = createDevice("test", "test"); + createRelatedData(device.getId()); + + RuleChainMetaData ruleChainMetaData = createRuleChain(); + RuleChainId ruleChainId = ruleChainMetaData.getRuleChainId(); + RuleNodeId ruleNode1Id = ruleChainMetaData.getNodes().get(0).getId(); + RuleNodeId ruleNode2Id = ruleChainMetaData.getNodes().get(1).getId(); + createRelatedData(ruleChainId); + createRelatedData(ruleNode1Id); + createRelatedData(ruleNode2Id); + + UserId userId = savedDifferentTenantUser.getId(); + createRelatedData(userId); + + loginSysAdmin(); + deleteDifferentTenant(); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoRelatedData(device.getId()); + verifyNoRelatedData(ruleNode1Id); + verifyNoRelatedData(ruleNode2Id); + verifyNoRelatedData(ruleChainId); + verifyNoRelatedData(userId); + verifyNoRelatedData(tenantId); + }); + } + + private void createRelatedData(EntityId entityId) throws Exception { + createTelemetry(entityId); + for (String scope : List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE, DataConstants.CLIENT_SCOPE)) { + createAttribute(entityId, scope, scope + ATTRIBUTE_KEY); + } + createEvent(entityId); + } + + private void verifyNoRelatedData(EntityId entityId) throws Exception { + List expectedTaskTypes = List.of(HousekeeperTaskType.DELETE_TELEMETRY, HousekeeperTaskType.DELETE_ATTRIBUTES, HousekeeperTaskType.DELETE_EVENTS, HousekeeperTaskType.DELETE_ENTITY_ALARMS); + for (HousekeeperTaskType taskType : expectedTaskTypes) { + verify(housekeeperService).submitTask(argThat(task -> task.getTaskType() == taskType && task.getEntityId().equals(entityId))); + } + + assertThat(getLatestTelemetry(entityId)).isNull(); + assertThat(getTimeseriesHistory(entityId)).isEmpty(); + for (String scope : List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE, DataConstants.CLIENT_SCOPE)) { + assertThat(getAttribute(entityId, scope, scope + ATTRIBUTE_KEY)).isNull(); + } + assertThat(getEvents(entityId)).isEmpty(); + } + + private void createAttribute(EntityId entityId, String scope, String key) throws Exception { + attributesService.save(tenantId, entityId, scope, new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, KV_VALUE))).get(); + } + + private void createTelemetry(EntityId entityId) throws Exception { + timeseriesService.save(tenantId, entityId, new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(TELEMETRY_KEY, KV_VALUE))).get(); + } + + private void createEvent(EntityId entityId) { + LifecycleEvent event = LifecycleEvent.builder() + .tenantId(tenantId) + .entityId(entityId.getId()) + .serviceId("test") + .lcEventType("test") + .success(true) + .build(); + eventService.saveAsync(event); + await().atMost(10, TimeUnit.SECONDS) + .until(() -> !getEvents(entityId).isEmpty()); + + } + + private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { + return timeseriesService.findLatest(tenantId, entityId, HousekeeperServiceTest.TELEMETRY_KEY).get().orElse(null); + } + + private List getTimeseriesHistory(EntityId entityId) throws Exception { + return timeseriesService.findAll(tenantId, entityId, List.of(new BaseReadTsKvQuery(HousekeeperServiceTest.TELEMETRY_KEY, 0, System.currentTimeMillis(), 10, "DESC"))).get(); + } + + private AttributeKvEntry getAttribute(EntityId entityId, String scope, String key) throws Exception { + return attributesService.find(tenantId, entityId, scope, key).get().orElse(null); + } + + private List getEvents(EntityId entityId) { + return eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, new TimePageLink(100)).getData() + .stream().filter(event -> Optional.ofNullable(event.getBody()).map(body -> body.get("event")) + .map(JsonNode::asText).orElse("").equals("test")) + .collect(Collectors.toList()); + } + + private RuleChainMetaData createRuleChain() { + RuleChain ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setName("Test"); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(true); + ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); + ruleChain = ruleChainService.saveRuleChain(ruleChain); + RuleChainId ruleChainId = ruleChain.getId(); + + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChainId); + + RuleNode ruleNode1 = new RuleNode(); + ruleNode1.setName("Simple Rule Node 1"); + ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); + ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + ruleNode1.setDebugMode(true); + TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); + configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); + ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); + + RuleNode ruleNode2 = new RuleNode(); + ruleNode2.setName("Simple Rule Node 2"); + ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); + ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + ruleNode2.setDebugMode(true); + TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); + configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); + ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); + + metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); + metaData.setFirstNodeIndex(0); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); + ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); + return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java index 6e6233b1b2..40064fbbde 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.HashMap; import java.util.Map; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -52,7 +53,8 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry { @Override public EntityDaoService getServiceByEntityType(EntityType entityType) { - return entityDaoServicesMap.get(entityType); + return Optional.ofNullable(entityDaoServicesMap.get(entityType)) + .orElseThrow(() -> new IllegalArgumentException("Unsupported entity type " + entityType)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index b81b6dc5e6..13a1e1cb09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.dao.housekeeper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; @@ -24,11 +26,12 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.relation.RelationService; import java.util.Optional; -import java.util.UUID; @Component @RequiredArgsConstructor @@ -37,12 +40,15 @@ public class CleanUpService { private final Optional housekeeperService; private final RelationService relationService; + @Autowired + @Lazy + private EntityServiceRegistry entityServiceRegistry; @TransactionalEventListener(fallbackExecution = true) public void handleEntityDeletionEvent(DeleteEntityEvent event) { TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); - log.trace("[{}] Handling entity deletion event: {}", tenantId, event); + log.trace("[{}][{}][{}] Handling entity deletion event", tenantId, entityId.getEntityType(), entityId.getId()); cleanUpRelatedData(tenantId, entityId); if (entityId.getEntityType() == EntityType.USER) { housekeeperService.ifPresent(housekeeperService -> { @@ -52,6 +58,7 @@ public class CleanUpService { } public void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { + log.trace("[{}][{}][{}] Cleaning up related data", tenantId, entityId.getEntityType(), entityId.getId()); // todo: skipped entities list relationService.deleteEntityRelations(tenantId, entityId); housekeeperService.ifPresent(housekeeperService -> { @@ -63,11 +70,14 @@ public class CleanUpService { } public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { - UUID tasksKey = UUID.randomUUID(); // so that all tasks are pushed to single partition to be processed synchronously - // todo: just use tenantId as key in the impl - housekeeperService.ifPresent(housekeeperService -> { + housekeeperService.ifPresentOrElse(housekeeperService -> { + for (EntityType entityType : entityTypes) { + housekeeperService.submitTask(HousekeeperTask.deleteEntities(tenantId, entityType)); + } + }, () -> { for (EntityType entityType : entityTypes) { - housekeeperService.submitTask(tasksKey, HousekeeperTask.deleteEntities(tenantId, entityType)); + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); + entityService.deleteByTenantId(tenantId); } }); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java index 1bbbb25df1..e3c0800f69 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java @@ -17,15 +17,8 @@ package org.thingsboard.server.dao.housekeeper; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; -import java.util.UUID; - public interface HousekeeperService { - default void submitTask(HousekeeperTask task) { - submitTask(task.getEntityId().getId(), task); - } - - // tasks with the same key will be pushed to the same partition and thus processed synchronously - void submitTask(UUID key, HousekeeperTask task); + void submitTask(HousekeeperTask task); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 92d4ea1baf..18f3d58ba7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -438,12 +438,14 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC checkRuleNodesAndDelete(tenantId, ruleChainId); } + @Transactional @Override public void deleteRuleChainsByTenantId(TenantId tenantId) { Validator.validateId(tenantId, "Incorrect tenant id for delete rule chains request."); tenantRuleChainsRemover.removeEntities(tenantId, tenantId); } + @Transactional @Override public void deleteByTenantId(TenantId tenantId) { deleteRuleChainsByTenantId(tenantId); @@ -776,10 +778,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private void deleteRuleNodes(TenantId tenantId, List ruleNodes) { List ruleNodeIds = ruleNodes.stream().map(RuleNode::getId).collect(Collectors.toList()); - for (var node : ruleNodes) { - cleanUpService.cleanUpRelatedData(tenantId, node.getId()); - } ruleNodeDao.deleteByIdIn(ruleNodeIds); + for (var nodeId : ruleNodeIds) { + cleanUpService.cleanUpRelatedData(tenantId, nodeId); + } } @Override From 0d3c3d6b25c24235bbfcd106ce24d354a82ebca7 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 13 Mar 2024 14:09:05 +0200 Subject: [PATCH 016/147] Housekeeper: tests and refactoring --- .../DefaultHousekeeperService.java | 22 ++++-- .../HousekeeperReprocessingService.java | 34 +++++++--- .../src/main/resources/thingsboard.yml | 9 ++- .../housekeeper/HousekeeperServiceTest.java | 68 +++++++++++++++++-- .../resources/application-test.properties | 1 + .../server/queue/TbQueueConsumer.java | 2 + .../AbstractTbQueueConsumerTemplate.java | 5 ++ .../queue/memory/InMemoryTbQueueConsumer.java | 5 ++ .../dao/housekeeper/CleanUpService.java | 2 +- 9 files changed, 121 insertions(+), 27 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java index 63e8bb6a75..589c570144 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -53,6 +54,7 @@ import java.util.stream.Collectors; @TbCoreComponent @Service @Slf4j +@ConditionalOnProperty(name = "queue.core.housekeeper.enabled", havingValue = "true", matchIfMissing = true) public class DefaultHousekeeperService implements HousekeeperService { private final Map> taskProcessors; @@ -136,9 +138,7 @@ public class DefaultHousekeeperService implements HousekeeperService { } if (log.isDebugEnabled()) { - log.debug("[{}] {} task {}", task.getTenantId(), - msg.getTask().getErrorsCount() == 0 ? "Processing" : "Reprocessing", - msg.getTask().getValue()); + log.debug("[{}] {} task {}", task.getTenantId(), isNew(msg.getTask()) ? "Processing" : "Reprocessing", msg.getTask().getValue()); } try { Future future = executor.submit(() -> { @@ -146,7 +146,9 @@ public class DefaultHousekeeperService implements HousekeeperService { return null; }); future.get(taskProcessingTimeout, TimeUnit.MILLISECONDS); + statsService.reportProcessed(task.getTaskType(), msg); + log.debug("[{}] Successfully {} task {}", task.getTenantId(), isNew(msg.getTask()) ? "processed" : "reprocessed", msg.getTask().getValue()); } catch (InterruptedException e) { throw e; } catch (Throwable e) { @@ -160,6 +162,7 @@ public class DefaultHousekeeperService implements HousekeeperService { task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType(), msg.getTask().getAttempt(), task, error); reprocessingService.submitForReprocessing(msg, error); + statsService.reportFailure(task.getTaskType(), msg); notificationRuleProcessor.process(TaskProcessingFailureTrigger.builder() .task(task) @@ -185,12 +188,19 @@ public class DefaultHousekeeperService implements HousekeeperService { .build()), null); } + private boolean isNew(HousekeeperTaskProto task) { + return task.getErrorsCount() == 0; + } + @PreDestroy - private void stop() { - log.info("Stopped Housekeeper service"); + private void stop() throws Exception { stopped = true; consumer.unsubscribe(); - consumerExecutor.shutdownNow(); + consumerExecutor.shutdown(); + if (!consumerExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + consumerExecutor.shutdownNow(); + } + log.info("Stopped Housekeeper service"); } } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 4d361cfba4..c37496ff49 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; @@ -47,16 +49,18 @@ import java.util.concurrent.TimeUnit; @TbCoreComponent @Service @Slf4j +@ConditionalOnProperty(name = "queue.core.housekeeper.enabled", havingValue = "true", matchIfMissing = true) public class HousekeeperReprocessingService { private final DefaultHousekeeperService housekeeperService; private final PartitionService partitionService; private final TbCoreQueueFactory queueFactory; - private final TbQueueProducerProvider producerProvider; + private final TbQueueProducer> producer; + private final TopicPartitionInfo submitTpi; - @Value("${queue.core.housekeeper.reprocessing-start-delay-sec:15}") // fixme: to 5 minutes + @Value("${queue.core.housekeeper.reprocessing-start-delay-sec:300}") private int startDelay; - @Value("${queue.core.housekeeper.task-reprocessing-delay-sec:30}") // fixme: to 30 minutes or 1 hour + @Value("${queue.core.housekeeper.task-reprocessing-delay-sec:3600}") private int reprocessingDelay; @Value("${queue.core.housekeeper.max-reprocessing-attempts:10}") private int maxReprocessingAttempts; @@ -74,7 +78,8 @@ public class HousekeeperReprocessingService { this.housekeeperService = housekeeperService; this.partitionService = partitionService; this.queueFactory = queueFactory; - this.producerProvider = producerProvider; + this.producer = producerProvider.getHousekeeperReprocessingMsgProducer(); + this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); } @PostConstruct @@ -101,7 +106,13 @@ public class HousekeeperReprocessingService { while (!stopped) { try { List> msgs = consumer.poll(pollInterval); - if (msgs.isEmpty() || msgs.stream().anyMatch(msg -> msg.getValue().getTask().getTs() >= startTs)) { // msg batch size should be 1. otherwise some tasks won't be reprocessed immediately + if (msgs.isEmpty() || msgs.stream().anyMatch(msg -> msg.getValue().getTask().getTs() >= startTs)) { + // it's not time yet to process the message + if (!consumer.isCommitSupported()) { + for (TbProtoQueueMsg msg : msgs) { + producer.send(submitTpi, new TbProtoQueueMsg<>(msg.getKey(), msg.getValue()), null); + } + } break; } @@ -114,7 +125,6 @@ public class HousekeeperReprocessingService { } catch (Throwable e) { log.error("Unexpected error during message reprocessing [{}]", msg, e); submitForReprocessing(msg.getValue(), e); - // fixme: msgs are duplicated } } consumer.commit(); @@ -150,16 +160,18 @@ public class HousekeeperReprocessingService { .build(); log.trace("Submitting for reprocessing: {}", msg); - var producer = producerProvider.getHousekeeperReprocessingMsgProducer(); - TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); // reprocessing topic has single partition, so we don't care about the msg key + producer.send(submitTpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); // reprocessing topic has single partition, so we don't care about the msg key } @PreDestroy - private void stop() { + private void stop() throws Exception { stopped = true; scheduler.shutdownNow(); - consumerExecutor.shutdownNow(); + consumerExecutor.shutdown(); + if (!consumerExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + consumerExecutor.shutdownNow(); + } + log.info("Stopped Housekeeper reprocessing service"); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4ced0655f0..a156762c71 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1427,8 +1427,10 @@ queue: ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Version Control topic version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic housekeeper: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1" - housekeeper-reprocessing: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1" + # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days + housekeeper-reprocessing: "retention.ms:7776000000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1585,12 +1587,13 @@ queue: # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" housekeeper: + enabled: "${TB_HOUSEKEEPER_ENABLED:true}" topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" reprocessing-topic: "${TB_HOUSEKEEPER_REPROCESSING_TOPIC:tb_housekeeper.reprocessing}" poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" - reprocessing-start-delay-sec: "${TB_HOUSEKEEPER_REPROCESSING_START_DELAY_SEC:15}" # fixme: to 5 minutes - task-reprocessing-delay-sec: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_SEC:30}" # fixme: to 30 minutes or 1 hour + reprocessing-start-delay-sec: "${TB_HOUSEKEEPER_REPROCESSING_START_DELAY_SEC:300}" + task-reprocessing-delay-sec: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_SEC:3600}" max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:30}" vc: diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index b132cf34fd..a0564d3bcb 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.LifecycleEvent; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -56,33 +58,46 @@ import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.housekeeper.HousekeeperService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.service.housekeeper.processor.TelemetryDeletionTaskProcessor; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @TestPropertySource(properties = { - "transport.http.enabled=true" + "queue.core.housekeeper.enabled=true", + "transport.http.enabled=true", + "queue.core.housekeeper.reprocessing-start-delay-sec=1", + "queue.core.housekeeper.task-reprocessing-delay-sec=2", + "queue.core.housekeeper.poll-interval-ms=1000" }) public class HousekeeperServiceTest extends AbstractControllerTest { @SpyBean - private HousekeeperService housekeeperService; + private DefaultHousekeeperService housekeeperService; + @SpyBean + private HousekeeperReprocessingService housekeeperReprocessingService; @Autowired private EventService eventService; @Autowired @@ -93,6 +108,8 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private RuleChainService ruleChainService; @Autowired private AlarmService alarmService; + @SpyBean + private TelemetryDeletionTaskProcessor telemetryDeletionTaskProcessor; private TenantId tenantId; @@ -112,7 +129,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenDeviceIsDeleted_thenCleanUpRelatedData() throws Exception { - Device device = createDevice("test", "test"); + Device device = createDevice("wekfwepf", "wekfwepf"); createRelatedData(device.getId()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); @@ -143,7 +160,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenUserIsDeleted_thenCleanUpRelatedData() throws Exception { - Device device = createDevice("test", "test"); + Device device = createDevice("vneoruvhwe", "vneoruvhwe"); UserId userId = customerUserId; createRelatedData(userId); Alarm alarm = Alarm.builder() @@ -172,7 +189,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { createRelatedData(tenantId); - Device device = createDevice("test", "test"); + Device device = createDevice("oi324rujoi", "oi324rujoi"); createRelatedData(device.getId()); RuleChainMetaData ruleChainMetaData = createRuleChain(); @@ -199,6 +216,45 @@ public class HousekeeperServiceTest extends AbstractControllerTest { }); } + @Test + public void whenTaskProcessingFails_thenReprocessUntilSuccessful() throws Exception { + TimeoutException error = new TimeoutException("Test timeout"); + doThrow(error).when(telemetryDeletionTaskProcessor).process(any()); + + Device device = createDevice("vep9ruv32", "vep9ruv32"); + createRelatedData(device.getId()); + + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + + int attempts = 3; + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + verify(housekeeperService).processTask(argThat(verifyTaskSubmission(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, + task -> task.getErrorsCount() == 0))); + + for (int i = 1; i <= attempts; i++) { + int attempt = i; + verify(housekeeperReprocessingService).submitForReprocessing(argThat(verifyTaskSubmission(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, + task -> task.getErrorsCount() > 0 && task.getAttempt() == attempt)), argThat(e -> e.getMessage().equals(error.getMessage()))); + verify(housekeeperService).processTask(argThat(verifyTaskSubmission(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, + task -> task.getErrorsCount() > 0 && task.getAttempt() == attempt))); + } + }); + + assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); + doCallRealMethod().when(telemetryDeletionTaskProcessor).process(any()); // fixing the code + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(getTimeseriesHistory(device.getId())).isEmpty(); + }); + } + + private ArgumentMatcher verifyTaskSubmission(EntityId entityId, HousekeeperTaskType taskType, + Predicate additionalCheck) { + return msg -> { + HousekeeperTask task = JacksonUtil.fromString(msg.getTask().getValue(), HousekeeperTask.class); + return task.getEntityId().equals(entityId) && task.getTaskType() == taskType && additionalCheck.test(msg.getTask()); + }; + } + private void createRelatedData(EntityId entityId) throws Exception { createTelemetry(entityId); for (String scope : List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE, DataConstants.CLIENT_SCOPE)) { diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 92959e4552..5e4900fdc1 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -54,3 +54,4 @@ sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 server.log_controller_error_stack_trace=false +queue.core.housekeeper.enabled=false diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java index 51456ab0e2..3747e6d026 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java @@ -36,6 +36,8 @@ public interface TbQueueConsumer { void commit(); + boolean isCommitSupported(); + boolean isStopped(); List getFullTopicNames(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 531e30b343..9105c829bc 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -201,6 +201,11 @@ public abstract class AbstractTbQueueConsumerTemplate i return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); } + @Override + public boolean isCommitSupported() { + return true; + } + protected boolean isLongPollingSupported() { return false; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java index de3833d124..269924087b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java @@ -114,4 +114,9 @@ public class InMemoryTbQueueConsumer implements TbQueueCon return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); } + @Override + public boolean isCommitSupported() { + return false; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 13a1e1cb09..015917e592 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -58,7 +58,7 @@ public class CleanUpService { } public void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { - log.trace("[{}][{}][{}] Cleaning up related data", tenantId, entityId.getEntityType(), entityId.getId()); + log.debug("[{}][{}][{}] Cleaning up related data", tenantId, entityId.getEntityType(), entityId.getId()); // todo: skipped entities list relationService.deleteEntityRelations(tenantId, entityId); housekeeperService.ifPresent(housekeeperService -> { From 82476b629c0c6ac3ad31b627aa5734cf66d1f50d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 13 Mar 2024 18:34:27 +0200 Subject: [PATCH 017/147] Max reprocessing attempts for Housekeeper task --- .../HousekeeperReprocessingService.java | 45 ++++++++++++-- .../housekeeper/HousekeeperServiceTest.java | 58 +++++++++++++++---- 2 files changed, 86 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index c37496ff49..5c577dd39f 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -45,6 +45,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; @TbCoreComponent @Service @@ -70,6 +71,7 @@ public class HousekeeperReprocessingService { private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-scheduler")); + protected AtomicInteger cycle = new AtomicInteger(); private boolean stopped; public HousekeeperReprocessingService(@Lazy DefaultHousekeeperService housekeeperService, @@ -86,6 +88,7 @@ public class HousekeeperReprocessingService { private void init() { scheduler.scheduleWithFixedDelay(() -> { try { + cycle.incrementAndGet(); startReprocessing(); } catch (Throwable e) { log.error("Unexpected error during reprocessing", e); @@ -109,8 +112,9 @@ public class HousekeeperReprocessingService { if (msgs.isEmpty() || msgs.stream().anyMatch(msg -> msg.getValue().getTask().getTs() >= startTs)) { // it's not time yet to process the message if (!consumer.isCommitSupported()) { + // resubmitting consumed messages if committing is not supported (for in-memory queue) for (TbProtoQueueMsg msg : msgs) { - producer.send(submitTpi, new TbProtoQueueMsg<>(msg.getKey(), msg.getValue()), null); + submit(msg.getKey(), msg.getValue()); } } break; @@ -119,7 +123,7 @@ public class HousekeeperReprocessingService { for (TbProtoQueueMsg msg : msgs) { log.trace("Reprocessing task: {}", msg); try { - housekeeperService.processTask(msg.getValue()); + reprocessTask(msg.getValue()); } catch (InterruptedException e) { return; } catch (Throwable e) { @@ -144,7 +148,26 @@ public class HousekeeperReprocessingService { }); } - // todo: dead letter queue if attempts count exceeds the configured maximum + private void reprocessTask(ToHousekeeperServiceMsg msg) throws Exception { + int attempt = msg.getTask().getAttempt(); + if (attempt > maxReprocessingAttempts) { + if (cycle.get() == 1) { // only reprocessing tasks with exceeded failures on first cycle (after start-up) + log.info("Trying to reprocess task with {} failed attempts: {}", attempt, msg); + } else { + // resubmitting msg to be processed on the next service start + msg = msg.toBuilder() + .setTask(msg.getTask().toBuilder() + .setTs(getReprocessingTs()) + .build()) + .build(); + submit(UUID.randomUUID(), msg); + return; + } + } + + housekeeperService.processTask(msg); + } + public void submitForReprocessing(ToHousekeeperServiceMsg msg, Throwable error) { HousekeeperTaskProto task = msg.getTask(); @@ -155,12 +178,24 @@ public class HousekeeperReprocessingService { .setTask(task.toBuilder() .setAttempt(attempt) .clearErrors().addAllErrors(errors) - .setTs(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis((long) (reprocessingDelay * 0.8))) + .setTs(getReprocessingTs()) .build()) .build(); log.trace("Submitting for reprocessing: {}", msg); - producer.send(submitTpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); // reprocessing topic has single partition, so we don't care about the msg key + submit(UUID.randomUUID(), msg); // reprocessing topic has single partition, so we don't care about the msg key + + if (task.getAttempt() >= maxReprocessingAttempts) { + log.warn("Failed to process task in {} attempts: {}", task.getAttempt(), msg); + } + } + + private void submit(UUID key, ToHousekeeperServiceMsg msg) { + producer.send(submitTpi, new TbProtoQueueMsg<>(key, msg), null); + } + + private long getReprocessingTs() { + return System.currentTimeMillis() + TimeUnit.SECONDS.toMillis((long) (reprocessingDelay * 0.8)); // *0.8 so that msgs submitted just after finishing reprocessing are processed on the next cycle } @PreDestroy diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index a0564d3bcb..ef3ce8c786 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.LifecycleEvent; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -81,6 +83,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -90,7 +93,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. "transport.http.enabled=true", "queue.core.housekeeper.reprocessing-start-delay-sec=1", "queue.core.housekeeper.task-reprocessing-delay-sec=2", - "queue.core.housekeeper.poll-interval-ms=1000" + "queue.core.housekeeper.poll-interval-ms=1000", + "queue.core.housekeeper.max-reprocessing-attempts=5" }) public class HousekeeperServiceTest extends AbstractControllerTest { @@ -170,7 +174,8 @@ public class HousekeeperServiceTest extends AbstractControllerTest { .severity(AlarmSeverity.MAJOR) .build(); alarm = doPost("/api/alarm", alarm, Alarm.class); - alarm = doPost("/api/alarm/" + alarm.getId() + "/assign/" + userId, "", Alarm.class); + AlarmId alarmId = alarm.getId(); + alarm = doPost("/api/alarm/" + alarmId + "/assign/" + userId, "", Alarm.class); assertThat(alarm.getAssigneeId()).isEqualTo(userId); assertThat(alarmService.findAlarmIdsByAssigneeId(tenantId, userId, new PageLink(100)).getData()).isNotEmpty(); @@ -178,8 +183,8 @@ public class HousekeeperServiceTest extends AbstractControllerTest { await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { verifyNoRelatedData(userId); + assertThat(alarmService.findAlarmById(tenantId, alarmId).getAssigneeId()).isNull(); }); - assertThat(alarmService.findAlarmById(tenantId, alarm.getId()).getAssigneeId()).isNull(); } @Test @@ -228,15 +233,12 @@ public class HousekeeperServiceTest extends AbstractControllerTest { int attempts = 3; await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { - verify(housekeeperService).processTask(argThat(verifyTaskSubmission(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, - task -> task.getErrorsCount() == 0))); - + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, 0); for (int i = 1; i <= attempts; i++) { int attempt = i; - verify(housekeeperReprocessingService).submitForReprocessing(argThat(verifyTaskSubmission(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, + verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, task -> task.getErrorsCount() > 0 && task.getAttempt() == attempt)), argThat(e -> e.getMessage().equals(error.getMessage()))); - verify(housekeeperService).processTask(argThat(verifyTaskSubmission(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, - task -> task.getErrorsCount() > 0 && task.getAttempt() == attempt))); + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, attempt); } }); @@ -247,11 +249,43 @@ public class HousekeeperServiceTest extends AbstractControllerTest { }); } - private ArgumentMatcher verifyTaskSubmission(EntityId entityId, HousekeeperTaskType taskType, - Predicate additionalCheck) { + @Test + public void whenReprocessingAttemptsExceeded_thenReprocessOnNextStartUp() throws Exception { + TimeoutException error = new TimeoutException("Test timeout"); + doThrow(error).when(telemetryDeletionTaskProcessor).process(any()); + + Device device = createDevice("woeifjiowejf", "woeifjiowejf"); + createRelatedData(device.getId()); + + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + + int maxAttempts = 5; + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + for (int i = 1; i <= maxAttempts; i++) { + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, i); + } + }); + + Mockito.clearInvocations(housekeeperService); + doCallRealMethod().when(telemetryDeletionTaskProcessor).process(any()); + TimeUnit.SECONDS.sleep(2); + verify(housekeeperService, never()).processTask(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, null))); + + housekeeperReprocessingService.cycle.set(0); // imitating start-up + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, 6); + }); + } + + private void verifyTaskProcessing(EntityId entityId, HousekeeperTaskType taskType, int expectedAttempt) throws Exception { + verify(housekeeperService).processTask(argThat(getTaskMatcher(entityId, taskType, task -> task.getAttempt() == expectedAttempt))); + } + + private ArgumentMatcher getTaskMatcher(EntityId entityId, HousekeeperTaskType taskType, + Predicate additionalCheck) { return msg -> { HousekeeperTask task = JacksonUtil.fromString(msg.getTask().getValue(), HousekeeperTask.class); - return task.getEntityId().equals(entityId) && task.getTaskType() == taskType && additionalCheck.test(msg.getTask()); + return task.getEntityId().equals(entityId) && task.getTaskType() == taskType && (additionalCheck == null || additionalCheck.test(msg.getTask())); }; } From 22f5573e8d9360692847a8589be1b3c50dd4f030 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 13 Mar 2024 19:59:13 +0200 Subject: [PATCH 018/147] Housekeeper client for all service types --- .../HousekeeperReprocessingService.java | 18 +++--- ...erService.java => HousekeeperService.java} | 44 ++++---------- .../stats/HousekeeperStatsService.java | 8 ++- .../src/main/resources/thingsboard.yml | 23 ++++++-- .../housekeeper/HousekeeperServiceTest.java | 7 ++- .../msg/housekeeper/HousekeeperClient.java | 4 +- .../housekeeper/DefaultHousekeeperClient.java | 58 +++++++++++++++++++ .../provider/AwsSqsMonolithQueueFactory.java | 10 ++-- .../provider/AwsSqsTbCoreQueueFactory.java | 10 ++-- .../AwsSqsTbRuleEngineQueueFactory.java | 6 +- .../AwsSqsTbVersionControlQueueFactory.java | 5 ++ .../provider/AwsSqsTransportQueueFactory.java | 5 ++ .../HousekeeperClientQueueFactory.java | 26 +++++++++ .../InMemoryTbTransportQueueFactory.java | 5 ++ .../KafkaTbRuleEngineQueueFactory.java | 12 ++++ .../KafkaTbTransportQueueFactory.java | 12 ++++ .../KafkaTbVersionControlQueueFactory.java | 13 +++++ .../provider/PubSubMonolithQueueFactory.java | 12 ++-- .../provider/PubSubTbCoreQueueFactory.java | 12 ++-- .../PubSubTbRuleEngineQueueFactory.java | 7 ++- .../PubSubTbVersionControlQueueFactory.java | 5 ++ .../provider/PubSubTransportQueueFactory.java | 5 ++ .../RabbitMqMonolithQueueFactory.java | 12 ++-- .../provider/RabbitMqTbCoreQueueFactory.java | 12 ++-- .../RabbitMqTbRuleEngineQueueFactory.java | 5 ++ .../RabbitMqTbVersionControlQueueFactory.java | 5 ++ .../RabbitMqTransportQueueFactory.java | 5 ++ .../ServiceBusMonolithQueueFactory.java | 12 ++-- .../ServiceBusTbCoreQueueFactory.java | 12 ++-- .../ServiceBusTbRuleEngineQueueFactory.java | 7 ++- ...erviceBusTbVersionControlQueueFactory.java | 5 ++ .../ServiceBusTransportQueueFactory.java | 5 ++ .../queue/provider/TbCoreQueueFactory.java | 4 +- .../provider/TbCoreQueueProducerProvider.java | 7 --- .../provider/TbQueueProducerProvider.java | 2 - .../TbRuleEngineProducerProvider.java | 12 ++-- .../provider/TbRuleEngineQueueFactory.java | 2 +- .../provider/TbTransportQueueFactory.java | 2 +- .../TbTransportQueueProducerProvider.java | 11 ++-- .../TbVersionControlProducerProvider.java | 13 ++--- .../TbVersionControlQueueFactory.java | 2 +- .../dao/housekeeper/CleanUpService.java | 39 ++++--------- .../dao/service/AbstractServiceTest.java | 18 ++++++ 43 files changed, 340 insertions(+), 159 deletions(-) rename application/src/main/java/org/thingsboard/server/service/housekeeper/{DefaultHousekeeperService.java => HousekeeperService.java} (78%) rename dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java => common/message/src/main/java/org/thingsboard/server/common/msg/housekeeper/HousekeeperClient.java (88%) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/provider/HousekeeperClientQueueFactory.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 5c577dd39f..8b1dece58e 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -32,7 +32,6 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import javax.annotation.PostConstruct; @@ -53,11 +52,11 @@ import java.util.concurrent.atomic.AtomicInteger; @ConditionalOnProperty(name = "queue.core.housekeeper.enabled", havingValue = "true", matchIfMissing = true) public class HousekeeperReprocessingService { - private final DefaultHousekeeperService housekeeperService; + private final HousekeeperService housekeeperService; private final PartitionService partitionService; private final TbCoreQueueFactory queueFactory; - private final TbQueueProducer> producer; - private final TopicPartitionInfo submitTpi; + private TbQueueProducer> producer; + private TopicPartitionInfo submitTpi; @Value("${queue.core.housekeeper.reprocessing-start-delay-sec:300}") private int startDelay; @@ -74,18 +73,19 @@ public class HousekeeperReprocessingService { protected AtomicInteger cycle = new AtomicInteger(); private boolean stopped; - public HousekeeperReprocessingService(@Lazy DefaultHousekeeperService housekeeperService, - PartitionService partitionService, TbCoreQueueFactory queueFactory, - TbQueueProducerProvider producerProvider) { + public HousekeeperReprocessingService(@Lazy HousekeeperService housekeeperService, + PartitionService partitionService, + TbCoreQueueFactory queueFactory) { this.housekeeperService = housekeeperService; this.partitionService = partitionService; this.queueFactory = queueFactory; - this.producer = producerProvider.getHousekeeperReprocessingMsgProducer(); - this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); } @PostConstruct private void init() { + producer = queueFactory.createHousekeeperReprocessingMsgProducer(); + submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); + scheduler.scheduleWithFixedDelay(() -> { try { cycle.incrementAndGet(); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java similarity index 78% rename from application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java rename to application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 589c570144..4c0c0e72d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/DefaultHousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -26,15 +26,11 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.notification.rule.trigger.TaskProcessingFailureTrigger; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.housekeeper.HousekeeperService; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; @@ -43,6 +39,7 @@ import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; import javax.annotation.PreDestroy; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -55,16 +52,14 @@ import java.util.stream.Collectors; @Service @Slf4j @ConditionalOnProperty(name = "queue.core.housekeeper.enabled", havingValue = "true", matchIfMissing = true) -public class DefaultHousekeeperService implements HousekeeperService { +public class HousekeeperService { private final Map> taskProcessors; private final HousekeeperReprocessingService reprocessingService; - private final HousekeeperStatsService statsService; + private final Optional statsService; private final NotificationRuleProcessor notificationRuleProcessor; private final TbQueueConsumer> consumer; - private final TbQueueProducer> producer; - private final TopicPartitionInfo submitTpi; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); private final ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); @@ -76,18 +71,15 @@ public class DefaultHousekeeperService implements HousekeeperService { private boolean stopped; - public DefaultHousekeeperService(HousekeeperReprocessingService reprocessingService, - TbCoreQueueFactory queueFactory, - TbQueueProducerProvider producerProvider, - HousekeeperStatsService statsService, - NotificationRuleProcessor notificationRuleProcessor, - @Lazy List> taskProcessors) { + public HousekeeperService(HousekeeperReprocessingService reprocessingService, + TbCoreQueueFactory queueFactory, + Optional statsService, + NotificationRuleProcessor notificationRuleProcessor, + @Lazy List> taskProcessors) { this.reprocessingService = reprocessingService; this.statsService = statsService; this.notificationRuleProcessor = notificationRuleProcessor; this.consumer = queueFactory.createHousekeeperMsgConsumer(); - this.producer = producerProvider.getHousekeeperMsgProducer(); - this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); this.taskProcessors = taskProcessors.stream().collect(Collectors.toMap(HousekeeperTaskProcessor::getTaskType, p -> p)); } @@ -147,7 +139,7 @@ public class DefaultHousekeeperService implements HousekeeperService { }); future.get(taskProcessingTimeout, TimeUnit.MILLISECONDS); - statsService.reportProcessed(task.getTaskType(), msg); + statsService.ifPresent(statsService -> statsService.reportProcessed(task.getTaskType(), msg)); log.debug("[{}] Successfully {} task {}", task.getTenantId(), isNew(msg.getTask()) ? "processed" : "reprocessed", msg.getTask().getValue()); } catch (InterruptedException e) { throw e; @@ -163,7 +155,7 @@ public class DefaultHousekeeperService implements HousekeeperService { task.getTaskType(), msg.getTask().getAttempt(), task, error); reprocessingService.submitForReprocessing(msg, error); - statsService.reportFailure(task.getTaskType(), msg); + statsService.ifPresent(statsService -> statsService.reportFailure(task.getTaskType(), msg)); notificationRuleProcessor.process(TaskProcessingFailureTrigger.builder() .task(task) .error(error) @@ -172,22 +164,6 @@ public class DefaultHousekeeperService implements HousekeeperService { } } - @Override - public void submitTask(HousekeeperTask task) { - log.trace("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); - /* - * using msg key as entity id so that msgs related to certain entity are pushed to same partition, - * e.g. on tenant deletion (entity id is tenant id), we need to clean up tenant entities in certain order - * */ - producer.send(submitTpi, new TbProtoQueueMsg<>(task.getEntityId().getId(), ToHousekeeperServiceMsg.newBuilder() - .setTask(HousekeeperTaskProto.newBuilder() - .setValue(JacksonUtil.toString(task)) - .setTs(task.getTs()) - .setAttempt(0) - .build()) - .build()), null); - } - private boolean isNew(HousekeeperTaskProto task) { return task.getErrorsCount() == 0; } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java index 3f6e88a299..400bc36479 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -17,13 +17,14 @@ package org.thingsboard.server.service.housekeeper.stats; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; -import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import java.util.ArrayList; @@ -31,11 +32,11 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Service @Slf4j +@ConditionalOnProperty(name = "queue.core.housekeeper.stats.enabled", havingValue = "true", matchIfMissing = true) public class HousekeeperStatsService { private final Map stats = new EnumMap<>(HousekeeperTaskType.class); @@ -46,7 +47,8 @@ public class HousekeeperStatsService { } } - @Scheduled(initialDelay = 60, fixedDelay = 60, timeUnit = TimeUnit.SECONDS) + @Scheduled(initialDelayString = "${queue.core.housekeeper.stats.print-interval-ms:60000}", + fixedDelayString = "${queue.core.housekeeper.stats.print-interval-ms:60000}") private void reportStats() { String statsStr = stats.values().stream().map(stats -> { String countersStr = stats.getCounters().stream() diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a156762c71..5baa155ae9 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1401,9 +1401,11 @@ queue: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" tb_housekeeper: + # Amount of records to be returned in a single poll. For Housekeeper tasks topic, we should consume messages (tasks) one by one - key: max.poll.records value: "1" tb_housekeeper.reprocessing: + # Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one - key: max.poll.records value: "1" other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" @@ -1428,9 +1430,9 @@ queue: # Kafka properties for Version Control topic version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1" - # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days - housekeeper-reprocessing: "retention.ms:7776000000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time + housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1587,14 +1589,27 @@ queue: # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" housekeeper: + # Enabled/disable Housekeeping service, which does cleanup of telemetry, attributes, events, etc. enabled: "${TB_HOUSEKEEPER_ENABLED:true}" + # Topic name for Housekeeper tasks topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" + # Topic name for Housekeeper tasks to be reprocessed reprocessing-topic: "${TB_HOUSEKEEPER_REPROCESSING_TOPIC:tb_housekeeper.reprocessing}" + # Poll interval for topics related to Housekeeper poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" + # Timeout in milliseconds for task processing. Tasks that fail to finish on time will be submitted for reprocessing task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" + # Delay in seconds for starting tasks reprocessing after start-up reprocessing-start-delay-sec: "${TB_HOUSEKEEPER_REPROCESSING_START_DELAY_SEC:300}" + # Period in seconds of tasks reprocessing task-reprocessing-delay-sec: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_SEC:3600}" - max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:30}" + # Maximum amount of task reprocessing attempts. After exceeding, the task will be ignored until the next service start-up + max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:10}" + stats: + # Enable/disable statistics for Housekeeper + enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" + # Statistics printing interval for Housekeeper + print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" vc: # Default topic name for Kafka, RabbitMQ, etc. diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index ef3ce8c786..daf363467d 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -99,7 +100,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class HousekeeperServiceTest extends AbstractControllerTest { @SpyBean - private DefaultHousekeeperService housekeeperService; + private HousekeeperService housekeeperService; + @SpyBean + private HousekeeperClient housekeeperClient; @SpyBean private HousekeeperReprocessingService housekeeperReprocessingService; @Autowired @@ -300,7 +303,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private void verifyNoRelatedData(EntityId entityId) throws Exception { List expectedTaskTypes = List.of(HousekeeperTaskType.DELETE_TELEMETRY, HousekeeperTaskType.DELETE_ATTRIBUTES, HousekeeperTaskType.DELETE_EVENTS, HousekeeperTaskType.DELETE_ENTITY_ALARMS); for (HousekeeperTaskType taskType : expectedTaskTypes) { - verify(housekeeperService).submitTask(argThat(task -> task.getTaskType() == taskType && task.getEntityId().equals(entityId))); + verify(housekeeperClient).submitTask(argThat(task -> task.getTaskType() == taskType && task.getEntityId().equals(entityId))); } assertThat(getLatestTelemetry(entityId)).isNull(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java b/common/message/src/main/java/org/thingsboard/server/common/msg/housekeeper/HousekeeperClient.java similarity index 88% rename from dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/housekeeper/HousekeeperClient.java index e3c0800f69..e2a3af9242 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/HousekeeperService.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/housekeeper/HousekeeperClient.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.housekeeper; +package org.thingsboard.server.common.msg.housekeeper; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; -public interface HousekeeperService { +public interface HousekeeperClient { void submitTask(HousekeeperTask task); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java new file mode 100644 index 0000000000..b766f3826b --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java @@ -0,0 +1,58 @@ +/** + * 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.queue.housekeeper; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; + +@Service +@Slf4j +public class DefaultHousekeeperClient implements HousekeeperClient { + + private final TbQueueProducer> producer; + private final TopicPartitionInfo submitTpi; + + public DefaultHousekeeperClient(TbQueueProducerProvider producerProvider) { + this.producer = producerProvider.getHousekeeperMsgProducer(); + this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); + } + + @Override + public void submitTask(HousekeeperTask task) { + log.trace("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); + /* + * using msg key as entity id so that msgs related to certain entity are pushed to same partition, + * e.g. on tenant deletion (entity id is tenant id), we need to clean up tenant entities in certain order + * */ + producer.send(submitTpi, new TbProtoQueueMsg<>(task.getEntityId().getId(), ToHousekeeperServiceMsg.newBuilder() + .setTask(TransportProtos.HousekeeperTaskProto.newBuilder() + .setValue(JacksonUtil.toString(task)) + .setTs(task.getTs()) + .setAttempt(0) + .build()) + .build()), null); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index 1217186410..e60594c4a1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -229,22 +229,24 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index 1a4f92fcd3..eb3706657b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -208,22 +208,24 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index 6d48ceb1b6..1653955ae9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -33,8 +33,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; @@ -159,6 +159,10 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory return new TbAwsSqsProducerTemplate<>(otaAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } @PreDestroy private void destroy() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java index 3aec2e3a39..6801703231 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java @@ -80,6 +80,11 @@ public class AwsSqsTbVersionControlQueueFactory implements TbVersionControlQueue ); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java index c27ad4e092..96a9686d13 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java @@ -131,6 +131,11 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/HousekeeperClientQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/HousekeeperClientQueueFactory.java new file mode 100644 index 0000000000..79c38a04e9 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/HousekeeperClientQueueFactory.java @@ -0,0 +1,26 @@ +/** + * 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.queue.provider; + +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; + +public interface HousekeeperClientQueueFactory { + + TbQueueProducer> createHousekeeperMsgProducer(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index f08a822466..4ee79aef60 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -120,4 +120,9 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 63798f95a1..869f364970 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -72,6 +72,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin jsExecutorResponseAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin fwUpdatesAdmin; + private final TbQueueAdmin housekeeperAdmin; private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -97,6 +98,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.jsExecutorResponseAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getJsExecutorResponseConfigs()); this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); + this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); } @Override @@ -230,6 +232,16 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return requestBuilder.build(); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("tb-rule-engine-housekeeper-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .admin(housekeeperAdmin) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java index 7cecc94290..4140925775 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java @@ -66,6 +66,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { private final TbQueueAdmin transportApiRequestAdmin; private final TbQueueAdmin transportApiResponseAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin housekeeperAdmin; public KafkaTbTransportQueueFactory(TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, @@ -90,6 +91,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { this.transportApiRequestAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTransportApiRequestConfigs()); this.transportApiResponseAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getTransportApiResponseConfigs()); this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); + this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); } @Override @@ -173,6 +175,16 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { return requestBuilder.build(); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("tb-transport-housekeeper-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .admin(housekeeperAdmin) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java index e39da6bdfc..95086e04b4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.provider; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; @@ -51,6 +52,7 @@ public class KafkaTbVersionControlQueueFactory implements TbVersionControlQueueF private final TbQueueAdmin coreAdmin; private final TbQueueAdmin vcAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin housekeeperAdmin; public KafkaTbVersionControlQueueFactory(TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, @@ -69,6 +71,7 @@ public class KafkaTbVersionControlQueueFactory implements TbVersionControlQueueF this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); + this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); } @@ -105,6 +108,16 @@ public class KafkaTbVersionControlQueueFactory implements TbVersionControlQueueF return requestBuilder.build(); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("tb-vc-housekeeper-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) + .admin(housekeeperAdmin) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index e6a23b8068..0f2485b6a7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -41,8 +41,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; import org.thingsboard.server.queue.pubsub.TbPubSubConsumerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; @@ -230,22 +230,24 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 1b52a2ba42..00a6d83bdd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -39,8 +39,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; import org.thingsboard.server.queue.pubsub.TbPubSubConsumerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; @@ -201,22 +201,24 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index 070384e60e..d0192d3094 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -36,8 +36,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; import org.thingsboard.server.queue.pubsub.TbPubSubConsumerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; @@ -161,6 +161,11 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java index ed40a43deb..509cf371b8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java @@ -79,6 +79,11 @@ public class PubSubTbVersionControlQueueFactory implements TbVersionControlQueue ); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java index 3323308bef..d91dd91873 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java @@ -131,6 +131,11 @@ public class PubSubTransportQueueFactory implements TbTransportQueueFactory { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index fa89956b64..78b60bca28 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -41,8 +41,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqAdmin; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqConsumerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; @@ -227,22 +227,24 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index b1f3de672f..db3414b62d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -38,8 +38,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqAdmin; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqConsumerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; @@ -200,22 +200,24 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index 929ea8298c..556aa968c6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -160,6 +160,11 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java index bd14db38b4..a614df3fdb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java @@ -79,6 +79,11 @@ public class RabbitMqTbVersionControlQueueFactory implements TbVersionControlQue ); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java index d5fb7bd0e2..389a6d11ee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java @@ -132,6 +132,11 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 5f4be98ca0..a1f1c7adc8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -45,8 +45,8 @@ import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; @@ -226,22 +226,24 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index 579fba272c..854295cb6b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -44,8 +44,8 @@ import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; @@ -201,22 +201,24 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createHousekeeperMsgProducer() { - return null; + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override public TbQueueConsumer> createHousekeeperMsgConsumer() { - return null; + return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return null; + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return null; + return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index b09ab1d30d..066e84e780 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -41,8 +41,8 @@ import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; @@ -160,6 +160,11 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java index 2e5e5c4bc3..bed6c427c9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java @@ -79,6 +79,11 @@ public class ServiceBusTbVersionControlQueueFactory implements TbVersionControlQ ); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java index d782a1ff9e..0db62ac90c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java @@ -133,6 +133,11 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } + @Override + public TbQueueProducer> createHousekeeperMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 866eb931f2..20a67c2e09 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory { +public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory { /** * Used to push messages to instances of TB Transport Service @@ -132,8 +132,6 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory { */ TbQueueProducer> createVersionControlMsgProducer(); - TbQueueProducer> createHousekeeperMsgProducer(); - TbQueueConsumer> createHousekeeperMsgConsumer(); TbQueueProducer> createHousekeeperReprocessingMsgProducer(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index e6bc8f0484..aa2c55fca1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -43,7 +43,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; private TbQueueProducer> toHousekeeper; - private TbQueueProducer> toHousekeeperReprocessing; public TbCoreQueueProducerProvider(TbCoreQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -59,7 +58,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toVersionControl = tbQueueProvider.createVersionControlMsgProducer(); this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); - this.toHousekeeperReprocessing = tbQueueProvider.createHousekeeperReprocessingMsgProducer(); } @Override @@ -102,9 +100,4 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toHousekeeper; } - @Override - public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { - return toHousekeeperReprocessing; - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index ff56d2e8be..a752301cd8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -82,6 +82,4 @@ public interface TbQueueProducerProvider { TbQueueProducer> getHousekeeperMsgProducer(); - TbQueueProducer> getHousekeeperReprocessingMsgProducer(); - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index ee6700906c..748c48e52a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -40,6 +41,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toRuleEngineNotifications; private TbQueueProducer> toTbCoreNotifications; private TbQueueProducer> toUsageStats; + private TbQueueProducer> toHousekeeper; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -53,6 +55,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toRuleEngineNotifications = tbQueueProvider.createRuleEngineNotificationsMsgProducer(); this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); + this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); } @Override @@ -91,13 +94,8 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getHousekeeperMsgProducer() { - return null; // fixme - } - - @Override - public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Rule Engine!"); + public TbQueueProducer> getHousekeeperMsgProducer() { + return toHousekeeper; } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index cd4575af6a..f23c7e47f3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -32,7 +32,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory { +public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java index 909089f175..c1a0a0fd24 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java @@ -26,7 +26,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -public interface TbTransportQueueFactory extends TbUsageStatsClientQueueFactory { +public interface TbTransportQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory { TbQueueRequestTemplate, TbProtoQueueMsg> createTransportApiRequestTemplate(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index 869c588fc7..c0472ba6f0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -38,6 +39,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider private TbQueueProducer> toTbCore; private TbQueueProducer> toTbCoreNotifications; private TbQueueProducer> toUsageStats; + private TbQueueProducer> toHousekeeper; public TbTransportQueueProducerProvider(TbTransportQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -49,6 +51,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider this.toRuleEngine = tbQueueProvider.createRuleEngineMsgProducer(); this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); + this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); } @Override @@ -87,12 +90,8 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getHousekeeperMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Transport!"); + public TbQueueProducer> getHousekeeperMsgProducer() { + return toHousekeeper; } - @Override - public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Transport!"); - } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 3f4bd209e4..c4e8b697a3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -36,6 +37,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider private final TbVersionControlQueueFactory tbQueueProvider; private TbQueueProducer> toTbCoreNotifications; private TbQueueProducer> toUsageStats; + private TbQueueProducer> toHousekeeper; public TbVersionControlProducerProvider(TbVersionControlQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -45,6 +47,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider public void init() { this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); + this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); } @Override @@ -54,7 +57,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider @Override public TbQueueProducer> getRuleEngineMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); } @Override @@ -83,12 +86,8 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getHousekeeperMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + public TbQueueProducer> getHousekeeperMsgProducer() { + return toHousekeeper; } - @Override - public TbQueueProducer> getHousekeeperReprocessingMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); - } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlQueueFactory.java index 168a466015..3917df24cd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlQueueFactory.java @@ -25,7 +25,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Version Control Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbVersionControlQueueFactory extends TbUsageStatsClientQueueFactory { +public interface TbVersionControlQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory { /** * Used to push notifications to other instances of TB Core Service diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 015917e592..56946c4705 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -17,8 +17,6 @@ package org.thingsboard.server.dao.housekeeper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; @@ -26,23 +24,17 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.entity.EntityDaoService; -import org.thingsboard.server.dao.entity.EntityServiceRegistry; +import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.relation.RelationService; -import java.util.Optional; - @Component @RequiredArgsConstructor @Slf4j public class CleanUpService { - private final Optional housekeeperService; + private final HousekeeperClient housekeeperClient; private final RelationService relationService; - @Autowired - @Lazy - private EntityServiceRegistry entityServiceRegistry; @TransactionalEventListener(fallbackExecution = true) public void handleEntityDeletionEvent(DeleteEntityEvent event) { @@ -51,9 +43,7 @@ public class CleanUpService { log.trace("[{}][{}][{}] Handling entity deletion event", tenantId, entityId.getEntityType(), entityId.getId()); cleanUpRelatedData(tenantId, entityId); if (entityId.getEntityType() == EntityType.USER) { - housekeeperService.ifPresent(housekeeperService -> { - housekeeperService.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); - }); + housekeeperClient.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); } } @@ -61,25 +51,16 @@ public class CleanUpService { log.debug("[{}][{}][{}] Cleaning up related data", tenantId, entityId.getEntityType(), entityId.getId()); // todo: skipped entities list relationService.deleteEntityRelations(tenantId, entityId); - housekeeperService.ifPresent(housekeeperService -> { - housekeeperService.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); - housekeeperService.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); - }); + housekeeperClient.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); + housekeeperClient.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); + housekeeperClient.submitTask(HousekeeperTask.deleteEvents(tenantId, entityId)); + housekeeperClient.submitTask(HousekeeperTask.deleteEntityAlarms(tenantId, entityId)); } public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { - housekeeperService.ifPresentOrElse(housekeeperService -> { - for (EntityType entityType : entityTypes) { - housekeeperService.submitTask(HousekeeperTask.deleteEntities(tenantId, entityType)); - } - }, () -> { - for (EntityType entityType : entityTypes) { - EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); - entityService.deleteByTenantId(tenantId); - } - }); + for (EntityType entityType : entityTypes) { + housekeeperClient.submitTask(HousekeeperTask.deleteEntities(tenantId, entityType)); + } } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index e8232af692..f222c05b60 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; +import org.thingsboard.server.common.data.housekeeper.EntitiesDeletionHousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -51,6 +53,9 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.audit.AuditLogLevelFilter; import org.thingsboard.server.dao.audit.AuditLogLevelMask; import org.thingsboard.server.dao.audit.AuditLogLevelProperties; +import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; +import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.dao.tenant.TenantService; import java.io.IOException; @@ -77,6 +82,9 @@ public abstract class AbstractServiceTest { @Autowired protected TenantService tenantService; + @Autowired + protected EntityServiceRegistry entityServiceRegistry; + protected TenantId tenantId; @Before @@ -126,6 +134,16 @@ public abstract class AbstractServiceTest { return new AuditLogLevelFilter(props); } + @Bean + public HousekeeperClient housekeeperClient() { + return task -> { + if (task.getTaskType() == HousekeeperTaskType.DELETE_ENTITIES) { + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(((EntitiesDeletionHousekeeperTask) task).getEntityType()); + entityService.deleteByTenantId(task.getTenantId()); + } + }; + } + protected DeviceProfile createDeviceProfile(TenantId tenantId, String name) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); From a576a73b8629378fcbebe3b39379da9029b78e91 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 13 Mar 2024 20:08:17 +0200 Subject: [PATCH 019/147] Housekeeper topic configs for all ymls --- msa/vc-executor/src/main/resources/tb-vc-executor.yml | 5 +++++ transport/coap/src/main/resources/tb-coap-transport.yml | 5 +++++ transport/http/src/main/resources/tb-http-transport.yml | 5 +++++ transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml | 5 +++++ transport/mqtt/src/main/resources/tb-mqtt-transport.yml | 5 +++++ transport/snmp/src/main/resources/tb-snmp-transport.yml | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 289baaa97a..a92a7eea2b 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -137,6 +137,8 @@ queue: notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -250,6 +252,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" vc: # Default topic name for Kafka, RabbitMQ, etc. topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index faa5f43065..a758a64882 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -277,6 +277,8 @@ queue: transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use the default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" @@ -398,6 +400,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" js: # JS Eval request topic request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 49bf202200..4c8dcd0e45 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -261,6 +261,8 @@ queue: transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" @@ -383,6 +385,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" js: # JS Eval request topic request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index b6dc7248ab..669e42bc8a 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -358,6 +358,8 @@ queue: transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use the default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" @@ -477,6 +479,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" js: # JS Eval request topic request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 55223caf4e..c958ae2fd7 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -293,6 +293,8 @@ queue: transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use the default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" @@ -414,6 +416,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" js: # JS Eval request topic request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index a40374e1f6..ea9f2c9ae1 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -243,6 +243,8 @@ queue: transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -371,6 +373,9 @@ queue: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" js: # JS Eval request topic request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" From 85728c0e248eb8669720316fc8a3e1287077cdc7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 15 Mar 2024 17:26:31 +0200 Subject: [PATCH 020/147] added ownerName ENTITY_FIELD to EntityDataQuery --- .../controller/EntityQueryControllerTest.java | 104 ++++++++++++++++++ .../query/DefaultEntityQueryRepository.java | 30 +++-- .../dao/sql/query/EntityKeyMapping.java | 82 +++++++++----- 3 files changed, 182 insertions(+), 34 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 67ad2cbe55..edd2a372c1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -593,4 +594,107 @@ public class EntityQueryControllerTest extends AbstractControllerTest { assertThat(getErrorMessage(result)).contains("Invalid").contains("sort property"); } + @Test + public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { + loginTenantAdmin(); + int numOfDevices = 3; + + for (int i = 0; i < numOfDevices; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setType("default"); + + Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class); + JsonNode content = JacksonUtil.toJsonNode("{\"alarmActiveTime\": 1" + i + "}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + savedDevice.getUuidId() + "/SERVER_SCOPE", content) + .andExpect(status().isOk()); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter(""); + + KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); + KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); + KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); + KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "ownerName"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "ownerType")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "alarmActiveTime")); + + // all devices with ownerName = TEST TENANT + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); + checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT"); + + // all devices with wrong ownerName + EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); + checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null); + + // all devices with owner type = TENANT + EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); + checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT"); + + // all devices with owner type = CUSTOMER + EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); + checkEntitiesByQuery(customerEntitiesQuery, 0, null, null); + } + + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { + Awaitility.await() + .alias("data by query") + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); + var loadedEntities = new ArrayList<>(data.getData()); + return loadedEntities.size() == expectedNumOfDevices; + }); + if (expectedNumOfDevices == 0) { + return; + } + var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); + var loadedEntities = new ArrayList<>(data.getData()); + + Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); + + for (int i = 0; i < expectedNumOfDevices; i++) { + var entity = loadedEntities.get(i); + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue(); + String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue(); + String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue(); + + Assert.assertEquals("Device" + i, name); + Assert.assertEquals( expectedOwnerName, ownerName); + Assert.assertEquals( expectedOwnerType, ownerType); + Assert.assertEquals("1" + i, alarmActiveTime); + } + } + + private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) { + KeyFilter tenantOwnerNameFilter = new KeyFilter(); + tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); + StringFilterPredicate ownerNamePredicate = new StringFilterPredicate(); + ownerNamePredicate.setValue(FilterPredicateValue.fromString(value)); + ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL); + tenantOwnerNameFilter.setPredicate(ownerNamePredicate); + return tenantOwnerNameFilter; + } + + private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) { + KeyFilter numericFilter = new KeyFilter(); + numericFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, attribute)); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(value)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + numericFilter.setPredicate(predicate); + return numericFilter; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index d2b2567bd7..a7c11b70e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -357,7 +357,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) .collect(Collectors.toList()); - List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest()) + List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() != null) .collect(Collectors.toList()); List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) @@ -365,6 +365,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); + String aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, ""); String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); String entityTypeStr; @@ -385,7 +386,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { addEntityTableQuery(ctx, query.getEntityFilter()), entityWhereClause, latestJoinsCnt, - ""); + aliasWhereQuery); String countQuery = String.format("select count(id) %s", fromClauseCount); @@ -427,7 +428,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) .collect(Collectors.toList()); - List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest()) + List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() != null) .collect(Collectors.toList()); List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) @@ -437,7 +438,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false); - String textSearchQuery = DefaultEntityQueryRepository.this.buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); + String aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch()); String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); String entityTypeStr; if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { @@ -463,7 +464,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { addEntityTableQuery(ctx, query.getEntityFilter()), entityWhereClause, latestJoinsCnt, - textSearchQuery); + aliasWhereQuery); String fromClauseData = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s", topSelection, @@ -471,7 +472,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { addEntityTableQuery(ctx, query.getEntityFilter()), entityWhereClause, latestJoinsData, - textSearchQuery); + aliasWhereQuery); if (!StringUtils.isEmpty(pageLink.getTextSearch())) { //Unfortunately, we need to sacrifice performance in case of full text search, because it is applied to all joined records. @@ -798,6 +799,21 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return from; } + private String buildAliasWhereQuery(QueryContext ctx, EntityFilter entityFilter, List selectionMapping, String searchText) { + List aliasFiltersMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() == null) + .collect(Collectors.toList()); + String entityFieldsQuery = EntityKeyMapping.buildQuery(ctx, aliasFiltersMapping, entityFilter.getType()); + String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, searchText); + String result = ""; + if (!entityFieldsQuery.isEmpty()) { + result += " where (" + entityFieldsQuery + ")"; + } + if (!searchTextQuery.isEmpty()) { + result += (result.isEmpty() ? " where ": " and ") + "(" + searchTextQuery + ") "; + } + return result; + } + private String buildTextSearchQuery(QueryContext ctx, List selectionMapping, String searchText) { if (!StringUtils.isEmpty(searchText) && !selectionMapping.isEmpty()) { String sqlSearchText = "%" + searchText + "%"; @@ -809,7 +825,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } else { searchAliasesExpression = searchAliases.get(0); } - return String.format(" WHERE %s ILIKE :%s", searchAliasesExpression, "lowerSearchTextParam"); + return String.format(" %s ILIKE :%s", searchAliasesExpression, "lowerSearchTextParam"); } else { return ""; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index b928e8790e..0f51ef72b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -48,6 +49,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; +import static org.thingsboard.server.dao.sql.query.DefaultEntityQueryRepository.resolveEntityType; @Data public class EntityKeyMapping { @@ -55,6 +58,7 @@ public class EntityKeyMapping { private static final Map> allowedEntityFieldMap = new HashMap<>(); private static final Map entityFieldColumnMap = new HashMap<>(); private static final Map> aliases = new HashMap<>(); + private static final Map> propertiesFunctions = new EnumMap<>(EntityType.class); public static final String CREATED_TIME = "createdTime"; public static final String ENTITY_TYPE = "entityType"; @@ -75,6 +79,18 @@ public class EntityKeyMapping { public static final String PHONE = "phone"; public static final String ADDITIONAL_INFO = "additionalInfo"; public static final String RELATED_PARENT_ID = "parentId"; + public static final String OWNER_NAME = "ownerName"; + public static final String OWNER_TYPE = "ownerType"; + public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + + "THEN (select title from tenant where id = e.tenant_id) " + + "ELSE (select title from customer where id = e.customer_id) END"; + public static final String OWNER_TYPE_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + + "THEN 'TENANT' " + + "ELSE 'CUSTOMER' END"; + public static final Map ownerPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY + ); public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); public static final List widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME); @@ -140,6 +156,12 @@ public class EntityKeyMapping { aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); + propertiesFunctions.put(EntityType.DEVICE, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.ASSET, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.ENTITY_VIEW, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.USER, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.DASHBOARD, ownerPropertiesFunctions); + Map userEntityAliases = new HashMap<>(); userEntityAliases.put(TITLE, EMAIL); userEntityAliases.put(LABEL, EMAIL); @@ -150,6 +172,7 @@ public class EntityKeyMapping { private int index; private String alias; private boolean isLatest; + private String entityKeyColumn; private boolean isSelection; private boolean isSearchable; private boolean isSortOrder; @@ -179,12 +202,14 @@ public class EntityKeyMapping { if (entityKey.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)) { return String.format("'%s' as %s", entityType.name(), getValueAlias()); } else { - Set existingEntityFields = getExistingEntityFields(filterType, entityType); - String alias = getEntityFieldAlias(filterType, entityType); - if (existingEntityFields.contains(alias)) { - String column = entityFieldColumnMap.get(alias); - return String.format("cast(e.%s as varchar) as %s", column, getValueAlias()); + if (getEntityKeyColumn() != null) { + return String.format("cast(e.%s as varchar) as %s", getEntityKeyColumn(), getValueAlias()); } else { + Map entityPropertiesFunctions = propertiesFunctions.get(entityType); + String entityFieldAlias = getEntityFieldAlias(filterType, entityType); + if (entityPropertiesFunctions != null && entityPropertiesFunctions.containsKey(entityFieldAlias)) { + return String.format("%s as %s", entityPropertiesFunctions.get(entityFieldAlias), getValueAlias()); + } return String.format("'' as %s", getValueAlias()); } } @@ -234,7 +259,7 @@ public class EntityKeyMapping { public Stream toQueries(QueryContext ctx, EntityFilterType filterType) { if (hasFilter()) { - String keyAlias = entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) ? "e" : alias; + String keyAlias = (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) ? "e" : alias; return keyFilters.stream().map(keyFilter -> this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType)); } else { @@ -315,6 +340,9 @@ public class EntityKeyMapping { } public static List prepareKeyMapping(EntityDataQuery query) { + EntityType entityType = resolveEntityType(query.getEntityFilter()); + EntityFilterType entityFilterType = query.getEntityFilter().getType(); + List entityFields = query.getEntityFields() != null ? query.getEntityFields() : Collections.emptyList(); List latestValues = query.getLatestValues() != null ? query.getLatestValues() : Collections.emptyList(); Map> filters = @@ -330,6 +358,7 @@ public class EntityKeyMapping { mapping.setSelection(true); mapping.setSearchable(!key.getKey().equals(ADDITIONAL_INFO)); mapping.setEntityKey(key); + mapping.setEntityKeyColumn(entityType, entityFilterType); return mapping; } ).collect(Collectors.toList()); @@ -361,6 +390,7 @@ public class EntityKeyMapping { sortOrderMapping.setEntityKey(sortOrderKey); sortOrderMapping.setSortOrder(true); sortOrderMapping.setIgnore(true); + sortOrderMapping.setEntityKeyColumn(entityType, entityFilterType); if (sortOrderKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { entityFieldsMappings.add(sortOrderMapping); } else { @@ -398,6 +428,14 @@ public class EntityKeyMapping { return mappings; } + private void setEntityKeyColumn(EntityType entityType, EntityFilterType entityFilterType) { + Set existingEntityFields = getExistingEntityFields(entityFilterType, entityType); + String entityFieldAlias = getEntityFieldAlias(entityFilterType, entityType); + if (existingEntityFields.contains(entityFieldAlias)) { + entityKeyColumn = entityFieldColumnMap.get(entityFieldAlias); + } + } + public static List prepareEntityCountKeyMapping(EntityCountQuery query) { Map> filters = query.getKeyFilters() != null ? @@ -489,30 +527,20 @@ public class EntityKeyMapping { private String buildSimplePredicateQuery(QueryContext ctx, String alias, EntityKey key, KeyFilterPredicate predicate, EntityFilterType filterType) { if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { - Set existingEntityFields = getExistingEntityFields(filterType, ctx.getEntityType()); - String entityFieldAlias = getEntityFieldAlias(filterType, ctx.getEntityType()); - String column = null; - if (existingEntityFields.contains(entityFieldAlias)) { - column = entityFieldColumnMap.get(entityFieldAlias); - } - if (column != null) { - String field = alias + "." + column; - if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { - return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); - } else if (predicate.getType().equals(FilterPredicateType.STRING)) { - if (key.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)) { - field = ctx.getEntityType().toString(); - return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate) - .replace("lower(" + field, "lower('" + field + "'") - .replace(field + " ", "'" + field + "' "); - } else { - return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); - } + String field = (getEntityKeyColumn() != null) ? alias + "." + getEntityKeyColumn() : alias; + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { + return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); + } else if (predicate.getType().equals(FilterPredicateType.STRING)) { + if (key.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)) { + field = ctx.getEntityType().toString(); + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate) + .replace("lower(" + field, "lower('" + field + "'") + .replace(field + " ", "'" + field + "' "); } else { - return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate); + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); } } else { - return null; + return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate); } } else { if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { From fc865252a8d2da6e62a50ca36164cf4bc7d28575 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Sat, 16 Mar 2024 15:41:48 +0200 Subject: [PATCH 021/147] Use Autowired in TenantServiceImpl --- .../validator/AssetProfileDataValidator.java | 1 - .../validator/DashboardDataValidator.java | 2 -- .../validator/DeviceDataValidator.java | 2 -- .../validator/DeviceProfileDataValidator.java | 1 - .../validator/ResourceDataValidator.java | 1 - .../validator/RuleChainDataValidator.java | 1 - .../server/dao/tenant/TenantServiceImpl.java | 36 +++++++++++++------ .../usagerecord/ApiUsageStateServiceImpl.java | 2 +- 8 files changed, 26 insertions(+), 20 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java index 7543c247e0..19a04fd867 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java @@ -42,7 +42,6 @@ public class AssetProfileDataValidator extends DataValidator { @Lazy private AssetProfileService assetProfileService; @Autowired - @Lazy private TenantService tenantService; @Lazy @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java index 829da4d5c7..178905f7f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; @@ -29,7 +28,6 @@ import org.thingsboard.server.dao.tenant.TenantService; public class DashboardDataValidator extends DataValidator { @Autowired - @Lazy private TenantService tenantService; @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java index a8f1bdd972..e2047dc448 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; @@ -40,7 +39,6 @@ public class DeviceDataValidator extends AbstractHasOtaPackageValidator private DeviceDao deviceDao; @Autowired - @Lazy private TenantService tenantService; @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index a06fd5edaa..23f3a6bbdb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -86,7 +86,6 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { private WidgetTypeDao widgetTypeDao; @Autowired - @Lazy private TenantService tenantService; @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 2f758b1b9c..222aaa3eeb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -53,7 +53,6 @@ public class RuleChainDataValidator extends DataValidator { private RuleChainService ruleChainService; @Autowired - @Lazy private TenantService tenantService; @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 6ab9361b19..181989a620 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -18,6 +18,8 @@ package org.thingsboard.server.dao.tenant; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; @@ -38,9 +40,9 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.notification.NotificationSettingsService; -import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.service.validator.TenantDataValidator; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; @@ -58,16 +60,28 @@ public class TenantServiceImpl extends AbstractCachedEntityService tenantValidator; - private final TbTransactionalCache existsTenantCache; + @Autowired + private TenantDao tenantDao; + @Autowired + private TenantProfileService tenantProfileService; + @Autowired + @Lazy + private UserService userService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired + private DeviceProfileService deviceProfileService; + @Lazy + @Autowired + private ApiUsageStateService apiUsageStateService; + @Autowired + private AdminSettingsService adminSettingsService; + @Autowired + private NotificationSettingsService notificationSettingsService; + @Autowired + private TenantDataValidator tenantValidator; + @Autowired + protected TbTransactionalCache existsTenantCache; @TransactionalEventListener(classes = TenantEvictEvent.class) @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index 0b880b7c5f..1019481d67 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -59,7 +59,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A private final DataValidator apiUsageStateValidator; public ApiUsageStateServiceImpl(ApiUsageStateDao apiUsageStateDao, TenantProfileDao tenantProfileDao, - @Lazy TenantService tenantService, @Lazy TimeseriesService tsService, + TenantService tenantService, @Lazy TimeseriesService tsService, DataValidator apiUsageStateValidator) { this.apiUsageStateDao = apiUsageStateDao; this.tenantProfileDao = tenantProfileDao; From b0d4faf798c2c3ab8de13ee78307095fc24c5373 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Sat, 16 Mar 2024 15:49:40 +0200 Subject: [PATCH 022/147] Fix transactional calls in TransactionalEventListener --- .../src/main/resources/thingsboard.yml | 10 +++--- .../housekeeper/DefaultHousekeeperClient.java | 36 ++++++++++++++----- .../provider/KafkaTbCoreQueueFactory.java | 24 ++++++------- .../KafkaTbRuleEngineQueueFactory.java | 5 +-- .../KafkaTbTransportQueueFactory.java | 5 +-- .../KafkaTbVersionControlQueueFactory.java | 5 +-- .../PubSubTbRuleEngineQueueFactory.java | 6 ++-- .../provider/PubSubTransportQueueFactory.java | 3 +- .../dao/housekeeper/CleanUpService.java | 18 +++++++--- 9 files changed, 73 insertions(+), 39 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 5baa155ae9..651d2154de 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1401,13 +1401,15 @@ queue: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" tb_housekeeper: - # Amount of records to be returned in a single poll. For Housekeeper tasks topic, we should consume messages (tasks) one by one + # Consumer properties for Housekeeper tasks topic - key: max.poll.records - value: "1" + # Amount of records to be returned in a single poll. For Housekeeper tasks topic, we should consume messages (tasks) one by one + value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_MAX_POLL_RECORDS:1}" tb_housekeeper.reprocessing: - # Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one + # Consumer properties for Housekeeper reprocessing topic - key: max.poll.records - value: "1" + # Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one + value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_MAX_POLL_RECORDS:1}" other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java index b766f3826b..e9737bf90c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java @@ -19,10 +19,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @@ -33,26 +35,42 @@ public class DefaultHousekeeperClient implements HousekeeperClient { private final TbQueueProducer> producer; private final TopicPartitionInfo submitTpi; + private final TbQueueCallback submitCallback; public DefaultHousekeeperClient(TbQueueProducerProvider producerProvider) { this.producer = producerProvider.getHousekeeperMsgProducer(); this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); + this.submitCallback = new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.trace("Submitted Housekeeper task"); + } + + @Override + public void onFailure(Throwable t) { + log.error("Failed to submit Housekeeper task", t); + } + }; } @Override public void submitTask(HousekeeperTask task) { - log.trace("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); + log.debug("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); /* * using msg key as entity id so that msgs related to certain entity are pushed to same partition, * e.g. on tenant deletion (entity id is tenant id), we need to clean up tenant entities in certain order * */ - producer.send(submitTpi, new TbProtoQueueMsg<>(task.getEntityId().getId(), ToHousekeeperServiceMsg.newBuilder() - .setTask(TransportProtos.HousekeeperTaskProto.newBuilder() - .setValue(JacksonUtil.toString(task)) - .setTs(task.getTs()) - .setAttempt(0) - .build()) - .build()), null); + try { + producer.send(submitTpi, new TbProtoQueueMsg<>(task.getEntityId().getId(), ToHousekeeperServiceMsg.newBuilder() + .setTask(TransportProtos.HousekeeperTaskProto.newBuilder() + .setValue(JacksonUtil.toString(task)) + .setTs(task.getTs()) + .setAttempt(0) + .build()) + .build()), submitCallback); + } catch (Throwable t) { + log.error("Failed to submit Housekeeper task {}", task, t); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 478963d300..03c7cf1173 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -21,9 +21,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -39,8 +39,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -309,8 +309,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { - return TbKafkaProducerTemplate.>builder() + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("tb-core-housekeeper-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) @@ -319,21 +319,21 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createHousekeeperMsgConsumer() { - return TbKafkaConsumerTemplate.>builder() + public TbQueueConsumer> createHousekeeperMsgConsumer() { + return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) .topic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) .clientId("tb-core-housekeeper-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("tb-core-housekeeper-consumer")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(housekeeperAdmin) .statsService(consumerStatsService) .build(); } @Override - public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { - return TbKafkaProducerTemplate.>builder() + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { + return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("tb-core-housekeeper-reprocessing-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) @@ -342,13 +342,13 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { - return TbKafkaConsumerTemplate.>builder() + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { + return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) .topic(topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())) .clientId("tb-core-housekeeper-reprocessing-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("tb-core-housekeeper-reprocessing-consumer")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(housekeeperReprocessingAdmin) .statsService(consumerStatsService) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 869f364970..f6fd5d4619 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -25,6 +25,7 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -233,8 +234,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { - return TbKafkaProducerTemplate.>builder() + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("tb-rule-engine-housekeeper-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java index 4140925775..8464c98770 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; @@ -176,8 +177,8 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { - return TbKafkaProducerTemplate.>builder() + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("tb-transport-housekeeper-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java index 95086e04b4..4d3ddc4925 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -109,8 +110,8 @@ public class KafkaTbVersionControlQueueFactory implements TbVersionControlQueueF } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { - return TbKafkaProducerTemplate.>builder() + public TbQueueProducer> createHousekeeperMsgProducer() { + return TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("tb-vc-housekeeper-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(coreSettings.getHousekeeperTopic())) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index d0192d3094..2b118251ac 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -25,6 +25,8 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -157,12 +159,12 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java index d91dd91873..062bd65cad 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; @@ -132,7 +133,7 @@ public class PubSubTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 56946c4705..a2bc68b61b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -18,6 +18,8 @@ package org.thingsboard.server.dao.housekeeper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -36,14 +38,20 @@ public class CleanUpService { private final HousekeeperClient housekeeperClient; private final RelationService relationService; - @TransactionalEventListener(fallbackExecution = true) + @TransactionalEventListener(fallbackExecution = true) // after transaction commit + @Transactional(propagation = Propagation.NOT_SUPPORTED) public void handleEntityDeletionEvent(DeleteEntityEvent event) { TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); - log.trace("[{}][{}][{}] Handling entity deletion event", tenantId, entityId.getEntityType(), entityId.getId()); - cleanUpRelatedData(tenantId, entityId); - if (entityId.getEntityType() == EntityType.USER) { - housekeeperClient.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); + EntityType entityType = entityId.getEntityType(); + try { + log.trace("[{}][{}][{}] Handling entity deletion event", tenantId, entityType, entityId.getId()); + cleanUpRelatedData(tenantId, entityId); + if (entityType == EntityType.USER) { + housekeeperClient.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); + } + } catch (Throwable e) { + log.error("[{}][{}][{}] Failed to handle entity deletion event", tenantId, entityType, entityId.getId(), e); } } From a6719efee58042153f3caa3e592d828851121e9f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 18 Mar 2024 12:24:58 +0200 Subject: [PATCH 023/147] Housekeeper: alarms deletion --- .../entitiy/AbstractTbEntityService.java | 12 ----- .../entitiy/asset/DefaultTbAssetService.java | 1 - .../device/DefaultTbDeviceService.java | 1 - .../housekeeper/HousekeeperService.java | 3 +- ....java => AlarmsDeletionTaskProcessor.java} | 20 ++++--- .../service/ttl/AlarmsCleanUpService.java | 1 - .../housekeeper/HousekeeperServiceTest.java | 54 ++++++++++++++++++- .../server/dao/alarm/AlarmService.java | 4 +- .../data/housekeeper/HousekeeperTask.java | 4 +- .../data/housekeeper/HousekeeperTaskType.java | 4 +- .../server/common/data/page/PageData.java | 2 + .../server/dao/alarm/AlarmDao.java | 4 +- .../server/dao/alarm/BaseAlarmService.java | 38 ++++++++++--- .../dao/housekeeper/CleanUpService.java | 2 +- .../dao/sql/alarm/EntityAlarmRepository.java | 4 +- .../server/dao/sql/alarm/JpaAlarmDao.java | 9 +++- 16 files changed, 123 insertions(+), 40 deletions(-) rename application/src/main/java/org/thingsboard/server/service/housekeeper/processor/{EntityAlarmsDeletionTaskProcessor.java => AlarmsDeletionTaskProcessor.java} (57%) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index ff8dd52ec5..9af65c0965 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -25,15 +25,10 @@ import org.springframework.context.annotation.Lazy; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.alarm.AlarmInfo; -import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -72,13 +67,6 @@ public abstract class AbstractTbEntityService { @Lazy private EntitiesVersionControlService vcService; - protected void removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { - PageData alarms = - alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)); - - alarms.getData().stream().map(AlarmInfo::getId).forEach(alarmId -> alarmService.delAlarm(tenantId, alarmId)); - } - protected T checkNotNull(T reference) throws ThingsboardException { return checkNotNull(reference, "Requested item wasn't found!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 58940318be..e30f6f9922 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -78,7 +78,6 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb TenantId tenantId = asset.getTenantId(); AssetId assetId = asset.getId(); try { - removeAlarmsByEntityId(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); notificationEntityService.logEntityAction(tenantId, assetId, asset, asset.getCustomerId(), actionType, user, assetId.toString()); tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index 8d43a43cbc..f7d11e88d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -97,7 +97,6 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T TenantId tenantId = device.getTenantId(); DeviceId deviceId = device.getId(); try { - removeAlarmsByEntityId(tenantId, deviceId); deviceService.deleteDevice(tenantId, deviceId); notificationEntityService.notifyDeleteDevice(tenantId, deviceId, device.getCustomerId(), device, user, deviceId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 4c0c0e72d5..fa59eae4c9 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -130,7 +130,7 @@ public class HousekeeperService { } if (log.isDebugEnabled()) { - log.debug("[{}] {} task {}", task.getTenantId(), isNew(msg.getTask()) ? "Processing" : "Reprocessing", msg.getTask().getValue()); + log.debug("[{}] {} {}", task.getTenantId(), isNew(msg.getTask()) ? "Processing" : "Reprocessing", task.getDescription()); } try { Future future = executor.submit(() -> { @@ -140,7 +140,6 @@ public class HousekeeperService { future.get(taskProcessingTimeout, TimeUnit.MILLISECONDS); statsService.ifPresent(statsService -> statsService.reportProcessed(task.getTaskType(), msg)); - log.debug("[{}] Successfully {} task {}", task.getTenantId(), isNew(msg.getTask()) ? "processed" : "reprocessed", msg.getTask().getValue()); } catch (InterruptedException e) { throw e; } catch (Throwable e) { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java similarity index 57% rename from application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java rename to application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index 0827ce2441..2d7cf690d9 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntityAlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -16,27 +16,35 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.dao.alarm.AlarmService; @Component @RequiredArgsConstructor -public class EntityAlarmsDeletionTaskProcessor implements HousekeeperTaskProcessor { +@Slf4j +public class AlarmsDeletionTaskProcessor implements HousekeeperTaskProcessor { private final AlarmService alarmService; @Override public void process(HousekeeperTask task) throws Exception { - alarmService.deleteEntityAlarmRecords(task.getTenantId(), task.getEntityId()); - // fixme: do we need to remove alarms by originator ??? - // fixme: why alarm comments are not deleted ?? + EntityType entityType = task.getEntityId().getEntityType(); + if (entityType == EntityType.DEVICE || entityType == EntityType.ASSET) { + int count = alarmService.deleteAlarmsByEntityId(task.getTenantId(), task.getEntityId()); + log.debug("[{}][{}][{}] Deleted {} alarms", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), count); + } else { + int count = alarmService.deleteEntityAlarmRecords(task.getTenantId(), task.getEntityId()); + log.debug("[{}][{}][{}] Deleted {} entity alarms", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), count); + } } @Override public HousekeeperTaskType getTaskType() { - return HousekeeperTaskType.DELETE_ENTITY_ALARMS; + return HousekeeperTaskType.DELETE_ALARMS; } } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java index 76de56cf13..c0f3158c61 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java @@ -56,7 +56,6 @@ public class AlarmsCleanUpService { private final TenantService tenantService; private final AlarmDao alarmDao; private final AlarmService alarmService; - private final RelationService relationService; private final EntityActionService entityActionService; private final PartitionService partitionService; private final TbTenantProfileCache tenantProfileCache; diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index daf363467d..1204e36184 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -32,12 +32,17 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.EntityAlarm; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.LifecycleEvent; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.id.AlarmId; +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.RuleNodeId; @@ -52,15 +57,19 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -115,6 +124,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private RuleChainService ruleChainService; @Autowired private AlarmService alarmService; + @Autowired + private AlarmDao alarmDao; + @Autowired + private RelationService relationService; @SpyBean private TelemetryDeletionTaskProcessor telemetryDeletionTaskProcessor; @@ -200,6 +213,11 @@ public class HousekeeperServiceTest extends AbstractControllerTest { Device device = createDevice("oi324rujoi", "oi324rujoi"); createRelatedData(device.getId()); + Asset asset = createAsset(); + createRelatedData(asset.getId()); + createRelation(device.getId(), asset.getId()); + createAlarm(device.getId(), asset.getId()); + RuleChainMetaData ruleChainMetaData = createRuleChain(); RuleChainId ruleChainId = ruleChainMetaData.getRuleChainId(); RuleNodeId ruleNode1Id = ruleChainMetaData.getNodes().get(0).getId(); @@ -216,6 +234,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verifyNoRelatedData(device.getId()); + verifyNoRelatedData(asset.getId()); verifyNoRelatedData(ruleNode1Id); verifyNoRelatedData(ruleNode2Id); verifyNoRelatedData(ruleChainId); @@ -301,7 +320,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } private void verifyNoRelatedData(EntityId entityId) throws Exception { - List expectedTaskTypes = List.of(HousekeeperTaskType.DELETE_TELEMETRY, HousekeeperTaskType.DELETE_ATTRIBUTES, HousekeeperTaskType.DELETE_EVENTS, HousekeeperTaskType.DELETE_ENTITY_ALARMS); + List expectedTaskTypes = List.of(HousekeeperTaskType.DELETE_TELEMETRY, HousekeeperTaskType.DELETE_ATTRIBUTES, HousekeeperTaskType.DELETE_EVENTS, HousekeeperTaskType.DELETE_ALARMS); for (HousekeeperTaskType taskType : expectedTaskTypes) { verify(housekeeperClient).submitTask(argThat(task -> task.getTaskType() == taskType && task.getEntityId().equals(entityId))); } @@ -312,6 +331,11 @@ public class HousekeeperServiceTest extends AbstractControllerTest { assertThat(getAttribute(entityId, scope, scope + ATTRIBUTE_KEY)).isNull(); } assertThat(getEvents(entityId)).isEmpty(); + assertThat(alarmDao.findEntityAlarmRecordsByEntityId(tenantId, entityId)).isEmpty(); + assertThat(alarmService.findAlarms(tenantId, AlarmQuery.builder().pageLink(new TimePageLink(100)).build()).getData()) + .filteredOn(alarm -> alarm.getOriginator().equals(entityId)).isEmpty(); + assertThat(relationService.findByTo(tenantId, entityId, RelationTypeGroup.COMMON)).isEmpty(); + assertThat(relationService.findByFrom(tenantId, entityId, RelationTypeGroup.COMMON)).isEmpty(); } private void createAttribute(EntityId entityId, String scope, String key) throws Exception { @@ -336,6 +360,27 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } + private void createRelation(DeviceId to, AssetId from) { + EntityRelation relation = new EntityRelation(from, to, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, relation); + } + + private void createAlarm(DeviceId deviceId, EntityId propagatedEntityId) { + Alarm alarm = doPost("/api/alarm", Alarm.builder() + .tenantId(tenantId) + .originator(deviceId) + .severity(AlarmSeverity.CRITICAL) + .type("test alarm for " + deviceId) + .propagate(true) + .build(), Alarm.class); + + List entityAlarms = alarmDao.findEntityAlarmRecords(tenantId, alarm.getId()); + assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(deviceId) && entityAlarm.getAlarmType().equals(alarm.getType())); + assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(propagatedEntityId) && entityAlarm.getAlarmType().equals(alarm.getType())); + assertThat(alarmService.findAlarms(tenantId, AlarmQuery.builder().pageLink(new TimePageLink(100)).build()).getData()) + .filteredOn(a -> a.getOriginator().equals(deviceId)).isNotEmpty(); + } + private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { return timeseriesService.findLatest(tenantId, entityId, HousekeeperServiceTest.TELEMETRY_KEY).get().orElse(null); } @@ -355,6 +400,13 @@ public class HousekeeperServiceTest extends AbstractControllerTest { .collect(Collectors.toList()); } + private Asset createAsset() { + Asset asset = new Asset(); + asset.setName("test"); + asset.setType("test"); + return doPost("/api/asset", asset, Asset.class); + } + private RuleChainMetaData createRuleChain() { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 7a55348bed..5607e4ff50 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -81,6 +81,8 @@ public interface AlarmService extends EntityDaoService { void delAlarmTypes(TenantId tenantId, Set types); + int deleteAlarmsByEntityId(TenantId tenantId, EntityId entityId); + // Other API Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); @@ -106,7 +108,7 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); - void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); + int deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); void deleteEntityAlarmRecordsByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index 44598d511c..bc41f553ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -69,8 +69,8 @@ public class HousekeeperTask implements Serializable { return new AlarmsUnassignHousekeeperTask(user); } - public static HousekeeperTask deleteEntityAlarms(TenantId tenantId, EntityId entityId) { - return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ENTITY_ALARMS); + public static HousekeeperTask deleteAlarms(TenantId tenantId, EntityId entityId) { + return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ALARMS); } public static HousekeeperTask deleteEntities(TenantId tenantId, EntityType entityType) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java index 4f30f94a45..eb5833b06f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java @@ -26,8 +26,8 @@ public enum HousekeeperTaskType { DELETE_ATTRIBUTES("attributes deletion"), DELETE_TELEMETRY("telemetry deletion"), DELETE_EVENTS("events deletion"), - UNASSIGN_ALARMS("alarms unassigning"), - DELETE_ENTITY_ALARMS("entity alarms deletion"); + DELETE_ALARMS("alarms deletion"), + UNASSIGN_ALARMS("alarms unassigning"); private final String description; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java index 9042e4865f..02129ea31b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; +import lombok.ToString; import java.io.Serializable; import java.util.Collections; @@ -29,6 +30,7 @@ import java.util.stream.Collectors; @ApiModel @EqualsAndHashCode +@ToString public class PageData implements Serializable { public static final PageData EMPTY_PAGE_DATA = new PageData<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index c752ae706d..339c8fd988 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -84,7 +84,9 @@ public interface AlarmDao extends Dao { List findEntityAlarmRecords(TenantId tenantId, AlarmId id); - void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); + List findEntityAlarmRecordsByEntityId(TenantId tenantId, EntityId entityId); + + int deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); void deleteEntityAlarmRecordsByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index a14c172a57..9b7004042d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -183,16 +184,23 @@ public class BaseAlarmService extends AbstractCachedEntityService alarms; + int count = 0; + do { + alarms = findAlarms(tenantId, AlarmQuery.builder() + .affectedEntityId(entityId) + .pageLink(new TimePageLink(pageLink, null, null)) + .build()); + for (AlarmInfo alarm : alarms.getData()) { + deleteAlarm(tenantId, alarm, true); + count++; + } + } while (alarms.hasNext()); + return count; + } + private List createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException { Set propagatedEntitiesSet = new LinkedHashSet<>(); propagatedEntitiesSet.add(alarm.getOriginator()); @@ -322,9 +348,9 @@ public class BaseAlarmService extends AbstractCachedEntityService findAllByEntityId(UUID entityId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 9430773ff9..1483cf8a8e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -309,8 +309,13 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId) { - entityAlarmRepository.deleteByEntityId(entityId.getId()); + public List findEntityAlarmRecordsByEntityId(TenantId tenantId, EntityId entityId) { + return DaoUtil.convertDataList(entityAlarmRepository.findAllByEntityId(entityId.getId())); + } + + @Override + public int deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId) { + return entityAlarmRepository.deleteByEntityId(entityId.getId()); } @Override From dfb40ecbe053796e6844e318ff0fef3f432bac48 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 18 Mar 2024 12:35:29 +0200 Subject: [PATCH 024/147] Skipped entity types list for cleaning up related data --- .../server/dao/housekeeper/CleanUpService.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 24cfe7c8f6..48531aab90 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -30,6 +30,9 @@ import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.relation.RelationService; +import java.util.EnumSet; +import java.util.Set; + @Component @RequiredArgsConstructor @Slf4j @@ -38,6 +41,12 @@ public class CleanUpService { private final HousekeeperClient housekeeperClient; private final RelationService relationService; + private final Set skippedEntities = EnumSet.of( + EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, + EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE + ); + @TransactionalEventListener(fallbackExecution = true) // after transaction commit @Transactional(propagation = Propagation.NOT_SUPPORTED) public void handleEntityDeletionEvent(DeleteEntityEvent event) { @@ -46,7 +55,9 @@ public class CleanUpService { EntityType entityType = entityId.getEntityType(); try { log.trace("[{}][{}][{}] Handling entity deletion event", tenantId, entityType, entityId.getId()); - cleanUpRelatedData(tenantId, entityId); + if (!skippedEntities.contains(entityType)) { + cleanUpRelatedData(tenantId, entityId); + } if (entityType == EntityType.USER) { housekeeperClient.submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity())); } @@ -57,7 +68,6 @@ public class CleanUpService { public void cleanUpRelatedData(TenantId tenantId, EntityId entityId) { log.debug("[{}][{}][{}] Cleaning up related data", tenantId, entityId.getEntityType(), entityId.getId()); - // todo: skipped entities list relationService.deleteEntityRelations(tenantId, entityId); housekeeperClient.submitTask(HousekeeperTask.deleteAttributes(tenantId, entityId)); housekeeperClient.submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId)); From 302fed18883caf74d4626ba8f47d1bf00d0ff372 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 18 Mar 2024 16:55:39 +0200 Subject: [PATCH 025/147] Await tenant cleanup after tenant deletion in tests --- .../server/controller/AbstractWebTest.java | 14 ++++++++++++-- .../housekeeper/HousekeeperServiceTest.java | 1 - .../src/test/resources/application-test.properties | 1 - 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 63c974fa96..85edac9933 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -100,6 +100,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; @@ -273,7 +275,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Tenant tenant = new Tenant(); tenant.setTitle(TEST_TENANT_NAME); - Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Tenant savedTenant = createTenant(tenant); Assert.assertNotNull(savedTenant); tenantId = savedTenant.getId(); tenantProfileId = savedTenant.getTenantProfileId(); @@ -367,6 +369,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { throw new RuntimeException(e); } } + Awaitility.await("tenant cleanup finish").atMost(30, TimeUnit.SECONDS) + .until(() -> tsService.findLatest(TenantId.SYS_TENANT_ID, tenantId, "test").get().isEmpty()); } private List getAllTenants() throws Exception { @@ -419,7 +423,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { loginSysAdmin(); Tenant tenant = new Tenant(); tenant.setTitle(TEST_DIFFERENT_TENANT_NAME); - savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); + savedDifferentTenant = createTenant(tenant); differentTenantId = savedDifferentTenant.getId(); Assert.assertNotNull(savedDifferentTenant); User differentTenantAdmin = new User(); @@ -429,6 +433,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { savedDifferentTenantUser = createUserAndLogin(differentTenantAdmin, DIFFERENT_TENANT_ADMIN_PASSWORD); } + protected Tenant createTenant(Tenant tenant) throws Exception { + tenant = doPost("/api/tenant", tenant, Tenant.class); + tsService.save(TenantId.SYS_TENANT_ID, tenant.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("test", "test"))).get(); // creating marker ts to later know when Housekeeper finishes tenant cleanup + return tenant; + } + protected void loginDifferentCustomer() throws Exception { if (savedDifferentCustomer != null) { login(savedDifferentCustomer.getEmail(), CUSTOMER_USER_PASSWORD); diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 1204e36184..71c11a4e95 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -99,7 +99,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest @TestPropertySource(properties = { - "queue.core.housekeeper.enabled=true", "transport.http.enabled=true", "queue.core.housekeeper.reprocessing-start-delay-sec=1", "queue.core.housekeeper.task-reprocessing-delay-sec=2", diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 5e4900fdc1..92959e4552 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -54,4 +54,3 @@ sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 server.log_controller_error_stack_trace=false -queue.core.housekeeper.enabled=false From 1ed95b24a60a9356279d66726d67afc1054b8b43 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 18 Mar 2024 18:02:06 +0200 Subject: [PATCH 026/147] Clean up api usage state data on deletion --- .../housekeeper/HousekeeperServiceTest.java | 22 ++++++++++++++----- .../dao/usagerecord/ApiUsageStateService.java | 2 -- .../dao/attributes/BaseAttributesService.java | 1 - .../server/dao/rule/BaseRuleChainService.java | 1 - .../server/dao/tenant/TenantServiceImpl.java | 3 --- .../usagerecord/ApiUsageStateServiceImpl.java | 19 ++++++++-------- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 71c11a4e95..97c87df5d1 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -28,6 +28,7 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EventInfo; @@ -48,7 +49,6 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -68,11 +68,13 @@ import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateDao; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.service.housekeeper.processor.TelemetryDeletionTaskProcessor; @@ -127,6 +129,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private AlarmDao alarmDao; @Autowired private RelationService relationService; + @Autowired + private ApiUsageStateDao apiUsageStateDao; + @Autowired + private EntityServiceRegistry entityServiceRegistry; @SpyBean private TelemetryDeletionTaskProcessor telemetryDeletionTaskProcessor; @@ -208,6 +214,9 @@ public class HousekeeperServiceTest extends AbstractControllerTest { tenantId = differentTenantId; createRelatedData(tenantId); + createDifferentTenantCustomer(); + createRelatedData(differentTenantCustomerId); + loginDifferentTenant(); Device device = createDevice("oi324rujoi", "oi324rujoi"); createRelatedData(device.getId()); @@ -228,6 +237,8 @@ public class HousekeeperServiceTest extends AbstractControllerTest { UserId userId = savedDifferentTenantUser.getId(); createRelatedData(userId); + ApiUsageState tenantApiUsageState = apiUsageStateDao.findApiUsageStateByEntityId(differentTenantId); + loginSysAdmin(); deleteDifferentTenant(); @@ -238,6 +249,8 @@ public class HousekeeperServiceTest extends AbstractControllerTest { verifyNoRelatedData(ruleNode2Id); verifyNoRelatedData(ruleChainId); verifyNoRelatedData(userId); + verifyNoRelatedData(differentTenantCustomerId); + verifyNoRelatedData(tenantApiUsageState.getId()); verifyNoRelatedData(tenantId); }); } @@ -323,11 +336,12 @@ public class HousekeeperServiceTest extends AbstractControllerTest { for (HousekeeperTaskType taskType : expectedTaskTypes) { verify(housekeeperClient).submitTask(argThat(task -> task.getTaskType() == taskType && task.getEntityId().equals(entityId))); } + assertThat(entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()).findEntity(tenantId, entityId)).isEmpty(); assertThat(getLatestTelemetry(entityId)).isNull(); assertThat(getTimeseriesHistory(entityId)).isEmpty(); for (String scope : List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE, DataConstants.CLIENT_SCOPE)) { - assertThat(getAttribute(entityId, scope, scope + ATTRIBUTE_KEY)).isNull(); + assertThat(attributesService.findAll(tenantId, entityId, scope).get()).isEmpty(); } assertThat(getEvents(entityId)).isEmpty(); assertThat(alarmDao.findEntityAlarmRecordsByEntityId(tenantId, entityId)).isEmpty(); @@ -388,10 +402,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest { return timeseriesService.findAll(tenantId, entityId, List.of(new BaseReadTsKvQuery(HousekeeperServiceTest.TELEMETRY_KEY, 0, System.currentTimeMillis(), 10, "DESC"))).get(); } - private AttributeKvEntry getAttribute(EntityId entityId, String scope, String key) throws Exception { - return attributesService.find(tenantId, entityId, scope, key).get().orElse(null); - } - private List getEvents(EntityId entityId) { return eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, new TimePageLink(100)).getData() .stream().filter(event -> Optional.ofNullable(event.getBody()).map(body -> body.get("event")) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java index 2ed877dabd..3d09247c7e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java @@ -31,8 +31,6 @@ public interface ApiUsageStateService extends EntityDaoService { ApiUsageState findApiUsageStateByEntityId(EntityId entityId); - void deleteApiUsageStateByTenantId(TenantId tenantId); - void deleteApiUsageStateByEntityId(EntityId entityId); ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 9359383071..1cace5b71c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -118,7 +118,6 @@ public class BaseAttributesService implements AttributesService { @Override public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { List> deleted = attributesDao.removeAllByEntityId(tenantId, entityId); - // todo: probably better to retrieve by scope in case there are too many attributes return deleted.size(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 18f3d58ba7..3c6489146c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -178,7 +178,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC List updatedRuleNodes = new ArrayList<>(); List existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId()); for (RuleNode existingNode : existingRuleNodes) { - cleanUpService.cleanUpRelatedData(tenantId, existingNode.getId()); // fixme: for sure? Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 181989a620..83e9ca8ed5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.tenant; import com.google.common.util.concurrent.ListenableFuture; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -54,7 +53,6 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TenantDaoService") @Slf4j -@RequiredArgsConstructor public class TenantServiceImpl extends AbstractCachedEntityService implements TenantService { private static final String DEFAULT_TENANT_REGION = "Global"; @@ -153,7 +151,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Tue, 19 Mar 2024 15:44:27 +0200 Subject: [PATCH 027/147] UI: added support of 'ownerName' and 'ownerType' entity fields to datakeys autocomplete --- ui-ngx/src/app/core/http/entity.service.ts | 8 ++++++++ ui-ngx/src/app/shared/models/entity.models.ts | 10 ++++++++++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 4 +++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index b29fd6c9a0..76dd47c7f4 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -765,6 +765,8 @@ export class EntityService { entityFieldKeys.push(entityFields.firstName.keyName); entityFieldKeys.push(entityFields.lastName.keyName); entityFieldKeys.push(entityFields.phone.keyName); + entityFieldKeys.push(entityFields.ownerName.keyName); + entityFieldKeys.push(entityFields.ownerType.keyName); break; case EntityType.TENANT: case EntityType.CUSTOMER: @@ -781,6 +783,8 @@ export class EntityService { case EntityType.ENTITY_VIEW: entityFieldKeys.push(entityFields.name.keyName); entityFieldKeys.push(entityFields.type.keyName); + entityFieldKeys.push(entityFields.ownerName.keyName); + entityFieldKeys.push(entityFields.ownerType.keyName); break; case EntityType.DEVICE: case EntityType.EDGE: @@ -788,9 +792,13 @@ export class EntityService { entityFieldKeys.push(entityFields.name.keyName); entityFieldKeys.push(entityFields.type.keyName); entityFieldKeys.push(entityFields.label.keyName); + entityFieldKeys.push(entityFields.ownerName.keyName); + entityFieldKeys.push(entityFields.ownerType.keyName); break; case EntityType.DASHBOARD: entityFieldKeys.push(entityFields.title.keyName); + entityFieldKeys.push(entityFields.ownerName.keyName); + entityFieldKeys.push(entityFields.ownerType.keyName); break; case EntityType.API_USAGE_STATE: entityFieldKeys.push(entityFields.name.keyName); diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 84d138f0a8..432559b544 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -161,6 +161,16 @@ export const entityFields: {[fieldName: string]: EntityField} = { keyName: 'label', name: 'entity-field.label', value: 'label' + }, + ownerName: { + keyName: 'ownerName', + name: 'entity-field.owner-name', + value: 'ownerName' + }, + ownerType: { + keyName: 'ownerType', + name: 'entity-field.owner-type', + value: 'ownerType' } }; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1948b84e79..4cf6762e68 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2259,7 +2259,9 @@ "address2": "Address 2", "zip": "Zip", "phone": "Phone", - "label": "Label" + "label": "Label", + "owner-name": "Owner name", + "owner-type": "Owner type" }, "entity-view": { "entity-view": "Entity view", From 56c65f249d8fe180dfa3e54f4bb886bbc574991a Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 20 Mar 2024 15:12:57 +0200 Subject: [PATCH 028/147] added selection mapping for entity property filters --- .../controller/EntityQueryControllerTest.java | 54 +++++++++++++++++++ .../dao/sql/query/EntityKeyMapping.java | 9 +++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index edd2a372c1..395f90618c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -594,6 +594,55 @@ public class EntityQueryControllerTest extends AbstractControllerTest { assertThat(getErrorMessage(result)).contains("Invalid").contains("sort property"); } + @Test + public void testFindDevicesCountByOwnerNameAndOwnerType() throws Exception { + loginTenantAdmin(); + int numOfDevices = 8; + + for (int i = 0; i < numOfDevices; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setType("default"); + + Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class); + JsonNode content = JacksonUtil.toJsonNode("{\"alarmActiveTime\": 1" + i + "}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + savedDevice.getUuidId() + "/SERVER_SCOPE", content) + .andExpect(status().isOk()); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter(""); + + KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); + KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30); + KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); + KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); + KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); + + // all devices with ownerName = TEST TENANT + EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); + checkEntitiesCount(query, numOfDevices); + + // all devices with ownerName = TEST TENANT + EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter)); + checkEntitiesCount(activeAlarmTimeToLongQuery, 0); + + // all devices with wrong ownerName + EntityCountQuery wrongTenantNameQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); + checkEntitiesCount(wrongTenantNameQuery, 0); + + // all devices with owner type = TENANT + EntityCountQuery tenantEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); + checkEntitiesCount(tenantEntitiesQuery, numOfDevices); + + // all devices with owner type = CUSTOMER + EntityCountQuery customerEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); + checkEntitiesCount(customerEntitiesQuery, 0); + } + @Test public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { loginTenantAdmin(); @@ -677,6 +726,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest { } } + private void checkEntitiesCount(EntityCountQuery query, int expectedNumOfDevices) { + var count = doPost("/api/entitiesQuery/count", query, Integer.class); + assertThat(count).isEqualTo(expectedNumOfDevices); + } + private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) { KeyFilter tenantOwnerNameFilter = new KeyFilter(); tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 0f51ef72b2..bf6f560948 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -418,8 +418,9 @@ public class EntityKeyMapping { mapping.setAlias(String.format("alias%s", index)); mapping.setKeyFilters(filters.get(filterField)); mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); - mapping.setSelection(false); mapping.setEntityKey(filterField); + mapping.setEntityKeyColumn(entityType, entityFilterType); + mapping.setSelection(mapping.getEntityKeyColumn() == null); mappings.add(mapping); index += 1; } @@ -437,6 +438,9 @@ public class EntityKeyMapping { } public static List prepareEntityCountKeyMapping(EntityCountQuery query) { + EntityType entityType = resolveEntityType(query.getEntityFilter()); + EntityFilterType entityFilterType = query.getEntityFilter().getType(); + Map> filters = query.getKeyFilters() != null ? query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); @@ -449,8 +453,9 @@ public class EntityKeyMapping { mapping.setAlias(String.format("alias%s", index)); mapping.setKeyFilters(filters.get(filterField)); mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); - mapping.setSelection(false); mapping.setEntityKey(filterField); + mapping.setEntityKeyColumn(entityType, entityFilterType); + mapping.setSelection(mapping.getEntityKeyColumn() == null); mappings.add(mapping); index += 1; } From 1c724df353d802fd6117d05afbcc625860487c88 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 20 Mar 2024 17:59:55 +0200 Subject: [PATCH 029/147] added await to make tests more stable --- .../server/controller/EntityQueryControllerTest.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 395f90618c..39ab60dc49 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -727,9 +727,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest { } private void checkEntitiesCount(EntityCountQuery query, int expectedNumOfDevices) { - var count = doPost("/api/entitiesQuery/count", query, Integer.class); - assertThat(count).isEqualTo(expectedNumOfDevices); - } + Awaitility.await() + .alias("count by query") + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + var count = doPost("/api/entitiesQuery/count", query, Integer.class); + return count == expectedNumOfDevices; + }); + } private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) { KeyFilter tenantOwnerNameFilter = new KeyFilter(); From a529abecedf2301b4c049cae87861a66500d4a0c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 21 Mar 2024 12:43:34 +0200 Subject: [PATCH 030/147] Fix attributes removal by entity id --- .../server/common/data/AttributeScope.java | 11 +++++++++++ .../server/dao/attributes/BaseAttributesService.java | 2 +- .../dao/attributes/CachedAttributesService.java | 10 ++++++---- .../server/dao/sql/attributes/JpaAttributeDao.java | 10 +++++----- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/AttributeScope.java b/common/data/src/main/java/org/thingsboard/server/common/data/AttributeScope.java index 0e8335c7cb..3c58dd3b72 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/AttributeScope.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/AttributeScope.java @@ -17,6 +17,10 @@ package org.thingsboard.server.common.data; import lombok.Getter; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + public enum AttributeScope { CLIENT_SCOPE(1), @@ -25,8 +29,15 @@ public enum AttributeScope { @Getter private final int id; + private static final Map values = Arrays.stream(values()) + .collect(Collectors.toMap(AttributeScope::getId, scope -> scope)); + AttributeScope(int id) { this.id = id; } + public static AttributeScope valueOf(int id) { + return values.get(id); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 4a63f4c04d..60856d361b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -164,7 +164,7 @@ public class BaseAttributesService implements AttributesService { @Override public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { - List> deleted = attributesDao.removeAllByEntityId(tenantId, entityId); + List> deleted = attributesDao.removeAllByEntityId(tenantId, entityId); return deleted.size(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index ad3e0ecf3c..45e671a891 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; @@ -40,7 +41,6 @@ import org.thingsboard.server.dao.cache.CacheExecutorService; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.sql.JpaExecutorService; -import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -308,11 +308,13 @@ public class CachedAttributesService implements AttributesService { @Override public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { - List> result = attributesDao.removeAllByEntityId(tenantId, entityId); + List> result = attributesDao.removeAllByEntityId(tenantId, entityId); result.forEach(deleted -> { - String scope = deleted.getKey(); + AttributeScope scope = deleted.getKey(); String key = deleted.getValue(); - cache.evict(new AttributeCacheKey(scope, entityId, key)); + if (scope != null && key != null) { + cache.evict(new AttributeCacheKey(scope, entityId, key)); + } }); return result.size(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index a227d1ed50..3bf8e43c70 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -35,8 +35,8 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.attributes.AttributesDao; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; @@ -209,10 +209,10 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Transactional @Override public List> removeAllByEntityId(TenantId tenantId, EntityId entityId) { - return jdbcTemplate.queryForList("DELETE FROM attribute_kv WHERE entity_type = ? and entity_id = ? " + - "RETURNING attribute_type, attribute_key", entityId.getEntityType().name(), entityId.getId()).stream() - .map(row -> Pair.of(row.get(ModelConstants.ATTRIBUTE_TYPE_COLUMN), - (String) row.get(ModelConstants.ATTRIBUTE_KEY_COLUMN))) + return jdbcTemplate.queryForList("DELETE FROM attribute_kv WHERE entity_id = ? " + + "RETURNING attribute_type, attribute_key", entityId.getId()).stream() + .map(row -> Pair.of(AttributeScope.valueOf((Integer) row.get(ModelConstants.ATTRIBUTE_TYPE_COLUMN)), + keyDictionaryDao.getKey((Integer) row.get(ModelConstants.ATTRIBUTE_KEY_COLUMN)))) .collect(Collectors.toList()); } From e5d48921649d542417eff83e0f11685daec3e37b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 27 Mar 2024 13:25:42 +0200 Subject: [PATCH 031/147] Separate latest and ts history deletion tasks for each key --- .../HousekeeperReprocessingService.java | 2 - .../housekeeper/HousekeeperService.java | 2 - .../AlarmsDeletionTaskProcessor.java | 2 +- .../AlarmsUnassignTaskProcessor.java | 2 +- .../AttributesDeletionTaskProcessor.java | 2 +- .../EntitiesDeletionTaskProcessor.java | 2 +- .../EventsDeletionTaskProcessor.java | 2 +- .../processor/HousekeeperTaskProcessor.java | 11 +++-- .../LatestTsDeletionTaskProcessor.java | 45 +++++++++++++++++ .../TelemetryDeletionTaskProcessor.java | 23 ++++++--- .../TsHistoryDeletionTaskProcessor.java | 48 +++++++++++++++++++ .../src/main/resources/thingsboard.yml | 2 - .../housekeeper/HousekeeperServiceTest.java | 28 ++++++----- .../data/housekeeper/HousekeeperTask.java | 11 +++++ .../data/housekeeper/HousekeeperTaskType.java | 2 + .../LatestTsDeletionHousekeeperTask.java | 37 ++++++++++++++ .../TsHistoryDeletionHousekeeperTask.java | 37 ++++++++++++++ 17 files changed, 224 insertions(+), 34 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TsHistoryDeletionTaskProcessor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 8b1dece58e..7410ec2903 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -49,7 +48,6 @@ import java.util.concurrent.atomic.AtomicInteger; @TbCoreComponent @Service @Slf4j -@ConditionalOnProperty(name = "queue.core.housekeeper.enabled", havingValue = "true", matchIfMissing = true) public class HousekeeperReprocessingService { private final HousekeeperService housekeeperService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index fa59eae4c9..1a8d07c9da 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -51,7 +50,6 @@ import java.util.stream.Collectors; @TbCoreComponent @Service @Slf4j -@ConditionalOnProperty(name = "queue.core.housekeeper.enabled", havingValue = "true", matchIfMissing = true) public class HousekeeperService { private final Map> taskProcessors; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index 2d7cf690d9..eea0651873 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -26,7 +26,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; @Component @RequiredArgsConstructor @Slf4j -public class AlarmsDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor { private final AlarmService alarmService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java index 9a8517903f..3ba6ecde52 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java @@ -29,7 +29,7 @@ import java.util.List; @Component @RequiredArgsConstructor @Slf4j -public class AlarmsUnassignTaskProcessor implements HousekeeperTaskProcessor { +public class AlarmsUnassignTaskProcessor extends HousekeeperTaskProcessor { private final TbAlarmService alarmService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java index c8ff5306a7..5514921e2e 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; @Component @RequiredArgsConstructor @Slf4j -public class AttributesDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class AttributesDeletionTaskProcessor extends HousekeeperTaskProcessor { private final AttributesService attributesService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java index a16a5f09bd..95d45a49be 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java @@ -24,7 +24,7 @@ import org.thingsboard.server.dao.entity.EntityServiceRegistry; @Component @RequiredArgsConstructor -public class EntitiesDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class EntitiesDeletionTaskProcessor extends HousekeeperTaskProcessor { private final EntityServiceRegistry entityServiceRegistry; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java index 551c8ce7e0..bf9c528eb7 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; @Component @RequiredArgsConstructor -public class EventsDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class EventsDeletionTaskProcessor extends HousekeeperTaskProcessor { private final EventService eventService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java index 79e87fcc91..f193b154db 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java @@ -15,13 +15,18 @@ */ package org.thingsboard.server.service.housekeeper.processor; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; -public interface HousekeeperTaskProcessor { +public abstract class HousekeeperTaskProcessor { - void process(T task) throws Exception; + @Autowired + protected HousekeeperClient housekeeperClient; - HousekeeperTaskType getTaskType(); + public abstract void process(T task) throws Exception; + + public abstract HousekeeperTaskType getTaskType(); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java new file mode 100644 index 0000000000..1f4b6b57ae --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java @@ -0,0 +1,45 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.LatestTsDeletionHousekeeperTask; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class LatestTsDeletionTaskProcessor extends HousekeeperTaskProcessor { + + private final TimeseriesService timeseriesService; + + @Override + public void process(LatestTsDeletionHousekeeperTask task) throws Exception { + timeseriesService.removeLatest(task.getTenantId(), task.getEntityId(), List.of(task.getKey())).get(); + log.debug("[{}][{}][{}] Deleted latest telemetry for key '{}'", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getKey()); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_LATEST_TS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java index c7f37ed091..5271e77c87 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TelemetryDeletionTaskProcessor.java @@ -18,10 +18,12 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.LatestTsDeletionHousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.TsHistoryDeletionHousekeeperTask; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.List; @@ -29,18 +31,25 @@ import java.util.List; @Component @RequiredArgsConstructor @Slf4j -public class TelemetryDeletionTaskProcessor implements HousekeeperTaskProcessor { +public class TelemetryDeletionTaskProcessor extends HousekeeperTaskProcessor { private final TimeseriesService timeseriesService; @Override public void process(HousekeeperTask task) throws Exception { - List keys = timeseriesService.findAllKeysByEntityIds(task.getTenantId(), List.of(task.getEntityId())); + TenantId tenantId = task.getTenantId(); + EntityId entityId = task.getEntityId(); + List keys = timeseriesService.findAllKeysByEntityIds(tenantId, List.of(entityId)); + for (String key : keys) { - DeleteTsKvQuery deleteQuery = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false, true); - timeseriesService.remove(task.getTenantId(), task.getEntityId(), List.of(deleteQuery)).get(); + var latestTsDeletionTask = new LatestTsDeletionHousekeeperTask(tenantId, entityId, key); + housekeeperClient.submitTask(latestTsDeletionTask); + + var tsHistoryDeletionTask = new TsHistoryDeletionHousekeeperTask(tenantId, entityId, key); + housekeeperClient.submitTask(tsHistoryDeletionTask); } - log.debug("[{}][{}][{}] Deleted {} telemetry keys", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), keys.size()); + + log.trace("[{}][{}][{}] Submitted latest and ts history deletion tasks for {} keys", tenantId, entityId.getEntityType(), entityId, keys.size()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TsHistoryDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TsHistoryDeletionTaskProcessor.java new file mode 100644 index 0000000000..cee99ae93e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TsHistoryDeletionTaskProcessor.java @@ -0,0 +1,48 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.TsHistoryDeletionHousekeeperTask; +import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TsHistoryDeletionTaskProcessor extends HousekeeperTaskProcessor { + + private final TimeseriesService timeseriesService; + + @Override + public void process(TsHistoryDeletionHousekeeperTask task) throws Exception { + DeleteTsKvQuery deleteQuery = new BaseDeleteTsKvQuery(task.getKey(), 0, System.currentTimeMillis(), false, false); + timeseriesService.remove(task.getTenantId(), task.getEntityId(), List.of(deleteQuery)).get(); + log.debug("[{}][{}][{}] Deleted timeseries history for key '{}'", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getKey()); + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_TS_HISTORY; + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 1645bdc84d..99b173399e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1624,8 +1624,6 @@ queue: # Statistics printing interval for Core microservices print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" housekeeper: - # Enabled/disable Housekeeping service, which does cleanup of telemetry, attributes, events, etc. - enabled: "${TB_HOUSEKEEPER_ENABLED:true}" # Topic name for Housekeeper tasks topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" # Topic name for Housekeeper tasks to be reprocessed diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 97c87df5d1..44891c2b7f 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EventInfo; @@ -78,6 +79,7 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateDao; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.service.housekeeper.processor.TelemetryDeletionTaskProcessor; +import org.thingsboard.server.service.housekeeper.processor.TsHistoryDeletionTaskProcessor; import java.util.Arrays; import java.util.Collections; @@ -134,7 +136,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Autowired private EntityServiceRegistry entityServiceRegistry; @SpyBean - private TelemetryDeletionTaskProcessor telemetryDeletionTaskProcessor; + private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor; private TenantId tenantId; @@ -258,7 +260,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenTaskProcessingFails_thenReprocessUntilSuccessful() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); - doThrow(error).when(telemetryDeletionTaskProcessor).process(any()); + doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); Device device = createDevice("vep9ruv32", "vep9ruv32"); createRelatedData(device.getId()); @@ -267,17 +269,17 @@ public class HousekeeperServiceTest extends AbstractControllerTest { int attempts = 3; await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, 0); + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, 0); for (int i = 1; i <= attempts; i++) { int attempt = i; - verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, + verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, task -> task.getErrorsCount() > 0 && task.getAttempt() == attempt)), argThat(e -> e.getMessage().equals(error.getMessage()))); - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, attempt); + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, attempt); } }); assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); - doCallRealMethod().when(telemetryDeletionTaskProcessor).process(any()); // fixing the code + doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); // fixing the code await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(getTimeseriesHistory(device.getId())).isEmpty(); }); @@ -286,7 +288,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenReprocessingAttemptsExceeded_thenReprocessOnNextStartUp() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); - doThrow(error).when(telemetryDeletionTaskProcessor).process(any()); + doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); Device device = createDevice("woeifjiowejf", "woeifjiowejf"); createRelatedData(device.getId()); @@ -296,18 +298,18 @@ public class HousekeeperServiceTest extends AbstractControllerTest { int maxAttempts = 5; await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { for (int i = 1; i <= maxAttempts; i++) { - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, i); + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, i); } }); Mockito.clearInvocations(housekeeperService); - doCallRealMethod().when(telemetryDeletionTaskProcessor).process(any()); + doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); TimeUnit.SECONDS.sleep(2); - verify(housekeeperService, never()).processTask(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, null))); + verify(housekeeperService, never()).processTask(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, null))); housekeeperReprocessingService.cycle.set(0); // imitating start-up await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TELEMETRY, 6); + verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, 6); }); } @@ -325,7 +327,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private void createRelatedData(EntityId entityId) throws Exception { createTelemetry(entityId); - for (String scope : List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE, DataConstants.CLIENT_SCOPE)) { + for (AttributeScope scope : List.of(AttributeScope.SERVER_SCOPE, AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE)) { createAttribute(entityId, scope, scope + ATTRIBUTE_KEY); } createEvent(entityId); @@ -351,7 +353,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { assertThat(relationService.findByFrom(tenantId, entityId, RelationTypeGroup.COMMON)).isEmpty(); } - private void createAttribute(EntityId entityId, String scope, String key) throws Exception { + private void createAttribute(EntityId entityId, AttributeScope scope, String key) throws Exception { attributesService.save(tenantId, entityId, scope, new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, KV_VALUE))).get(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index bc41f553ad..08862d26be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -34,6 +34,8 @@ import java.io.Serializable; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY, defaultImpl = HousekeeperTask.class) @JsonSubTypes({ + @Type(name = "DELETE_TS_HISTORY", value = TsHistoryDeletionHousekeeperTask.class), + @Type(name = "DELETE_LATEST_TS", value = LatestTsDeletionHousekeeperTask.class), @Type(name = "DELETE_ENTITIES", value = EntitiesDeletionHousekeeperTask.class), @Type(name = "UNASSIGN_ALARMS", value = AlarmsUnassignHousekeeperTask.class) }) @@ -57,10 +59,19 @@ public class HousekeeperTask implements Serializable { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ATTRIBUTES); } + /** + * Task for telemetry deletion, processed by TelemetryDeletionTaskProcessor. + * The processor will submit separate tasks for each key for latest and ts history deletion, + * processed by LatestTsDeletionTaskProcessor and TsHistoryDeletionTaskProcessor accordingly. + * */ public static HousekeeperTask deleteTelemetry(TenantId tenantId, EntityId entityId) { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_TELEMETRY); } + /** + * Task for events deletion, processed by EventsDeletionTaskProcessor. + * All types of events will be dropped for this entity. + * */ public static HousekeeperTask deleteEvents(TenantId tenantId, EntityId entityId) { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_EVENTS); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java index eb5833b06f..572e85f2cf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java @@ -25,6 +25,8 @@ public enum HousekeeperTaskType { DELETE_ENTITIES("entities deletion"), DELETE_ATTRIBUTES("attributes deletion"), DELETE_TELEMETRY("telemetry deletion"), + DELETE_LATEST_TS("latest telemetry deletion"), + DELETE_TS_HISTORY("timeseries history deletion"), DELETE_EVENTS("events deletion"), DELETE_ALARMS("alarms deletion"), UNASSIGN_ALARMS("alarms unassigning"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java new file mode 100644 index 0000000000..59e815bbfa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java @@ -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.common.data.housekeeper; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LatestTsDeletionHousekeeperTask extends HousekeeperTask { + + private String key; + + public LatestTsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) { + super(tenantId, entityId, HousekeeperTaskType.DELETE_LATEST_TS); + this.key = key; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java new file mode 100644 index 0000000000..1656626e6b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java @@ -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.common.data.housekeeper; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask { + + private String key; + + public TsHistoryDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) { + super(tenantId, entityId, HousekeeperTaskType.DELETE_TS_HISTORY); + this.key = key; + } + +} From f621d59be323a9c9e1e5fc8a331ba52f0b8ce887 Mon Sep 17 00:00:00 2001 From: imbeacon Date: Thu, 4 Apr 2024 10:45:41 +0300 Subject: [PATCH 032/147] Added logic when ping requests for gateway device will be also activity events for devices, connected through the gateway --- .../transport/mqtt/MqttTransportHandler.java | 16 +++- .../AbstractGatewaySessionHandler.java | 36 +++++++- .../mqtt/session/GatewaySessionHandler.java | 6 +- .../session/SparkplugNodeSessionHandler.java | 4 +- .../session/GatewaySessionHandlerTest.java | 83 +++++++++++++++++++ 5 files changed, 132 insertions(+), 13 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 8360111a54..2ecea63e42 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -45,6 +45,7 @@ import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -64,7 +65,6 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; @@ -323,6 +323,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (checkConnected(ctx, msg)) { ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); transportService.recordActivity(deviceSessionCtx.getSessionInfo()); + if (gatewaySessionHandler != null) { + gatewaySessionHandler.onGatewayPing(); + } } break; case DISCONNECT: @@ -1082,10 +1085,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (infoNode != null) { JsonNode gatewayNode = infoNode.get("gateway"); if (gatewayNode != null && gatewayNode.asBoolean()) { - gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId); + boolean overwriteDevicesActivity = false; if (infoNode.has(DefaultTransportService.OVERWRITE_ACTIVITY_TIME) && infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).isBoolean()) { - sessionMetaData.setOverwriteActivityTime(infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).asBoolean()); + overwriteDevicesActivity = infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).asBoolean(); + sessionMetaData.setOverwriteActivityTime(overwriteDevicesActivity); } + gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId, overwriteDevicesActivity); } } } catch (IOException e) { @@ -1099,7 +1104,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement SparkplugTopic sparkplugTopicNode = validatedSparkplugTopicConnectedNode(connectMessage); if (sparkplugTopicNode != null) { SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(connectMessage.payload().willMessageInBytes()); - sparkplugSessionHandler = new SparkplugNodeSessionHandler(this, deviceSessionCtx, sessionId, sparkplugTopicNode); + sparkplugSessionHandler = new SparkplugNodeSessionHandler(this, deviceSessionCtx, sessionId, true, sparkplugTopicNode); sparkplugSessionHandler.onAttributesTelemetryProto(0, sparkplugBProtoNode, sparkplugTopicNode); sessionMetaData.setOverwriteActivityTime(true); } else { @@ -1354,6 +1359,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { deviceSessionCtx.onDeviceUpdate(sessionInfo, device, deviceProfileOpt); + if (gatewaySessionHandler != null) { + gatewaySessionHandler.onDeviceUpdate(sessionInfo, device, deviceProfileOpt); + } } @Override diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 1cb391b1f4..dc6ba367ab 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -32,17 +32,21 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.adaptor.AdaptorException; -import org.thingsboard.server.common.adaptor.JsonConverter; -import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportApiProtos; @@ -64,6 +68,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -89,6 +94,8 @@ public abstract class AbstractGatewaySessionHandler createWeakMap() { @@ -164,6 +176,12 @@ public abstract class AbstractGatewaySessionHandler transportService.recordActivity(deviceSessionCtx.getSessionInfo())); + } + } + public void onDevicesDisconnect() { devices.forEach(this::deregisterSession); } @@ -221,6 +239,16 @@ public abstract class AbstractGatewaySessionHandler deviceProfileOpt) { + log.trace("[{}][{}] onDeviceUpdate: [{}]", gateway.getTenantId(), gateway.getDeviceId(), device); + if (device.getAdditionalInfo().has(GATEWAY_PROPERTY) + && device.getAdditionalInfo().get(GATEWAY_PROPERTY).asBoolean() + && device.getAdditionalInfo().has(OVERWRITE_ACTIVITY_TIME) + && device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).isBoolean()) { + overwriteDevicesActivity = device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).asBoolean(); + } + } + ListenableFuture onDeviceConnect(String deviceName, String deviceType) { T result = devices.get(deviceName); if (result == null) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index 9ccc88aa82..9aa228afff 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -25,10 +25,10 @@ import java.util.UUID; /** * Created by nickAS21 on 26.12.22 */ -public class GatewaySessionHandler extends AbstractGatewaySessionHandler { +public class GatewaySessionHandler extends AbstractGatewaySessionHandler { - public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { - super(deviceSessionCtx, sessionId); + public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId, boolean overwriteDevicesActivity) { + super(deviceSessionCtx, sessionId, overwriteDevicesActivity); } public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index d4f2364d79..67dda4681a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -73,8 +73,8 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler(); diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java index bd1ddd2d54..9b52d57dc2 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java @@ -15,15 +15,98 @@ */ package org.thingsboard.server.transport.mqtt.session; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.transport.mqtt.MqttTransportContext; + +import java.lang.reflect.Field; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; public class GatewaySessionHandlerTest { + @Mock + private TransportService transportService; + + @Mock + private DeviceSessionCtx deviceSessionCtx; + + @Mock + private MqttTransportContext transportContext; + + private GatewaySessionHandler handler; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(deviceSessionCtx.getSessionId()).thenReturn(UUID.randomUUID()); + doNothing().when(transportService).recordActivity(any()); + when(transportContext.getTransportService()).thenReturn(transportService); + when(deviceSessionCtx.getContext()).thenReturn(transportContext); + handler = new GatewaySessionHandler(deviceSessionCtx, UUID.randomUUID(), true); + when(handler.getNodeId()).thenReturn("nodeId"); + } + + @Test + public void shouldRecordActivityWhenOnGatewayPing() throws Exception { + // Given + ConcurrentHashMap devices = new ConcurrentHashMap<>(); + TransportDeviceInfo deviceInfo = new TransportDeviceInfo(); + deviceInfo.setDeviceId(new DeviceId(UUID.randomUUID())); + deviceInfo.setTenantId(new TenantId(UUID.randomUUID())); + deviceInfo.setCustomerId(new CustomerId(UUID.randomUUID())); + deviceInfo.setDeviceName("device1"); + deviceInfo.setDeviceType("default"); + deviceInfo.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); + deviceInfo.setAdditionalInfo("{\"gateway\": true, \"overwriteDeviceActivity\": true}"); + when(deviceSessionCtx.getDeviceInfo()).thenReturn(deviceInfo); + GatewayDeviceSessionContext gatewayDeviceSessionContext = new GatewayDeviceSessionContext(handler, deviceInfo, null, null, transportService); + devices.put("device1", gatewayDeviceSessionContext); + when(handler.getNodeId()).thenReturn("nodeId"); + Field devicesField = AbstractGatewaySessionHandler.class.getDeclaredField("devices"); + devicesField.setAccessible(true); + devicesField.set(handler, devices); + + // When + handler.onGatewayPing(); + + // Then + verify(transportService).recordActivity(gatewayDeviceSessionContext.getSessionInfo()); + } + + @Test + public void shouldNotRecordActivityWhenNoDevicesOnGatewayPing() throws Exception { + // Given + ConcurrentHashMap devices = new ConcurrentHashMap<>(); + Field devicesField = AbstractGatewaySessionHandler.class.getDeclaredField("devices"); + devicesField.setAccessible(true); + devicesField.set(handler, devices); + + // When + handler.onGatewayPing(); + + // Then + verify(transportService, never()).recordActivity(any()); + } + @Test public void givenGatewaySessionHandler_WhenCreateWeakMap_thenConcurrentReferenceHashMapClass() { GatewaySessionHandler gsh = mock(GatewaySessionHandler.class); From bead94d8327f068bfd1cf0988d310631383194ab Mon Sep 17 00:00:00 2001 From: imbeacon Date: Mon, 8 Apr 2024 11:45:26 +0300 Subject: [PATCH 033/147] Changes due to comments --- .../mqtt/session/AbstractGatewaySessionHandler.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index dc6ba367ab..3be1d632be 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.mqtt.session; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -241,11 +242,12 @@ public abstract class AbstractGatewaySessionHandler deviceProfileOpt) { log.trace("[{}][{}] onDeviceUpdate: [{}]", gateway.getTenantId(), gateway.getDeviceId(), device); - if (device.getAdditionalInfo().has(GATEWAY_PROPERTY) - && device.getAdditionalInfo().get(GATEWAY_PROPERTY).asBoolean() - && device.getAdditionalInfo().has(OVERWRITE_ACTIVITY_TIME) - && device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).isBoolean()) { - overwriteDevicesActivity = device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).asBoolean(); + JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); + if (deviceAdditionalInfo.has(GATEWAY_PROPERTY) + && deviceAdditionalInfo.get(GATEWAY_PROPERTY).asBoolean() + && deviceAdditionalInfo.has(OVERWRITE_ACTIVITY_TIME) + && deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).isBoolean()) { + overwriteDevicesActivity = deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).asBoolean(); } } From a6143f3c8abe1db72c80f373bca069511c1b353e Mon Sep 17 00:00:00 2001 From: imbeacon Date: Mon, 8 Apr 2024 13:05:58 +0300 Subject: [PATCH 034/147] Improvement for update of overwrite activity time --- .../mqtt/session/AbstractGatewaySessionHandler.java | 5 +---- .../transport/service/DefaultTransportService.java | 9 ++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 3be1d632be..b881291756 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -243,10 +243,7 @@ public abstract class AbstractGatewaySessionHandler deviceProfileOpt) { log.trace("[{}][{}] onDeviceUpdate: [{}]", gateway.getTenantId(), gateway.getDeviceId(), device); JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); - if (deviceAdditionalInfo.has(GATEWAY_PROPERTY) - && deviceAdditionalInfo.get(GATEWAY_PROPERTY).asBoolean() - && deviceAdditionalInfo.has(OVERWRITE_ACTIVITY_TIME) - && deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).isBoolean()) { + if (deviceAdditionalInfo.has(GATEWAY_PROPERTY) && deviceAdditionalInfo.has(OVERWRITE_ACTIVITY_TIME)) { overwriteDevicesActivity = deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).asBoolean(); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 4aa989ed25..2f822ccb1a 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.transport.service; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -1047,11 +1048,9 @@ public class DefaultTransportService extends TransportActivityManager implements .setDeviceProfileIdLSB(deviceProfileIdLSB) .setDeviceName(device.getName()) .setDeviceType(device.getType()).build(); - if (device.getAdditionalInfo().has("gateway") - && device.getAdditionalInfo().get("gateway").asBoolean() - && device.getAdditionalInfo().has(OVERWRITE_ACTIVITY_TIME) - && device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).isBoolean()) { - md.setOverwriteActivityTime(device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).asBoolean()); + JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); + if (deviceAdditionalInfo.has("gateway") && deviceAdditionalInfo.has(OVERWRITE_ACTIVITY_TIME)) { + md.setOverwriteActivityTime(deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).asBoolean()); } md.setSessionInfo(newSessionInfo); transportCallbackExecutor.submit(() -> md.getListener().onDeviceUpdate(newSessionInfo, device, Optional.ofNullable(newDeviceProfile))); From 49a76cad5a4e0641634d047db30c8cce3d532742 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 11 Apr 2024 11:53:32 +0300 Subject: [PATCH 035/147] Housekeeper: disabled task types; refactoring --- .../HousekeeperReprocessingService.java | 169 +++++------------- .../housekeeper/HousekeeperService.java | 120 ++++++------- .../stats/HousekeeperStatsService.java | 2 +- .../queue/consumer/QueueConsumerManager.java | 109 +++++++++++ .../src/main/resources/thingsboard.yml | 12 +- .../housekeeper/HousekeeperServiceTest.java | 19 +- .../AlarmsUnassignHousekeeperTask.java | 2 + .../EntitiesDeletionHousekeeperTask.java | 2 + .../LatestTsDeletionHousekeeperTask.java | 2 + .../TsHistoryDeletionHousekeeperTask.java | 2 + .../housekeeper/DefaultHousekeeperClient.java | 14 +- .../queue/housekeeper/HousekeeperConfig.java | 40 +++++ .../common/util/ThingsBoardThreadFactory.java | 5 + 13 files changed, 289 insertions(+), 209 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 7410ec2903..ba762facb1 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -17,23 +17,22 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.housekeeper.HousekeeperConfig; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; -import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.LinkedHashSet; import java.util.List; @@ -41,8 +40,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @TbCoreComponent @@ -50,160 +47,74 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j public class HousekeeperReprocessingService { + private final HousekeeperConfig config; private final HousekeeperService housekeeperService; - private final PartitionService partitionService; - private final TbCoreQueueFactory queueFactory; - private TbQueueProducer> producer; - private TopicPartitionInfo submitTpi; - - @Value("${queue.core.housekeeper.reprocessing-start-delay-sec:300}") - private int startDelay; - @Value("${queue.core.housekeeper.task-reprocessing-delay-sec:3600}") - private int reprocessingDelay; - @Value("${queue.core.housekeeper.max-reprocessing-attempts:10}") - private int maxReprocessingAttempts; - @Value("${queue.core.housekeeper.poll-interval-ms:500}") - private int pollInterval; + private final QueueConsumerManager> consumer; + private final TbQueueProducer> producer; + private final TopicPartitionInfo submitTpi; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-scheduler")); protected AtomicInteger cycle = new AtomicInteger(); - private boolean stopped; - public HousekeeperReprocessingService(@Lazy HousekeeperService housekeeperService, - PartitionService partitionService, + public HousekeeperReprocessingService(HousekeeperConfig config, + @Lazy HousekeeperService housekeeperService, TbCoreQueueFactory queueFactory) { + this.config = config; this.housekeeperService = housekeeperService; - this.partitionService = partitionService; - this.queueFactory = queueFactory; - } - - @PostConstruct - private void init() { - producer = queueFactory.createHousekeeperReprocessingMsgProducer(); - submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); - - scheduler.scheduleWithFixedDelay(() -> { - try { - cycle.incrementAndGet(); - startReprocessing(); - } catch (Throwable e) { - log.error("Unexpected error during reprocessing", e); - } - }, startDelay, reprocessingDelay, TimeUnit.SECONDS); + this.consumer = QueueConsumerManager.>builder() + .name("Housekeeper reprocessing") + .msgPackProcessor(this::processMsgs) + .pollInterval(config.getPollInterval()) + .consumerCreator(queueFactory::createHousekeeperReprocessingMsgConsumer) + .consumerExecutor(consumerExecutor) + .build(); + this.producer = queueFactory.createHousekeeperReprocessingMsgProducer(); + this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); } - public void startReprocessing() { - if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) { - return; - } - - var consumer = queueFactory.createHousekeeperReprocessingMsgConsumer(); - consumer.subscribe(); - consumerExecutor.submit(() -> { - log.info("Starting Housekeeper tasks reprocessing"); - long startTs = System.currentTimeMillis(); - while (!stopped) { - try { - List> msgs = consumer.poll(pollInterval); - if (msgs.isEmpty() || msgs.stream().anyMatch(msg -> msg.getValue().getTask().getTs() >= startTs)) { - // it's not time yet to process the message - if (!consumer.isCommitSupported()) { - // resubmitting consumed messages if committing is not supported (for in-memory queue) - for (TbProtoQueueMsg msg : msgs) { - submit(msg.getKey(), msg.getValue()); - } - } - break; - } - - for (TbProtoQueueMsg msg : msgs) { - log.trace("Reprocessing task: {}", msg); - try { - reprocessTask(msg.getValue()); - } catch (InterruptedException e) { - return; - } catch (Throwable e) { - log.error("Unexpected error during message reprocessing [{}]", msg, e); - submitForReprocessing(msg.getValue(), e); - } - } - consumer.commit(); - } catch (Throwable t) { - if (!consumer.isStopped()) { - log.warn("Failed to process messages from queue", t); - try { - Thread.sleep(pollInterval); - } catch (InterruptedException interruptedException) { - log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); - } - } - } - } - consumer.unsubscribe(); - log.info("Stopped Housekeeper tasks reprocessing"); - }); + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + consumer.subscribe(); // Kafka topic for tasks reprocessing has only 1 partition, so only one TB Core will reprocess tasks + consumer.launch(); } - private void reprocessTask(ToHousekeeperServiceMsg msg) throws Exception { - int attempt = msg.getTask().getAttempt(); - if (attempt > maxReprocessingAttempts) { - if (cycle.get() == 1) { // only reprocessing tasks with exceeded failures on first cycle (after start-up) - log.info("Trying to reprocess task with {} failed attempts: {}", attempt, msg); - } else { - // resubmitting msg to be processed on the next service start - msg = msg.toBuilder() - .setTask(msg.getTask().toBuilder() - .setTs(getReprocessingTs()) - .build()) - .build(); - submit(UUID.randomUUID(), msg); + private void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { + for (TbProtoQueueMsg msg : msgs) { + log.trace("Reprocessing task: {}", msg); + try { + housekeeperService.processTask(msg.getValue()); + } catch (InterruptedException e) { return; + } catch (Throwable e) { + log.error("Unexpected error during message reprocessing [{}]", msg, e); + submitForReprocessing(msg.getValue(), e); } } + consumer.commit(); - housekeeperService.processTask(msg); + Thread.sleep(config.getTaskReprocessingDelay()); } public void submitForReprocessing(ToHousekeeperServiceMsg msg, Throwable error) { HousekeeperTaskProto task = msg.getTask(); - - int attempt = task.getAttempt() + 1; Set errors = new LinkedHashSet<>(task.getErrorsList()); errors.add(StringUtils.truncate(ExceptionUtils.getStackTrace(error), 1024)); msg = msg.toBuilder() .setTask(task.toBuilder() - .setAttempt(attempt) + .setAttempt(task.getAttempt() + 1) .clearErrors().addAllErrors(errors) - .setTs(getReprocessingTs()) .build()) .build(); log.trace("Submitting for reprocessing: {}", msg); - submit(UUID.randomUUID(), msg); // reprocessing topic has single partition, so we don't care about the msg key - - if (task.getAttempt() >= maxReprocessingAttempts) { - log.warn("Failed to process task in {} attempts: {}", task.getAttempt(), msg); - } - } - - private void submit(UUID key, ToHousekeeperServiceMsg msg) { - producer.send(submitTpi, new TbProtoQueueMsg<>(key, msg), null); - } - - private long getReprocessingTs() { - return System.currentTimeMillis() + TimeUnit.SECONDS.toMillis((long) (reprocessingDelay * 0.8)); // *0.8 so that msgs submitted just after finishing reprocessing are processed on the next cycle + producer.send(submitTpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); } @PreDestroy - private void stop() throws Exception { - stopped = true; - scheduler.shutdownNow(); - consumerExecutor.shutdown(); - if (!consumerExecutor.awaitTermination(10, TimeUnit.SECONDS)) { - consumerExecutor.shutdownNow(); - } + private void stop() { + consumer.stop(); + consumerExecutor.shutdownNow(); log.info("Stopped Housekeeper reprocessing service"); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 1a8d07c9da..89953676c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.housekeeper; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -29,11 +28,13 @@ import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.housekeeper.HousekeeperConfig; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; +import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import javax.annotation.PreDestroy; import java.util.List; @@ -54,90 +55,79 @@ public class HousekeeperService { private final Map> taskProcessors; + private final HousekeeperConfig config; private final HousekeeperReprocessingService reprocessingService; private final Optional statsService; private final NotificationRuleProcessor notificationRuleProcessor; - private final TbQueueConsumer> consumer; + private final QueueConsumerManager> consumer; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-consumer")); - private final ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); + private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-task-processor")); - @Value("${queue.core.housekeeper.task-processing-timeout-ms:120000}") - private int taskProcessingTimeout; - @Value("${queue.core.housekeeper.poll-interval-ms:500}") - private int pollInterval; - - private boolean stopped; - - public HousekeeperService(HousekeeperReprocessingService reprocessingService, + public HousekeeperService(HousekeeperConfig config, + HousekeeperReprocessingService reprocessingService, TbCoreQueueFactory queueFactory, Optional statsService, NotificationRuleProcessor notificationRuleProcessor, @Lazy List> taskProcessors) { + this.config = config; this.reprocessingService = reprocessingService; this.statsService = statsService; this.notificationRuleProcessor = notificationRuleProcessor; - this.consumer = queueFactory.createHousekeeperMsgConsumer(); + this.consumer = QueueConsumerManager.>builder() + .name("Housekeeper") + .msgPackProcessor(this::processMsgs) + .pollInterval(config.getPollInterval()) + .consumerCreator(queueFactory::createHousekeeperMsgConsumer) + .consumerExecutor(consumerExecutor) + .build(); this.taskProcessors = taskProcessors.stream().collect(Collectors.toMap(HousekeeperTaskProcessor::getTaskType, p -> p)); } @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void afterStartUp() { consumer.subscribe(); - consumerExecutor.submit(() -> { - while (!stopped && !consumer.isStopped()) { - try { - List> msgs = consumer.poll(pollInterval); - if (msgs.isEmpty()) { - continue; - } + consumer.launch(); + } - for (TbProtoQueueMsg msg : msgs) { - log.trace("Processing task: {}", msg); - try { - processTask(msg.getValue()); - } catch (InterruptedException e) { - return; - } catch (Throwable e) { - log.error("Unexpected error during message processing [{}]", msg, e); - reprocessingService.submitForReprocessing(msg.getValue(), e); - } - } - consumer.commit(); - } catch (Throwable t) { - if (!consumer.isStopped()) { - log.warn("Failed to process messages from queue", t); - try { - Thread.sleep(pollInterval); - } catch (InterruptedException interruptedException) { - log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); - } - } - } + private void processMsgs(List> msgs, TbQueueConsumer> consumer) { + for (TbProtoQueueMsg msg : msgs) { + log.trace("Processing task: {} attempt={}", msg, msg.getValue().getTask().getAttempt()); + try { + processTask(msg.getValue()); + } catch (InterruptedException e) { + return; + } catch (Throwable e) { + log.error("Unexpected error during message processing [{}]", msg, e); + reprocessingService.submitForReprocessing(msg.getValue(), e); } - }); - log.info("Started Housekeeper service"); + } + consumer.commit(); } @SuppressWarnings("unchecked") protected void processTask(ToHousekeeperServiceMsg msg) throws Exception { HousekeeperTask task = JacksonUtil.fromString(msg.getTask().getValue(), HousekeeperTask.class); - HousekeeperTaskProcessor taskProcessor = (HousekeeperTaskProcessor) taskProcessors.get(task.getTaskType()); + HousekeeperTaskType taskType = task.getTaskType(); + if (config.getDisabledTaskTypes().contains(taskType)) { + log.debug("Task type {} is disabled, ignoring {}", taskType, task); + return; + } + HousekeeperTaskProcessor taskProcessor = (HousekeeperTaskProcessor) taskProcessors.get(taskType); if (taskProcessor == null) { - throw new IllegalArgumentException("Unsupported task type " + task.getTaskType()); + throw new IllegalArgumentException("Unsupported task type " + taskType); } if (log.isDebugEnabled()) { log.debug("[{}] {} {}", task.getTenantId(), isNew(msg.getTask()) ? "Processing" : "Reprocessing", task.getDescription()); } try { - Future future = executor.submit(() -> { + Future future = taskExecutor.submit(() -> { taskProcessor.process((T) task); return null; }); - future.get(taskProcessingTimeout, TimeUnit.MILLISECONDS); - - statsService.ifPresent(statsService -> statsService.reportProcessed(task.getTaskType(), msg)); + future.get(config.getTaskProcessingTimeout(), TimeUnit.MILLISECONDS); + statsService.ifPresent(statsService -> statsService.reportProcessed(taskType, msg)); } catch (InterruptedException e) { throw e; } catch (Throwable e) { @@ -145,19 +135,23 @@ public class HousekeeperService { if (e instanceof ExecutionException) { error = e.getCause(); } else if (e instanceof TimeoutException) { - error = new TimeoutException("Timeout after " + taskProcessingTimeout + " seconds"); + error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " seconds"); } log.error("[{}][{}][{}] {} task processing failed, submitting for reprocessing (attempt {}): {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), - task.getTaskType(), msg.getTask().getAttempt(), task, error); - reprocessingService.submitForReprocessing(msg, error); - - statsService.ifPresent(statsService -> statsService.reportFailure(task.getTaskType(), msg)); - notificationRuleProcessor.process(TaskProcessingFailureTrigger.builder() - .task(task) - .error(error) - .attempt(msg.getTask().getAttempt()) - .build()); + taskType, msg.getTask().getAttempt(), task, error); + + if (msg.getTask().getAttempt() < config.getMaxReprocessingAttempts()) { + reprocessingService.submitForReprocessing(msg, error); + } else { + log.error("Failed to process task in {} attempts: {}", msg.getTask().getAttempt(), msg); + notificationRuleProcessor.process(TaskProcessingFailureTrigger.builder() + .task(task) + .error(error) + .attempt(msg.getTask().getAttempt()) + .build()); + } + statsService.ifPresent(statsService -> statsService.reportFailure(taskType, msg)); } } @@ -167,12 +161,8 @@ public class HousekeeperService { @PreDestroy private void stop() throws Exception { - stopped = true; - consumer.unsubscribe(); - consumerExecutor.shutdown(); - if (!consumerExecutor.awaitTermination(10, TimeUnit.SECONDS)) { - consumerExecutor.shutdownNow(); - } + consumer.stop(); + consumerExecutor.shutdownNow(); log.info("Stopped Housekeeper service"); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java index 400bc36479..4944cf9f43 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -105,7 +105,7 @@ public class HousekeeperStatsService { } private StatsCounter register(String statsName, StatsFactory statsFactory) { - StatsCounter counter = statsFactory.createStatsCounter(StatsType.HOUSEKEEPER.getName(), statsName, Map.of("taskType", taskType.name())); + StatsCounter counter = statsFactory.createStatsCounter(StatsType.HOUSEKEEPER.getName(), statsName, "taskType", taskType.name()); counters.add(counter); return counter; } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java new file mode 100644 index 0000000000..e7b3a2f2b7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java @@ -0,0 +1,109 @@ +/** + * 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.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +@Slf4j +public class QueueConsumerManager { + + private final String name; + private final MsgPackProcessor msgPackProcessor; + private final long pollInterval; + private final ExecutorService consumerExecutor; + private final String threadPrefix; + + @Getter + private final TbQueueConsumer consumer; + private volatile boolean stopped; + + @Builder + public QueueConsumerManager(String name, MsgPackProcessor msgPackProcessor, + long pollInterval, Supplier> consumerCreator, + ExecutorService consumerExecutor, String threadPrefix) { + this.name = name; + this.pollInterval = pollInterval; + this.msgPackProcessor = msgPackProcessor; + this.consumerExecutor = consumerExecutor; + this.threadPrefix = threadPrefix; + this.consumer = consumerCreator.get(); + } + + public void subscribe() { + consumer.subscribe(); + } + + public void subscribe(Set partitions) { + consumer.subscribe(partitions); + } + + public void launch() { + log.info("[{}] Launching consumer", name); + consumerExecutor.submit(() -> { + if (threadPrefix != null) { + ThingsBoardThreadFactory.addThreadNamePrefix(threadPrefix); + } + try { + consumerLoop(consumer); + } catch (Throwable e) { + log.error("Failure in consumer loop", e); + } + log.info("[{}] Consumer stopped", name); + }); + } + + private void consumerLoop(TbQueueConsumer consumer) { + while (!stopped && !consumer.isStopped()) { + try { + List msgs = consumer.poll(pollInterval); + if (msgs.isEmpty()) { + continue; + } + msgPackProcessor.process(msgs, consumer); + } catch (Exception e) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", e); + try { + Thread.sleep(pollInterval); + } catch (InterruptedException interruptedException) { + log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); + } + } + } + } + } + + public void stop() { + log.debug("[{}] Stopping consumer", name); + stopped = true; + consumer.unsubscribe(); + } + + public interface MsgPackProcessor { + void process(List msgs, TbQueueConsumer consumer) throws Exception; + } +} \ No newline at end of file diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 938ef7986e..cbc4e79dd3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1638,11 +1638,13 @@ queue: poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" # Timeout in milliseconds for task processing. Tasks that fail to finish on time will be submitted for reprocessing task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" - # Delay in seconds for starting tasks reprocessing after start-up - reprocessing-start-delay-sec: "${TB_HOUSEKEEPER_REPROCESSING_START_DELAY_SEC:300}" - # Period in seconds of tasks reprocessing - task-reprocessing-delay-sec: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_SEC:3600}" - # Maximum amount of task reprocessing attempts. After exceeding, the task will be ignored until the next service start-up + # Comma-separated list of task types that shouldn't be processed. Available task types: + # DELETE_ATTRIBUTES, DELETE_TELEMETRY (both DELETE_LATEST_TS and DELETE_TS_HISTORY will be disabled), + # DELETE_LATEST_TS, DELETE_TS_HISTORY, DELETE_EVENTS, DELETE_ALARMS, UNASSIGN_ALARMS + disabled-task-types: "${TB_HOUSEKEEPER_DISABLED_TASK_TYPES:}" + # Delay in milliseconds between tasks reprocessing + task-reprocessing-delay-ms: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_MS:5000}" + # Maximum amount of task reprocessing attempts. After exceeding, the task will be dropped max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:10}" stats: # Enable/disable statistics for Housekeeper diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 44891c2b7f..82c488f0ab 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.housekeeper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; +import net.bytebuddy.implementation.bytecode.Throw; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -104,8 +105,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest @TestPropertySource(properties = { "transport.http.enabled=true", - "queue.core.housekeeper.reprocessing-start-delay-sec=1", - "queue.core.housekeeper.task-reprocessing-delay-sec=2", + "queue.core.housekeeper.task-reprocessing-delay-ms=2000", "queue.core.housekeeper.poll-interval-ms=1000", "queue.core.housekeeper.max-reprocessing-attempts=5" }) @@ -258,7 +258,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } @Test - public void whenTaskProcessingFails_thenReprocessUntilSuccessful() throws Exception { + public void whenTaskProcessingFails_thenReprocess() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); @@ -267,8 +267,8 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); - int attempts = 3; - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + int attempts = 2; + await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, 0); for (int i = 1; i <= attempts; i++) { int attempt = i; @@ -279,7 +279,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { }); assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); - doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); // fixing the code + doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(getTimeseriesHistory(device.getId())).isEmpty(); }); @@ -314,7 +314,12 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } private void verifyTaskProcessing(EntityId entityId, HousekeeperTaskType taskType, int expectedAttempt) throws Exception { - verify(housekeeperService).processTask(argThat(getTaskMatcher(entityId, taskType, task -> task.getAttempt() == expectedAttempt))); + try { + verify(housekeeperService).processTask(argThat(getTaskMatcher(entityId, taskType, task -> task.getAttempt() == expectedAttempt))); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } } private ArgumentMatcher getTaskMatcher(EntityId entityId, HousekeeperTaskType taskType, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java index af4e746987..5911418dc3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java @@ -19,9 +19,11 @@ import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.User; @Data +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java index 515703991c..4156200758 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java @@ -20,10 +20,12 @@ import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; @Data +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java index 59e815bbfa..cef43756e0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java @@ -19,10 +19,12 @@ import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @Data +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class LatestTsDeletionHousekeeperTask extends HousekeeperTask { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java index 1656626e6b..80b8edc345 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java @@ -19,10 +19,12 @@ import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @Data +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java index e9737bf90c..3b06f0cf3e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -33,11 +34,14 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @Slf4j public class DefaultHousekeeperClient implements HousekeeperClient { + private final HousekeeperConfig config; private final TbQueueProducer> producer; private final TopicPartitionInfo submitTpi; private final TbQueueCallback submitCallback; - public DefaultHousekeeperClient(TbQueueProducerProvider producerProvider) { + public DefaultHousekeeperClient(HousekeeperConfig config, + TbQueueProducerProvider producerProvider) { + this.config = config; this.producer = producerProvider.getHousekeeperMsgProducer(); this.submitTpi = TopicPartitionInfo.builder().topic(producer.getDefaultTopic()).build(); this.submitCallback = new TbQueueCallback() { @@ -55,7 +59,13 @@ public class DefaultHousekeeperClient implements HousekeeperClient { @Override public void submitTask(HousekeeperTask task) { - log.debug("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task.getTaskType()); + HousekeeperTaskType taskType = task.getTaskType(); + if (config.getDisabledTaskTypes().contains(taskType)) { + log.trace("Task type {} is disabled, ignoring {}", taskType, task); + return; + } + + log.debug("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), taskType); /* * using msg key as entity id so that msgs related to certain entity are pushed to same partition, * e.g. on tenant deletion (entity id is tenant id), we need to clean up tenant entities in certain order diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java new file mode 100644 index 0000000000..b03861a7ee --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java @@ -0,0 +1,40 @@ +/** + * 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.queue.housekeeper; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; + +import java.util.Set; + +@Component +@Getter +public class HousekeeperConfig { + + @Value("${queue.core.housekeeper.disabled-task-types:}") + private Set disabledTaskTypes; + @Value("${queue.core.housekeeper.task-processing-timeout-ms:120000}") + private int taskProcessingTimeout; + @Value("${queue.core.housekeeper.poll-interval-ms:500}") + private int pollInterval; + @Value("${queue.core.housekeeper.task-reprocessing-delay-ms:5000}") + private int taskReprocessingDelay; + @Value("${queue.core.housekeeper.max-reprocessing-attempts:10}") + private int maxReprocessingAttempts; + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java index de097cfd6c..4d0876bf8e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java @@ -51,6 +51,11 @@ public class ThingsBoardThreadFactory implements ThreadFactory { Thread.currentThread().setName(name); } + public static void addThreadNamePrefix(String prefix) { + String name = Thread.currentThread().getName(); + name = prefix + "-" + name; + Thread.currentThread().setName(name); + } @Override public Thread newThread(Runnable r) { From feb629f2c28f78739b51cf9a76d1217032bebf30 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 11 Apr 2024 14:07:35 +0300 Subject: [PATCH 036/147] Alarms deletion in batches with id offset --- .../housekeeper/HousekeeperService.java | 2 +- .../AlarmsDeletionTaskProcessor.java | 39 +++++++++--- .../housekeeper/HousekeeperServiceTest.java | 60 +++++++++++++------ .../server/dao/alarm/AlarmService.java | 2 - .../AlarmsDeletionHousekeeperTask.java | 46 ++++++++++++++ .../data/housekeeper/HousekeeperTask.java | 3 +- .../server/common/data/page/PageLink.java | 6 ++ .../server/common/data/page/SortOrder.java | 4 ++ .../org/thingsboard/server/dao/DaoUtil.java | 35 ++++++++++- .../server/dao/alarm/BaseAlarmService.java | 16 ----- .../server/dao/sql/alarm/AlarmRepository.java | 11 +++- .../server/dao/sql/alarm/JpaAlarmDao.java | 2 +- 12 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 89953676c3..b16c1b0de9 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -92,7 +92,7 @@ public class HousekeeperService { private void processMsgs(List> msgs, TbQueueConsumer> consumer) { for (TbProtoQueueMsg msg : msgs) { - log.trace("Processing task: {} attempt={}", msg, msg.getValue().getTask().getAttempt()); + log.trace("Processing task: {}", msg); try { processTask(msg.getValue()); } catch (InterruptedException e) { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index 42e77d9c8e..d775cdc4db 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -19,26 +19,49 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.AlarmsDeletionHousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmService; +import java.util.UUID; +import java.util.stream.Collectors; + @Component @RequiredArgsConstructor @Slf4j -public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor { +public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor { private final AlarmService alarmService; @Override - public void process(HousekeeperTask task) throws Exception { - EntityType entityType = task.getEntityId().getEntityType(); + public void process(AlarmsDeletionHousekeeperTask task) throws Exception { + EntityId entityId = task.getEntityId(); + EntityType entityType = entityId.getEntityType(); + TenantId tenantId = task.getTenantId(); + if (entityType == EntityType.DEVICE || entityType == EntityType.ASSET) { - int count = alarmService.deleteAlarmsByOriginatorId(task.getTenantId(), task.getEntityId()); - log.debug("[{}][{}][{}] Deleted {} alarms", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), count); + if (task.getAlarms() == null) { + DaoUtil.iterateWithKeyOffset(pageLink -> { + return alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, pageLink); + }, 128, alarms -> { + housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream() + .map(UUIDBased::getId).collect(Collectors.toList()))); + log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); + }); + } else { + for (UUID alarmId : task.getAlarms()) { + alarmService.delAlarm(tenantId, new AlarmId(alarmId)); + } + log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size()); + } } else { - int count = alarmService.deleteEntityAlarmRecords(task.getTenantId(), task.getEntityId()); - log.debug("[{}][{}][{}] Deleted {} entity alarms", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), count); + int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId); + log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count); } } diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 82c488f0ab..26062810a1 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -17,25 +17,24 @@ package org.thingsboard.server.service.housekeeper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; -import net.bytebuddy.implementation.bytecode.Throw; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.asset.Asset; @@ -79,7 +78,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateDao; import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; -import org.thingsboard.server.service.housekeeper.processor.TelemetryDeletionTaskProcessor; import org.thingsboard.server.service.housekeeper.processor.TsHistoryDeletionTaskProcessor; import java.util.Arrays; @@ -156,7 +154,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenDeviceIsDeleted_thenCleanUpRelatedData() throws Exception { - Device device = createDevice("wekfwepf", "wekfwepf"); + Device device = createDevice("test", "test"); createRelatedData(device.getId()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); @@ -187,7 +185,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenUserIsDeleted_thenCleanUpRelatedData() throws Exception { - Device device = createDevice("vneoruvhwe", "vneoruvhwe"); + Device device = createDevice("test", "test"); UserId userId = customerUserId; createRelatedData(userId); Alarm alarm = Alarm.builder() @@ -210,6 +208,20 @@ public class HousekeeperServiceTest extends AbstractControllerTest { }); } + @Test + public void whenDeviceIsDeleted_thenDeleteAllAlarms() throws Exception { + Device device = createDevice("test", "test"); + for (int i = 1; i <= 1000; i++) { + createAlarm(device.getId()); + } + + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoAlarms(device.getId()); + }); + } + @Test public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception { loginDifferentTenant(); @@ -220,7 +232,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { createRelatedData(differentTenantCustomerId); loginDifferentTenant(); - Device device = createDevice("oi324rujoi", "oi324rujoi"); + Device device = createDevice("test", "test"); createRelatedData(device.getId()); Asset asset = createAsset(); @@ -258,11 +270,12 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } @Test + @Ignore // FIXME !!! public void whenTaskProcessingFails_thenReprocess() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); - Device device = createDevice("vep9ruv32", "vep9ruv32"); + Device device = createDevice("test", "test"); createRelatedData(device.getId()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); @@ -286,11 +299,12 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } @Test + @Ignore // FIXME !!! public void whenReprocessingAttemptsExceeded_thenReprocessOnNextStartUp() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); - Device device = createDevice("woeifjiowejf", "woeifjiowejf"); + Device device = createDevice("test", "test"); createRelatedData(device.getId()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); @@ -332,32 +346,31 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private void createRelatedData(EntityId entityId) throws Exception { createTelemetry(entityId); - for (AttributeScope scope : List.of(AttributeScope.SERVER_SCOPE, AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE)) { + for (AttributeScope scope : AttributeScope.values()) { createAttribute(entityId, scope, scope + ATTRIBUTE_KEY); } createEvent(entityId); } private void verifyNoRelatedData(EntityId entityId) throws Exception { - List expectedTaskTypes = List.of(HousekeeperTaskType.DELETE_TELEMETRY, HousekeeperTaskType.DELETE_ATTRIBUTES, HousekeeperTaskType.DELETE_EVENTS, HousekeeperTaskType.DELETE_ALARMS); - for (HousekeeperTaskType taskType : expectedTaskTypes) { - verify(housekeeperClient).submitTask(argThat(task -> task.getTaskType() == taskType && task.getEntityId().equals(entityId))); - } assertThat(entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()).findEntity(tenantId, entityId)).isEmpty(); assertThat(getLatestTelemetry(entityId)).isNull(); assertThat(getTimeseriesHistory(entityId)).isEmpty(); - for (String scope : List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE, DataConstants.CLIENT_SCOPE)) { + for (AttributeScope scope : AttributeScope.values()) { assertThat(attributesService.findAll(tenantId, entityId, scope).get()).isEmpty(); } assertThat(getEvents(entityId)).isEmpty(); assertThat(alarmDao.findEntityAlarmRecordsByEntityId(tenantId, entityId)).isEmpty(); - assertThat(alarmService.findAlarms(tenantId, AlarmQuery.builder().pageLink(new TimePageLink(100)).build()).getData()) - .filteredOn(alarm -> alarm.getOriginator().equals(entityId)).isEmpty(); + verifyNoAlarms(entityId); assertThat(relationService.findByTo(tenantId, entityId, RelationTypeGroup.COMMON)).isEmpty(); assertThat(relationService.findByFrom(tenantId, entityId, RelationTypeGroup.COMMON)).isEmpty(); } + private void verifyNoAlarms(EntityId entityId) { + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, new PageLink(1)).getData()).isEmpty(); + } + private void createAttribute(EntityId entityId, AttributeScope scope, String key) throws Exception { attributesService.save(tenantId, entityId, scope, new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, KV_VALUE))).get(); } @@ -397,8 +410,17 @@ public class HousekeeperServiceTest extends AbstractControllerTest { List entityAlarms = alarmDao.findEntityAlarmRecords(tenantId, alarm.getId()); assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(deviceId) && entityAlarm.getAlarmType().equals(alarm.getType())); assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(propagatedEntityId) && entityAlarm.getAlarmType().equals(alarm.getType())); - assertThat(alarmService.findAlarms(tenantId, AlarmQuery.builder().pageLink(new TimePageLink(100)).build()).getData()) - .filteredOn(a -> a.getOriginator().equals(deviceId)).isNotEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, new PageLink(1)).getData()).isNotEmpty(); + } + + private void createAlarm(DeviceId deviceId) { + Alarm alarm = doPost("/api/alarm", Alarm.builder() + .tenantId(tenantId) + .originator(deviceId) + .severity(AlarmSeverity.CRITICAL) + .type("test alarm for " + deviceId + " " + RandomStringUtils.randomAlphabetic(10)) + .build(), Alarm.class); + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, new PageLink(1)).getData()).isNotEmpty(); } private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 57bee7ba1a..32ce3be62c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -81,8 +81,6 @@ public interface AlarmService extends EntityDaoService { void delAlarmTypes(TenantId tenantId, Set types); - int deleteAlarmsByOriginatorId(TenantId tenantId, EntityId entityId); - // Other API Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java new file mode 100644 index 0000000000..ba9627565d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java @@ -0,0 +1,46 @@ +/** + * 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.housekeeper; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; +import java.util.UUID; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AlarmsDeletionHousekeeperTask extends HousekeeperTask { + + private List alarms; + + public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, List alarms) { + super(tenantId, entityId, HousekeeperTaskType.DELETE_ALARMS); + this.alarms = alarms; + } + + public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) { + super(tenantId, entityId, HousekeeperTaskType.DELETE_ALARMS); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index 08862d26be..344edc3f0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -37,6 +37,7 @@ import java.io.Serializable; @Type(name = "DELETE_TS_HISTORY", value = TsHistoryDeletionHousekeeperTask.class), @Type(name = "DELETE_LATEST_TS", value = LatestTsDeletionHousekeeperTask.class), @Type(name = "DELETE_ENTITIES", value = EntitiesDeletionHousekeeperTask.class), + @Type(name = "DELETE_ALARMS", value = AlarmsDeletionHousekeeperTask.class), @Type(name = "UNASSIGN_ALARMS", value = AlarmsUnassignHousekeeperTask.class) }) @Data @@ -81,7 +82,7 @@ public class HousekeeperTask implements Serializable { } public static HousekeeperTask deleteAlarms(TenantId tenantId, EntityId entityId) { - return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ALARMS); + return new AlarmsDeletionHousekeeperTask(tenantId, entityId); } public static HousekeeperTask deleteEntities(TenantId tenantId, EntityType entityType) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java index 042a9b1896..8d806b7006 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java @@ -16,15 +16,20 @@ package org.thingsboard.server.common.data.page; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import org.springframework.data.domain.Sort; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; @Data +@AllArgsConstructor +@Builder public class PageLink { protected static final String DEFAULT_SORT_PROPERTY = "id"; @@ -34,6 +39,7 @@ public class PageLink { private final int pageSize; private final int page; private final SortOrder sortOrder; + private UUID idOffset; public PageLink(PageLink pageLink) { this.pageSize = pageLink.getPageSize(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java index 5cafed6df2..539dd1cb40 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java @@ -32,6 +32,10 @@ public class SortOrder { this.direction = direction; } + public static SortOrder of(String property, Direction direction) { + return new SortOrder(property, direction); + } + public static enum Direction { ASC, DESC } diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 865306c73b..a0941705eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -18,10 +18,12 @@ package org.thingsboard.server.dao; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; @@ -51,8 +53,17 @@ public abstract class DaoUtil { return new PageData<>(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); } - public static PageData pageToPageData(Page page) { - return new PageData<>(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); + public static PageData pageToPageData(Slice slice) { + int totalPages; + long totalElements; + if (slice instanceof Page page) { + totalPages = page.getTotalPages(); + totalElements = page.getTotalElements(); + } else { + totalPages = 0; + totalElements = 0; + } + return new PageData<>(slice.getContent(), totalPages, totalElements, slice.hasNext()); } public static Pageable toPageable(PageLink pageLink) { @@ -158,6 +169,26 @@ public abstract class DaoUtil { } while (hasNextBatch); } + public static void iterateWithKeyOffset(Function> findFunction, int pageSize, Consumer> processor) { + List data; + UUID last = null; + while (true) { + PageLink pageLink = PageLink.builder() + .pageSize(pageSize) + .sortOrder(SortOrder.of("id", SortOrder.Direction.ASC)) + .idOffset(last) + .build(); + data = findFunction.apply(pageLink).getData(); + + if (!data.isEmpty()) { + processor.accept(data); + last = data.get(data.size() - 1).getId(); + } else { + break; + } + } + } + public static String getStringId(UUIDBased id) { if (id != null) { return id.toString(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index f9ddc212c8..33b2d5a4c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -51,7 +51,6 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -215,21 +214,6 @@ public class BaseAlarmService extends AbstractCachedEntityService alarms; - int count = 0; - do { - alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, pageLink); - for (AlarmId alarmId : alarms.getData()) { - delAlarm(tenantId, alarmId); - count++; - } - } while (alarms.hasNext()); - return count; - } - private List createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException { Set propagatedEntitiesSet = new LinkedHashSet<>(); propagatedEntitiesSet.add(alarm.getOriginator()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 83827973ba..ff9a2ed7d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -17,13 +17,13 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -333,8 +333,13 @@ public interface AlarmRepository extends JpaRepository { @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId") Page findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId, @Param("assigneeId") UUID assigneeId, Pageable pageable); - @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.originatorId = :originatorId") - Page findAlarmIdsByOriginatorId(@Param("tenantId") UUID tenantId, @Param("originatorId") UUID originatorId, Pageable pageable); + // using Slice so that count query is not executed + @Query(value = "SELECT id FROM alarm WHERE tenant_id = :tenantId AND originator_id = :originatorId " + + "AND (cast(:idOffset as uuid) IS NULL OR id > cast(:idOffset as uuid))", nativeQuery = true) + Slice findAlarmIdsByOriginatorId(@Param("tenantId") UUID tenantId, + @Param("originatorId") UUID originatorId, + @Param("idOffset") UUID idOffset, + Pageable pageable); @Query(value = "SELECT create_or_update_active_alarm(:t_id, :c_id, :a_id, :a_created_ts, :a_o_id, :a_o_type, :a_type, :a_severity, " + ":a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index c326c6452b..6c1118cdba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -298,7 +298,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink) { - return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByOriginatorId(tenantId.getId(), originatorId.getId(), DaoUtil.toPageable(pageLink))) + return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByOriginatorId(tenantId.getId(), originatorId.getId(), pageLink.getIdOffset(), DaoUtil.toPageable(pageLink))) .mapData(AlarmId::new); } From 9064bfaa7663bcde45d4cb121cb2c5bf886f5ea9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 11 Apr 2024 17:18:47 +0300 Subject: [PATCH 037/147] Delete tenant entities in batches with id offset --- .../AlarmsDeletionTaskProcessor.java | 15 +++-- .../EntitiesDeletionTaskProcessor.java | 41 +++++++++++- .../housekeeper/HousekeeperServiceTest.java | 6 +- .../server/dao/alarm/AlarmService.java | 3 +- .../server/common/data/EntityType.java | 67 ++++++++++--------- .../AlarmsDeletionHousekeeperTask.java | 8 +-- .../EntitiesDeletionHousekeeperTask.java | 9 +++ .../server/common/data/page/PageLink.java | 14 ++-- .../java/org/thingsboard/server/dao/Dao.java | 2 + .../org/thingsboard/server/dao/DaoUtil.java | 21 ------ .../server/dao/alarm/AlarmDao.java | 2 +- .../server/dao/alarm/BaseAlarmService.java | 6 +- .../server/dao/entity/EntityDaoRegistry.java | 49 ++++++++++++++ .../server/dao/sql/JpaAbstractDao.java | 19 ++++++ .../server/dao/sql/alarm/JpaAlarmDao.java | 4 +- 15 files changed, 183 insertions(+), 83 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index d775cdc4db..c4834df71d 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -25,9 +25,9 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmService; +import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -46,13 +46,18 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor { - return alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, pageLink); - }, 128, alarms -> { + AlarmId last = null; + while (true) { + List alarms = alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, entityId, last, 128); + if (alarms.isEmpty()) { + break; + } + housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream() .map(UUIDBased::getId).collect(Collectors.toList()))); + last = alarms.get(alarms.size() - 1); log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); - }); + } } else { for (UUID alarmId : task.getAlarms()) { alarmService.delAlarm(tenantId, new AlarmId(alarmId)); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java index 95d45a49be..fb01fe4605 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java @@ -16,22 +16,59 @@ package org.thingsboard.server.service.housekeeper.processor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.housekeeper.EntitiesDeletionHousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.entity.EntityDaoRegistry; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; +import java.util.List; +import java.util.UUID; + @Component @RequiredArgsConstructor +@Slf4j public class EntitiesDeletionTaskProcessor extends HousekeeperTaskProcessor { + private final EntityDaoRegistry entityDaoRegistry; private final EntityServiceRegistry entityServiceRegistry; @Override public void process(EntitiesDeletionHousekeeperTask task) throws Exception { - EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(task.getEntityType()); - entityService.deleteByTenantId(task.getTenantId()); + EntityType entityType = task.getEntityType(); + TenantId tenantId = task.getTenantId(); + Dao entityDao = entityDaoRegistry.getDao(entityType); + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); + + if (task.getEntities() == null) { + UUID last = null; + while (true) { + List entities = entityDao.findIdsByTenantIdAndIdOffset(tenantId, last, 128); + if (entities.isEmpty()) { + break; + } + + housekeeperClient.submitTask(new EntitiesDeletionHousekeeperTask(tenantId, entityType, task.getEntities())); + last = entities.get(entities.size() - 1); + log.debug("[{}] Submitted task for deleting {} {}s", tenantId, entities.size(), entityType.getNormalName().toLowerCase()); + } + } else { + for (UUID entityUuid : task.getEntities()) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); +// Optional> entity = entityService.findEntity(tenantId, entityId); +// if (entity.isEmpty()) { +// continue; +// } + entityService.deleteEntity(tenantId, entityId); + } + } } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 26062810a1..efbfbffbb9 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -368,7 +368,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } private void verifyNoAlarms(EntityId entityId) { - assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, new PageLink(1)).getData()).isEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, entityId, null, 10)).isEmpty(); } private void createAttribute(EntityId entityId, AttributeScope scope, String key) throws Exception { @@ -410,7 +410,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { List entityAlarms = alarmDao.findEntityAlarmRecords(tenantId, alarm.getId()); assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(deviceId) && entityAlarm.getAlarmType().equals(alarm.getType())); assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(propagatedEntityId) && entityAlarm.getAlarmType().equals(alarm.getType())); - assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, new PageLink(1)).getData()).isNotEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, deviceId, null, 10)).isNotEmpty(); } private void createAlarm(DeviceId deviceId) { @@ -420,7 +420,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { .severity(AlarmSeverity.CRITICAL) .type("test alarm for " + deviceId + " " + RandomStringUtils.randomAlphabetic(10)) .build(), Alarm.class); - assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, new PageLink(1)).getData()).isNotEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, deviceId, null, 10)).isNotEmpty(); } private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 32ce3be62c..ee584ff856 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; +import java.util.List; import java.util.Set; @@ -106,7 +107,7 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); - PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink); + List findAlarmIdsByOriginatorIdAndIdOffset(TenantId tenantId, EntityId originatorId, AlarmId idOffset, int limit); int deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index bb43bf4d11..6748aebb50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -20,7 +20,6 @@ import org.apache.commons.lang3.StringUtils; import java.util.EnumSet; import java.util.List; -import java.util.stream.Collectors; /** * @author Andrew Shvayka @@ -28,51 +27,57 @@ import java.util.stream.Collectors; public enum EntityType { TENANT(1), CUSTOMER(2), - USER(3), + USER(3, "tb_user"), DASHBOARD(4), ASSET(5), DEVICE(6), - ALARM (7), - RULE_CHAIN (11), - RULE_NODE (12), - - ENTITY_VIEW (15) { + ALARM(7), + RULE_CHAIN(11), + RULE_NODE(12), + ENTITY_VIEW(15) { // backward compatibility for TbOriginatorTypeSwitchNode to return correct rule node connection. @Override - public String getNormalName () { + public String getNormalName() { return "Entity View"; } }, - WIDGETS_BUNDLE (16), - WIDGET_TYPE (17), - TENANT_PROFILE (20), - DEVICE_PROFILE (21), - ASSET_PROFILE (22), - API_USAGE_STATE (23), - TB_RESOURCE (24), - OTA_PACKAGE (25), - EDGE (26), - RPC (27), - QUEUE (28), - NOTIFICATION_TARGET (29), - NOTIFICATION_TEMPLATE (30), - NOTIFICATION_REQUEST (31), - NOTIFICATION (32), - NOTIFICATION_RULE (33), + WIDGETS_BUNDLE(16), + WIDGET_TYPE(17), + TENANT_PROFILE(20), + DEVICE_PROFILE(21), + ASSET_PROFILE(22), + API_USAGE_STATE(23), + TB_RESOURCE(24, "resource"), + OTA_PACKAGE(25), + EDGE(26), + RPC(27), + QUEUE(28), + NOTIFICATION_TARGET(29), + NOTIFICATION_TEMPLATE(30), + NOTIFICATION_REQUEST(31), + NOTIFICATION(32), + NOTIFICATION_RULE(33), QUEUE_STATS(34); @Getter private final int protoNumber; // Corresponds to EntityTypeProto + @Getter + private final String tableName; + @Getter + private final String normalName = StringUtils.capitalize(StringUtils.removeStart(name(), "TB_") + .toLowerCase().replaceAll("_", " ")); + + public static final List NORMAL_NAMES = EnumSet.allOf(EntityType.class).stream() + .map(EntityType::getNormalName).toList(); - private EntityType(int protoNumber) { + EntityType(int protoNumber) { this.protoNumber = protoNumber; + this.tableName = name().toLowerCase(); } - public static final List NORMAL_NAMES = EnumSet.allOf(EntityType.class).stream() - .map(EntityType::getNormalName).collect(Collectors.toUnmodifiableList()); - - @Getter - private final String normalName = StringUtils.capitalize(StringUtils.removeStart(name(), "TB_") - .toLowerCase().replaceAll("_", " ")); + EntityType(int protoNumber, String tableName) { + this.protoNumber = protoNumber; + this.tableName = tableName; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java index ba9627565d..9bd8f29bb4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java @@ -34,13 +34,13 @@ public class AlarmsDeletionHousekeeperTask extends HousekeeperTask { private List alarms; - public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, List alarms) { - super(tenantId, entityId, HousekeeperTaskType.DELETE_ALARMS); - this.alarms = alarms; + public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) { + this(tenantId, entityId, null); } - public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) { + public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, List alarms) { super(tenantId, entityId, HousekeeperTaskType.DELETE_ALARMS); + this.alarms = alarms; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java index 4156200758..e29c3d3c51 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java @@ -24,6 +24,9 @@ import lombok.ToString; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; +import java.util.List; +import java.util.UUID; + @Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @@ -31,10 +34,16 @@ import org.thingsboard.server.common.data.id.TenantId; public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { private EntityType entityType; + private List entities; protected EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { + this(tenantId, entityType, null); + } + + public EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType, List entities) { super(tenantId, tenantId, HousekeeperTaskType.DELETE_ENTITIES); this.entityType = entityType; + this.entities = entities; } @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java index 8d806b7006..d6a3b5ad7d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java @@ -16,20 +16,15 @@ package org.thingsboard.server.common.data.page; import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import org.springframework.data.domain.Sort; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.stream.Collectors; @Data -@AllArgsConstructor -@Builder public class PageLink { protected static final String DEFAULT_SORT_PROPERTY = "id"; @@ -39,7 +34,6 @@ public class PageLink { private final int pageSize; private final int page; private final SortOrder sortOrder; - private UUID idOffset; public PageLink(PageLink pageLink) { this.pageSize = pageLink.getPageSize(); @@ -69,10 +63,10 @@ public class PageLink { @JsonIgnore public PageLink nextPageLink() { - return new PageLink(this.pageSize, this.page+1, this.textSearch, this.sortOrder); + return new PageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder); } - public Sort toSort(SortOrder sortOrder, Map columnMap, boolean addDefaultSorting) { + public Sort toSort(SortOrder sortOrder, Map columnMap, boolean addDefaultSorting) { if (sortOrder == null) { return DEFAULT_SORT; } else { @@ -80,7 +74,7 @@ public class PageLink { } } - public Sort toSort(List sortOrders, Map columnMap, boolean addDefaultSorting) { + public Sort toSort(List sortOrders, Map columnMap, boolean addDefaultSorting) { if (addDefaultSorting && !isDefaultSortOrderAvailable(sortOrders)) { sortOrders = new ArrayList<>(sortOrders); sortOrders.add(new SortOrder(DEFAULT_SORT_PROPERTY, SortOrder.Direction.ASC)); @@ -88,7 +82,7 @@ public class PageLink { return Sort.by(sortOrders.stream().map(s -> toSortOrder(s, columnMap)).collect(Collectors.toList())); } - private Sort.Order toSortOrder(SortOrder sortOrder, Map columnMap) { + private Sort.Order toSortOrder(SortOrder sortOrder, Map columnMap) { String property = sortOrder.getProperty(); if (columnMap.containsKey(property)) { property = columnMap.get(property); diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java index 8d408945fa..e7290cdced 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/Dao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java @@ -43,6 +43,8 @@ public interface Dao { void removeAllByIds(Collection ids); + List findIdsByTenantIdAndIdOffset(TenantId tenantId, UUID idOffset, int limit); + default EntityType getEntityType() { return null; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index a0941705eb..01a72ea58c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -23,7 +23,6 @@ import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; @@ -169,26 +168,6 @@ public abstract class DaoUtil { } while (hasNextBatch); } - public static void iterateWithKeyOffset(Function> findFunction, int pageSize, Consumer> processor) { - List data; - UUID last = null; - while (true) { - PageLink pageLink = PageLink.builder() - .pageSize(pageSize) - .sortOrder(SortOrder.of("id", SortOrder.Direction.ASC)) - .idOffset(last) - .build(); - data = findFunction.apply(pageLink).getData(); - - if (!data.isEmpty()) { - processor.accept(data); - last = data.get(data.size() - 1).getId(); - } else { - break; - } - } - } - public static String getStringId(UUIDBased id) { if (id != null) { return id.toString(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 365a5ac2e0..03038b3bfe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -80,7 +80,7 @@ public interface AlarmDao extends Dao { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink); - PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink); + PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, AlarmId idOffset, PageLink pageLink); void createEntityAlarmRecord(EntityAlarm entityAlarm); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 33b2d5a4c3..9d0940a605 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -313,9 +313,9 @@ public class BaseAlarmService extends AbstractCachedEntityService findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink) { - log.trace("[{}] Executing findAlarmsByOriginatorId [{}]", tenantId, originatorId); - return alarmDao.findAlarmIdsByOriginatorId(tenantId, originatorId, pageLink); + public List findAlarmIdsByOriginatorIdAndIdOffset(TenantId tenantId, EntityId originatorId, AlarmId idOffset, int limit) { + log.trace("[{}] Executing findAlarmIdsByOriginatorIdAndIdOffset [{}][{}]", tenantId, originatorId, idOffset); + return alarmDao.findAlarmIdsByOriginatorId(tenantId, originatorId, idOffset, new PageLink(limit)).getData(); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java new file mode 100644 index 0000000000..261454966c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -0,0 +1,49 @@ +/** + * 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.dao.entity; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class EntityDaoRegistry { + + private final Map> daos; + + private EntityDaoRegistry(List> daos) { + this.daos = daos.stream().filter(dao -> dao.getEntityType() != null) + .collect(Collectors.toMap(Dao::getEntityType, dao -> dao)); + } + + @SuppressWarnings("unchecked") + public Dao getDao(EntityType entityType) { + Dao dao = (Dao) daos.get(entityType); + if (dao == null) { + throw new IllegalArgumentException("Missing dao for entity type " + entityType); + } + return dao; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index fdb96195fb..6bfd74ac87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -19,7 +19,9 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; @@ -41,6 +43,9 @@ public abstract class JpaAbstractDao, D> extends JpaAbstractDaoListeningExecutorService implements Dao { + @Autowired + protected JdbcTemplate jdbcTemplate; + protected abstract Class getEntityClass(); protected abstract JpaRepository getRepository(); @@ -117,6 +122,20 @@ public abstract class JpaAbstractDao, D> ids.forEach(repository::deleteById); } + @Override + public List findIdsByTenantIdAndIdOffset(TenantId tenantId, UUID idOffset, int limit) { + String tableName = Optional.ofNullable(getEntityType()) + .orElseThrow(() -> new IllegalArgumentException("Entity type not specified")) + .getTableName(); + if (idOffset == null) { + return jdbcTemplate.queryForList("SELECT id FROM " + tableName + " WHERE tenant_id = ? ORDER BY id LIMIT ?", + UUID.class, tenantId.getId(), limit); + } else { + return jdbcTemplate.queryForList("SELECT id FROM " + tableName + " WHERE tenant_id = ? AND id > ? ORDER BY id LIMIT ?", + UUID.class, tenantId.getId(), idOffset, limit); + } + } + @Override public List find(TenantId tenantId) { List entities = Lists.newArrayList(getRepository().findAll()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 6c1118cdba..da603a6c6f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -297,8 +297,8 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink) { - return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByOriginatorId(tenantId.getId(), originatorId.getId(), pageLink.getIdOffset(), DaoUtil.toPageable(pageLink))) + public PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, AlarmId idOffset, PageLink pageLink) { + return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByOriginatorId(tenantId.getId(), originatorId.getId(), idOffset != null ? idOffset.getId() : null, DaoUtil.toPageable(pageLink))) .mapData(AlarmId::new); } From 0b18de1482ba6769dfca8bd5116f0a94f2acc0c8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 12 Apr 2024 15:32:07 +0300 Subject: [PATCH 038/147] Refactor entities deletion --- .../AlarmsDeletionTaskProcessor.java | 6 +- .../EntitiesDeletionTaskProcessor.java | 9 +-- .../DefaultExportableEntitiesService.java | 2 +- .../server/dao/entity/EntityDaoService.java | 2 +- .../server/dao/user/UserService.java | 2 + .../housekeeper/DefaultHousekeeperClient.java | 2 +- .../dao/asset/AssetProfileServiceImpl.java | 21 ++++--- .../server/dao/asset/BaseAssetService.java | 19 ++++--- .../dao/customer/CustomerServiceImpl.java | 19 ++++--- .../dao/dashboard/DashboardServiceImpl.java | 12 ++-- .../dao/device/DeviceProfileServiceImpl.java | 21 ++++--- .../server/dao/device/DeviceServiceImpl.java | 26 +++++---- .../server/dao/edge/EdgeServiceImpl.java | 12 +++- .../server/dao/entity/EntityDaoRegistry.java | 2 - .../dao/entityview/EntityViewServiceImpl.java | 9 +++ .../DefaultNotificationRequestService.java | 10 ++++ .../DefaultNotificationRuleService.java | 10 ++-- .../DefaultNotificationTargetService.java | 15 ++--- .../DefaultNotificationTemplateService.java | 12 ++-- .../server/dao/ota/BaseOtaPackageService.java | 5 ++ .../server/dao/queue/BaseQueueService.java | 5 ++ .../dao/queue/BaseQueueStatsService.java | 6 ++ .../dao/resource/BaseResourceService.java | 10 ++-- .../server/dao/rpc/BaseRpcService.java | 5 ++ .../server/dao/rule/BaseRuleChainService.java | 17 ++++-- .../server/dao/sql/queue/JpaQueueDao.java | 9 ++- .../dao/sql/queue/JpaQueueStatsDao.java | 8 ++- .../dao/sql/tenant/JpaTenantProfileDao.java | 7 ++- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../usagerecord/ApiUsageStateServiceImpl.java | 10 ++++ .../server/dao/user/UserServiceImpl.java | 7 ++- .../dao/widget/WidgetTypeServiceImpl.java | 5 ++ .../dao/widget/WidgetsBundleServiceImpl.java | 22 +++++--- .../dao/service/EntityDaoRegistryTest.java | 55 +++++++++++++++++++ 34 files changed, 274 insertions(+), 110 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index c4834df71d..09fa0977d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -64,10 +64,10 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor> entity = entityService.findEntity(tenantId, entityId); -// if (entity.isEmpty()) { -// continue; -// } - entityService.deleteEntity(tenantId, entityId); + entityService.deleteEntity(tenantId, entityId, true); } + log.debug("[{}] Deleted {} {}s", tenantId, task.getEntities().size(), entityType.getNormalName().toLowerCase()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java index 6a2a5b4889..49314e62fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java @@ -147,7 +147,7 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi public void removeById(TenantId tenantId, I id) { EntityType entityType = id.getEntityType(); EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); - entityService.deleteEntity(tenantId, id); + entityService.deleteEntity(tenantId, id, false); } private > ExportableEntityDao getExportableEntityDao(EntityType entityType) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java index 992c4fad94..cf55cf1054 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java @@ -30,7 +30,7 @@ public interface EntityDaoService { throw new IllegalArgumentException("Not implemented for " + getEntityType()); } - default void deleteEntity(TenantId tenantId, EntityId id) { + default void deleteEntity(TenantId tenantId, EntityId id, boolean force) { throw new IllegalArgumentException(getEntityType().getNormalName() + " deletion not supported"); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index f137bcc334..0cad776e78 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -77,6 +77,8 @@ public interface UserService extends EntityDaoService { void deleteTenantAdmins(TenantId tenantId); + void deleteAllByTenantId(TenantId tenantId); + PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); PageData findUsersByCustomerIds(TenantId tenantId, List customerIds, PageLink pageLink); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java index 3b06f0cf3e..ce5311b0e7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/DefaultHousekeeperClient.java @@ -65,7 +65,7 @@ public class DefaultHousekeeperClient implements HousekeeperClient { return; } - log.debug("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), taskType); + log.debug("[{}][{}][{}] Submitting task: {}", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), task); /* * using msg key as entity id so that msgs related to certain entity are pushed to same partition, * e.g. on tenant deletion (entity id is tenant id), we need to clean up tenant entities in certain order diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 7946448666..6848c58d5c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -180,11 +180,20 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_ASSET_PROFILE_ID + id); - AssetProfile assetProfile = assetProfileDao.findById(tenantId, assetProfileId.getId()); - if (assetProfile != null && assetProfile.isDefault()) { + deleteEntity(tenantId, assetProfileId, false); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + AssetProfile assetProfile = assetProfileDao.findById(tenantId, id.getId()); + if (assetProfile == null) { + return; + } + if (!force && assetProfile.isDefault()) { throw new DataValidationException("Deletion of Default Asset Profile is prohibited!"); } - this.removeAssetProfile(tenantId, assetProfile); + removeAssetProfile(tenantId, assetProfile); } private void removeAssetProfile(TenantId tenantId, AssetProfile assetProfile) { @@ -312,12 +321,6 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_ASSET_ID + id); - if (entityViewService.existsByTenantIdAndEntityId(tenantId, assetId)) { + deleteEntity(tenantId, assetId, false); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + if (!force && entityViewService.existsByTenantIdAndEntityId(tenantId, id)) { throw new DataValidationException("Can't delete asset that has entity views!"); } - Asset asset = assetDao.findById(tenantId, assetId.getId()); + Asset asset = assetDao.findById(tenantId, id.getId()); + if (asset == null) { + return; + } deleteAsset(tenantId, asset); } @@ -473,12 +482,6 @@ public class BaseAssetService extends AbstractCachedEntityService INCORRECT_CUSTOMER_ID + id); + deleteEntity(tenantId, customerId, false); + } + + @Transactional + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + CustomerId customerId = (CustomerId) id; Customer customer = findCustomerById(tenantId, customerId); if (customer == null) { - throw new IncorrectParameterException("Unable to delete non-existent customer."); + if (force) { + return; + } else { + throw new IncorrectParameterException("Unable to delete non-existent customer."); + } } dashboardService.unassignCustomerDashboards(tenantId, customerId); entityViewService.unassignCustomerEntityViews(customer.getTenantId(), customerId); @@ -204,12 +215,6 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom return Optional.ofNullable(findCustomerById(tenantId, new CustomerId(entityId.getId()))); } - @Transactional - @Override - public void deleteEntity(TenantId tenantId, EntityId id) { - deleteCustomer(tenantId, (CustomerId) id); - } - @Override public long countByTenantId(TenantId tenantId) { return customerDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 8eef618382..167ae51fc3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -245,6 +245,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } } + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteDashboard(tenantId, (DashboardId) id); + } + @Override public PageData findDashboardsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -394,12 +400,6 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardDao.countByTenantId(tenantId); } - @Override - @Transactional - public void deleteEntity(TenantId tenantId, EntityId id) { - deleteDashboard(tenantId, (DashboardId) id); - } - @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 700d8b52f0..7a0f7a56c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -217,11 +217,20 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_PROFILE_ID + id); - DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); - if (deviceProfile != null && deviceProfile.isDefault()) { + deleteEntity(tenantId, deviceProfileId, false); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, id.getId()); + if (deviceProfile == null) { + return; + } + if (!force && deviceProfile.isDefault()) { throw new DataValidationException("Deletion of Default Device Profile is prohibited!"); } - this.removeDeviceProfile(tenantId, deviceProfile); + removeDeviceProfile(tenantId, deviceProfile); } private void removeDeviceProfile(TenantId tenantId, DeviceProfile deviceProfile) { @@ -361,12 +370,6 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_ID + id); - if (entityViewService.existsByTenantIdAndEntityId(tenantId, deviceId)) { + deleteEntity(tenantId, deviceId, false); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + if (!force && entityViewService.existsByTenantIdAndEntityId(tenantId, id)) { throw new DataValidationException("Can't delete device that has entity views!"); } - Device device = deviceDao.findById(tenantId, deviceId.getId()); + Device device = deviceDao.findById(tenantId, id.getId()); + if (device == null) { + return; + } deleteDevice(tenantId, device); } @@ -344,12 +353,11 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, id ->INCORRECT_TENANT_ID + id); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink); } - @Override public PageData findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) { log.trace("Executing findDeviceInfosByFilter, filter [{}], pageLink [{}]", filter, pageLink); @@ -410,14 +418,14 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByIds(List deviceIds) { log.trace("Executing findDevicesByIdsAsync, deviceIds [{}]", deviceIds); - validateIds(deviceIds, ids-> "Incorrect deviceIds " + ids); + validateIds(deviceIds, ids -> "Incorrect deviceIds " + ids); return deviceDao.findDevicesByIds(toUUIDs(deviceIds)); } @Override public ListenableFuture> findDevicesByIdsAsync(List deviceIds) { log.trace("Executing findDevicesByIdsAsync, deviceIds [{}]", deviceIds); - validateIds(deviceIds, ids-> "Incorrect deviceIds " + ids); + validateIds(deviceIds, ids -> "Incorrect deviceIds " + ids); return deviceDao.findDevicesByIdsAsync(toUUIDs(deviceIds)); } @@ -651,12 +659,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService tenantDevicesRemover = new PaginatedRemover<>() { @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 1fd3cacb6d..d5b7955d85 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -21,6 +21,7 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -61,7 +62,6 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; -import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; @@ -72,7 +72,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -225,6 +224,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService INCORRECT_EDGE_ID + id); Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + if (edge == null) { + return; + } edgeDao.removeById(tenantId, edgeId.getId()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(edgeId).build()); @@ -232,6 +234,12 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEdgesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java index 261454966c..10e301cce6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.entity; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -26,7 +25,6 @@ import java.util.Map; import java.util.stream.Collectors; @Service -@RequiredArgsConstructor @Slf4j public class EntityDaoRegistry { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index faf70654cb..e56055b614 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -328,11 +328,20 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService INCORRECT_ENTITY_VIEW_ID + id); EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId()); + if (entityView == null) { + return; + } entityViewDao.removeById(tenantId, entityViewId.getId()); publishEvictEvent(new EntityViewEvictEvent(entityView.getTenantId(), entityView.getId(), entityView.getEntityId(), null, entityView.getName(), null)); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(entityViewId).build()); } + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteEntityView(tenantId, (EntityViewId) id); + } + @Override public void deleteEntityViewsByTenantId(TenantId tenantId) { log.trace("Executing deleteEntityViewsByTenantId, tenantId [{}]", tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java index a74e9484c0..ebeb39ed91 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java @@ -93,6 +93,16 @@ public class DefaultNotificationRequestService implements NotificationRequestSer eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(request).entityId(request.getId()).build()); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + if (force) { + notificationRequestDao.removeById(tenantId, id.getId()); + } else { + NotificationRequest notificationRequest = findNotificationRequestById(tenantId, (NotificationRequestId) id); + deleteNotificationRequest(tenantId, notificationRequest); + } + } + @Override public PageData findScheduledNotificationRequests(PageLink pageLink) { return notificationRequestDao.findAllByStatus(NotificationRequestStatus.SCHEDULED, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java index f306818927..ccf8d83887 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java @@ -94,6 +94,11 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteNotificationRuleById(tenantId, (NotificationRuleId) id); + } + @Override public void deleteNotificationRulesByTenantId(TenantId tenantId) { notificationRuleDao.removeByTenantId(tenantId); @@ -109,11 +114,6 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem return Optional.ofNullable(findNotificationRuleById(tenantId, new NotificationRuleId(entityId.getId()))); } - @Override - public void deleteEntity(TenantId tenantId, EntityId id) { - deleteNotificationRuleById(tenantId, (NotificationRuleId) id); - } - @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index 5996ec6c90..3038578e99 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -187,10 +187,16 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public void deleteNotificationTargetById(TenantId tenantId, NotificationTargetId id) { - if (notificationRequestDao.existsByTenantIdAndStatusAndTargetId(tenantId, NotificationRequestStatus.SCHEDULED, id)) { + deleteEntity(tenantId, id, false); + } + + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + NotificationTargetId targetId = (NotificationTargetId) id; + if (!force && notificationRequestDao.existsByTenantIdAndStatusAndTargetId(tenantId, NotificationRequestStatus.SCHEDULED, targetId)) { throw new IllegalArgumentException("Recipients group is referenced by scheduled notification request"); } - if (notificationRuleDao.existsByTenantIdAndTargetId(tenantId, id)) { + if (!force && notificationRuleDao.existsByTenantIdAndTargetId(tenantId, targetId)) { throw new IllegalArgumentException("Recipients group is being used in notification rule"); } notificationTargetDao.removeById(tenantId, id.getId()); @@ -216,11 +222,6 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl return Optional.ofNullable(findNotificationTargetById(tenantId, new NotificationTargetId(entityId.getId()))); } - @Override - public void deleteEntity(TenantId tenantId, EntityId id) { - deleteNotificationTargetById(tenantId, (NotificationTargetId) id); - } - @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index bec6c3d61c..f29894ee9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -76,7 +76,12 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im @Override public void deleteNotificationTemplateById(TenantId tenantId, NotificationTemplateId id) { - if (notificationRequestDao.existsByTenantIdAndStatusAndTemplateId(tenantId, NotificationRequestStatus.SCHEDULED, id)) { + deleteEntity(tenantId, id, false); + } + + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + if (!force && notificationRequestDao.existsByTenantIdAndStatusAndTemplateId(tenantId, NotificationRequestStatus.SCHEDULED, (NotificationTemplateId) id)) { throw new IllegalArgumentException("Notification template is referenced by scheduled notification request"); } try { @@ -104,11 +109,6 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im return Optional.ofNullable(findNotificationTemplateById(tenantId, new NotificationTemplateId(entityId.getId()))); } - @Override - public void deleteEntity(TenantId tenantId, EntityId id) { - deleteNotificationTemplateById(tenantId, (NotificationTemplateId) id); - } - @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 9a29845c66..06c449880d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -218,6 +218,11 @@ public class BaseOtaPackageService extends AbstractCachedEntityService findQueuesByTenantId(TenantId tenantId) { log.trace("Executing findQueues, tenantId: [{}]", tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java index 2274a782bb..6352ac1a4c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java @@ -78,6 +78,11 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu queueStatsDao.deleteByTenantId(tenantId); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + queueStatsDao.removeById(tenantId, id.getId()); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findQueueStatsById(tenantId, new QueueStatsId(entityId.getId()))); @@ -87,4 +92,5 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu public EntityType getEntityType() { return EntityType.QUEUE_STATS; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index ad35ccd9b1..723d0e8861 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -159,6 +159,11 @@ public class BaseResourceService extends AbstractCachedEntityService findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { TenantId tenantId = filter.getTenantId(); @@ -213,11 +218,6 @@ public class BaseResourceService extends AbstractCachedEntityService implements Q return DaoUtil.toPageData(queueRepository .findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } -} \ No newline at end of file + + @Override + public EntityType getEntityType() { + return EntityType.QUEUE; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java index ac57d54e90..1cfdbf0b38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.dao.DaoUtil; @@ -63,4 +64,9 @@ public class JpaQueueStatsDao extends JpaAbstractDao INCORRECT_TENANT_ID + id); - userService.deleteByTenantId(tenantId); + userService.deleteAllByTenantId(tenantId); adminSettingsService.deleteAdminSettingsByTenantId(tenantId); notificationSettingsService.deleteNotificationSettings(tenantId); tenantDao.removeById(tenantId, tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index 6fe94e6341..d901ae950e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -77,12 +77,22 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A log.trace("Executing deleteApiUsageStateByEntityId [{}]", entityId); validateId(entityId.getId(), id -> "Invalid entity id " + id); ApiUsageState apiUsageState = findApiUsageStateByEntityId(entityId); + deleteApiUsageState(apiUsageState); + } + + private void deleteApiUsageState(ApiUsageState apiUsageState) { if (apiUsageState != null) { apiUsageStateDao.removeById(apiUsageState.getTenantId(), apiUsageState.getUuidId()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(apiUsageState.getTenantId()).entityId(apiUsageState.getId()).build()); } } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + ApiUsageState apiUsageState = findApiUsageStateById(tenantId, (ApiUsageStateId) id); + deleteApiUsageState(apiUsageState); + } + @Override public ApiUsageState createDefaultApiUsageState(TenantId tenantId, EntityId entityId) { entityId = Objects.requireNonNullElse(entityId, tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 8403520e04..90084e1a3f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -322,11 +322,16 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } @Override - public void deleteByTenantId(TenantId tenantId) { + public void deleteAllByTenantId(TenantId tenantId) { log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId); usersRemover.removeEntities(tenantId, tenantId); } + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteAllByTenantId(tenantId); + } + @Override public PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findCustomerUsers, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 8d24746929..f2506a2d9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -115,6 +115,11 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(widgetTypeId).build()); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteWidgetType(tenantId, (WidgetTypeId) id); + } + @Override public PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", fullSearch, deprecatedFilter, widgetTypes, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 3d16bb519b..ae53c70ec8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -92,12 +92,21 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { public void deleteWidgetsBundle(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing deleteWidgetsBundle [{}]", widgetsBundleId); Validator.validateId(widgetsBundleId, id -> "Incorrect widgetsBundleId " + id); - WidgetsBundle widgetsBundle = findWidgetsBundleById(tenantId, widgetsBundleId); + deleteEntity(tenantId, widgetsBundleId, false); + } + + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + WidgetsBundle widgetsBundle = findWidgetsBundleById(tenantId, (WidgetsBundleId) id); if (widgetsBundle == null) { - throw new IncorrectParameterException("Unable to delete non-existent widgets bundle."); + if (force) { + return; + } else { + throw new IncorrectParameterException("Unable to delete non-existent widgets bundle."); + } } - eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(widgetsBundleId).build()); - widgetsBundleDao.removeById(tenantId, widgetsBundleId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); + widgetsBundleDao.removeById(tenantId, id.getId()); } @Override @@ -189,11 +198,6 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId()))); } - @Override - public void deleteEntity(TenantId tenantId, EntityId id) { - deleteWidgetsBundle(tenantId, (WidgetsBundleId) id); - } - @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java new file mode 100644 index 0000000000..08d23db022 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java @@ -0,0 +1,55 @@ +/** + * 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.dao.service; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.entity.EntityDaoRegistry; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@Slf4j +@DaoSqlTest +public class EntityDaoRegistryTest extends AbstractServiceTest { + + @Autowired + EntityDaoRegistry entityDaoRegistry; + + @Test + public void givenAllEntityTypes_whenGetDao_thenAllPresent() { + for (EntityType entityType : EntityType.values()) { + Dao dao = assertDoesNotThrow(() -> entityDaoRegistry.getDao(entityType)); + assertThat(dao).isNotNull(); + } + } + + @Test + public void givenAllDaos_whenFindById_thenOk() { + for (EntityType entityType : EntityType.values()) { + assertDoesNotThrow(() -> { + entityDaoRegistry.getDao(entityType).findById(TenantId.SYS_TENANT_ID, UUID.randomUUID()); + }); + } + } + +} From 40d9789035007eaee639b438fc1d8a6176d7250b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 12 Apr 2024 17:24:06 +0300 Subject: [PATCH 039/147] More tests --- .../housekeeper/HousekeeperServiceTest.java | 19 +++++++++---- .../server/dao/sql/JpaAbstractDao.java | 28 +++++++++++-------- .../dao/service/EntityDaoRegistryTest.java | 23 ++++++++++++++- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index efbfbffbb9..a1015c4a4c 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -80,6 +80,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.service.housekeeper.processor.TsHistoryDeletionTaskProcessor; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -232,13 +233,17 @@ public class HousekeeperServiceTest extends AbstractControllerTest { createRelatedData(differentTenantCustomerId); loginDifferentTenant(); - Device device = createDevice("test", "test"); - createRelatedData(device.getId()); + List devices = new ArrayList<>(); + for (int i = 1; i <= 300; i++) { + Device device = createDevice("test" + i, "test" + i); + createRelatedData(device.getId()); + devices.add(device.getId()); + } Asset asset = createAsset(); createRelatedData(asset.getId()); - createRelation(device.getId(), asset.getId()); - createAlarm(device.getId(), asset.getId()); + createRelation(devices.get(0), asset.getId()); + createAlarm(devices.get(0), asset.getId()); RuleChainMetaData ruleChainMetaData = createRuleChain(); RuleChainId ruleChainId = ruleChainMetaData.getRuleChainId(); @@ -256,8 +261,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { loginSysAdmin(); deleteDifferentTenant(); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { - verifyNoRelatedData(device.getId()); + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + for (DeviceId deviceId : devices) { + verifyNoRelatedData(deviceId); + } verifyNoRelatedData(asset.getId()); verifyNoRelatedData(ruleNode1Id); verifyNoRelatedData(ruleNode2Id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 6bfd74ac87..973cb4b5b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.SqlDao; import java.util.Collection; @@ -122,24 +123,29 @@ public abstract class JpaAbstractDao, D> ids.forEach(repository::deleteById); } + @Override + public List find(TenantId tenantId) { + List entities = Lists.newArrayList(getRepository().findAll()); + return DaoUtil.convertDataList(entities); + } + @Override public List findIdsByTenantIdAndIdOffset(TenantId tenantId, UUID idOffset, int limit) { - String tableName = Optional.ofNullable(getEntityType()) - .orElseThrow(() -> new IllegalArgumentException("Entity type not specified")) - .getTableName(); + String query = "SELECT id FROM " + getEntityType().getTableName() + " WHERE " + getTenantIdColumn() + " = ? "; + Object[] params; if (idOffset == null) { - return jdbcTemplate.queryForList("SELECT id FROM " + tableName + " WHERE tenant_id = ? ORDER BY id LIMIT ?", - UUID.class, tenantId.getId(), limit); + params = new Object[]{tenantId.getId(), limit}; } else { - return jdbcTemplate.queryForList("SELECT id FROM " + tableName + " WHERE tenant_id = ? AND id > ? ORDER BY id LIMIT ?", - UUID.class, tenantId.getId(), idOffset, limit); + query += " AND id > ? "; + params = new Object[]{tenantId.getId(), idOffset, limit}; } + query += " ORDER BY id LIMIT ?"; + + return jdbcTemplate.queryForList(query, UUID.class, params); } - @Override - public List find(TenantId tenantId) { - List entities = Lists.newArrayList(getRepository().findAll()); - return DaoUtil.convertDataList(entities); + protected String getTenantIdColumn() { + return ModelConstants.TENANT_ID_COLUMN; } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java index 08d23db022..8524efc4a0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.service; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityType; @@ -26,6 +27,7 @@ import org.thingsboard.server.dao.entity.EntityDaoRegistry; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @Slf4j @@ -46,10 +48,29 @@ public class EntityDaoRegistryTest extends AbstractServiceTest { @Test public void givenAllDaos_whenFindById_thenOk() { for (EntityType entityType : EntityType.values()) { + Dao dao = entityDaoRegistry.getDao(entityType); assertDoesNotThrow(() -> { - entityDaoRegistry.getDao(entityType).findById(TenantId.SYS_TENANT_ID, UUID.randomUUID()); + dao.findById(TenantId.SYS_TENANT_ID, UUID.randomUUID()); }); } } + @Test + public void givenAllDaos_whenFindIdsByTenantIdAndIdOffset_thenOk() { + for (EntityType entityType : EntityType.values()) { + Dao dao = entityDaoRegistry.getDao(entityType); + try { + dao.findIdsByTenantIdAndIdOffset(TenantId.SYS_TENANT_ID, null, 10); + dao.findIdsByTenantIdAndIdOffset(TenantId.SYS_TENANT_ID, UUID.randomUUID(), 10); + } catch (Exception e) { + String error = ExceptionUtils.getRootCauseMessage(e); + if (error.contains("tenant_id")) { + log.debug("[{}] Ignoring not found tenant_id column", entityType); + } else { + fail("findIdsByTenantIdAndIdOffset for " + entityType + " dao threw error: " + error); + } + } + } + } + } From 881a8048ce7cbb598a410ad8cc5340fe22bbe9b9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 15 Apr 2024 12:34:16 +0300 Subject: [PATCH 040/147] Fix EntityDaoRegistry init --- application/src/main/resources/thingsboard.yml | 2 +- .../server/queue/housekeeper/HousekeeperConfig.java | 2 +- .../server/dao/entity/EntityDaoRegistry.java | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index cbc4e79dd3..024eb54974 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1643,7 +1643,7 @@ queue: # DELETE_LATEST_TS, DELETE_TS_HISTORY, DELETE_EVENTS, DELETE_ALARMS, UNASSIGN_ALARMS disabled-task-types: "${TB_HOUSEKEEPER_DISABLED_TASK_TYPES:}" # Delay in milliseconds between tasks reprocessing - task-reprocessing-delay-ms: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_MS:5000}" + task-reprocessing-delay-ms: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_MS:3000}" # Maximum amount of task reprocessing attempts. After exceeding, the task will be dropped max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:10}" stats: diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java index b03861a7ee..4ca3776d4d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/housekeeper/HousekeeperConfig.java @@ -32,7 +32,7 @@ public class HousekeeperConfig { private int taskProcessingTimeout; @Value("${queue.core.housekeeper.poll-interval-ms:500}") private int pollInterval; - @Value("${queue.core.housekeeper.task-reprocessing-delay-ms:5000}") + @Value("${queue.core.housekeeper.task-reprocessing-delay-ms:3000}") private int taskReprocessingDelay; @Value("${queue.core.housekeeper.max-reprocessing-attempts:10}") private int maxReprocessingAttempts; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java index 10e301cce6..7b2f2d42c1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -20,19 +20,23 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.dao.Dao; +import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Service @Slf4j public class EntityDaoRegistry { - private final Map> daos; + private final Map> daos = new EnumMap<>(EntityType.class); private EntityDaoRegistry(List> daos) { - this.daos = daos.stream().filter(dao -> dao.getEntityType() != null) - .collect(Collectors.toMap(Dao::getEntityType, dao -> dao)); + daos.forEach(dao -> { + EntityType entityType = dao.getEntityType(); + if (entityType != null) { + this.daos.put(entityType, dao); + } + }); } @SuppressWarnings("unchecked") From 6f9e9305bd6d03a64cda9b54befd74f36446e485 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 15 Apr 2024 16:02:37 +0300 Subject: [PATCH 041/147] Housekeeper refactoring --- .../HousekeeperReprocessingService.java | 3 -- .../housekeeper/HousekeeperService.java | 24 ++++++------ .../stats/HousekeeperStatsService.java | 1 + .../server/controller/AbstractWebTest.java | 13 +++++-- .../housekeeper/HousekeeperServiceTest.java | 37 ++++++------------- .../AlarmsDeletionHousekeeperTask.java | 5 +++ .../EntitiesDeletionHousekeeperTask.java | 2 +- .../LatestTsDeletionHousekeeperTask.java | 5 +++ .../TsHistoryDeletionHousekeeperTask.java | 5 +++ .../trigger/TaskProcessingFailureTrigger.java | 14 +------ 10 files changed, 49 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index ba762facb1..cdc0d3f880 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -40,7 +40,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; @TbCoreComponent @Service @@ -55,8 +54,6 @@ public class HousekeeperReprocessingService { private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper-reprocessing-consumer")); - protected AtomicInteger cycle = new AtomicInteger(); - public HousekeeperReprocessingService(HousekeeperConfig config, @Lazy HousekeeperService housekeeperService, TbCoreQueueFactory queueFactory) { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index b16c1b0de9..1f5ed1be58 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.notification.rule.trigger.TaskProcessingFailureTrigger; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.gen.transport.TransportProtos.HousekeeperTaskProto; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -118,15 +117,19 @@ public class HousekeeperService { throw new IllegalArgumentException("Unsupported task type " + taskType); } - if (log.isDebugEnabled()) { - log.debug("[{}] {} {}", task.getTenantId(), isNew(msg.getTask()) ? "Processing" : "Reprocessing", task.getDescription()); - } + try { + long startTs = System.currentTimeMillis(); Future future = taskExecutor.submit(() -> { taskProcessor.process((T) task); return null; }); future.get(config.getTaskProcessingTimeout(), TimeUnit.MILLISECONDS); + + long timing = System.currentTimeMillis() - startTs; + if (log.isDebugEnabled()) { + log.debug("[{}] Processed {} in {} ms (attempt {})", task.getTenantId(), task.getDescription(), timing, msg.getTask().getAttempt()); + } statsService.ifPresent(statsService -> statsService.reportProcessed(taskType, msg)); } catch (InterruptedException e) { throw e; @@ -137,14 +140,13 @@ public class HousekeeperService { } else if (e instanceof TimeoutException) { error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " seconds"); } - log.error("[{}][{}][{}] {} task processing failed, submitting for reprocessing (attempt {}): {}", - task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), - taskType, msg.getTask().getAttempt(), task, error); if (msg.getTask().getAttempt() < config.getMaxReprocessingAttempts()) { + log.warn("[{}] Failed to process {} (attempt {}), submitting for reprocessing", + task.getTenantId(), task.getDescription(), msg.getTask().getAttempt(), error); reprocessingService.submitForReprocessing(msg, error); } else { - log.error("Failed to process task in {} attempts: {}", msg.getTask().getAttempt(), msg); + log.error("[{}] Failed to process task in {} attempts: {}", task.getTenantId(), msg.getTask().getAttempt(), msg, e); notificationRuleProcessor.process(TaskProcessingFailureTrigger.builder() .task(task) .error(error) @@ -155,12 +157,8 @@ public class HousekeeperService { } } - private boolean isNew(HousekeeperTaskProto task) { - return task.getErrorsCount() == 0; - } - @PreDestroy - private void stop() throws Exception { + private void stop() { consumer.stop(); consumerExecutor.shutdownNow(); log.info("Stopped Housekeeper service"); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java index 4944cf9f43..da586aa820 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -69,6 +69,7 @@ public class HousekeeperStatsService { } public void reportProcessed(HousekeeperTaskType taskType, ToHousekeeperServiceMsg msg) { + // todo: report timings HousekeeperStats stats = this.stats.get(taskType); if (msg.getTask().getErrorsCount() == 0) { stats.getProcessedCounter().increment(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 5dd9e4b77f..ee239d12db 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -70,6 +70,7 @@ import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; import org.thingsboard.server.actors.device.SessionInfo; import org.thingsboard.server.actors.device.ToDeviceRpcRequestMetadata; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -102,7 +103,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -116,6 +117,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -231,7 +233,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { private TbTenantProfileService tbTenantProfileService; @Autowired - public TimeseriesService tsService; + protected TimeseriesService tsService; + + @Autowired + protected AttributesService attributesService; @Autowired protected DefaultActorService actorService; @@ -379,7 +384,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } } Awaitility.await("tenant cleanup finish").atMost(30, TimeUnit.SECONDS) - .until(() -> tsService.findLatest(TenantId.SYS_TENANT_ID, tenantId, "test").get().isEmpty()); + .until(() -> attributesService.find(TenantId.SYS_TENANT_ID, tenantId, AttributeScope.SERVER_SCOPE, "test").get().isEmpty()); } private List getAllTenants() throws Exception { @@ -444,7 +449,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected Tenant createTenant(Tenant tenant) throws Exception { tenant = doPost("/api/tenant", tenant, Tenant.class); - tsService.save(TenantId.SYS_TENANT_ID, tenant.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("test", "test"))).get(); // creating marker ts to later know when Housekeeper finishes tenant cleanup + attributesService.save(TenantId.SYS_TENANT_ID, tenant.getId(), AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry("test", "test"))).get(); // creating marker attr to later know when Housekeeper finishes tenant cleanup return tenant; } diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index a1015c4a4c..e66d3ed86a 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; @@ -64,7 +63,6 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmService; @@ -113,8 +111,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @SpyBean private HousekeeperService housekeeperService; @SpyBean - private HousekeeperClient housekeeperClient; - @SpyBean private HousekeeperReprocessingService housekeeperReprocessingService; @Autowired private EventService eventService; @@ -236,14 +232,17 @@ public class HousekeeperServiceTest extends AbstractControllerTest { List devices = new ArrayList<>(); for (int i = 1; i <= 300; i++) { Device device = createDevice("test" + i, "test" + i); - createRelatedData(device.getId()); devices.add(device.getId()); } + DeviceId firstDevice = devices.get(0); + createRelatedData(firstDevice); + DeviceId lastDevice = devices.get(devices.size() - 1); + createRelatedData(lastDevice); Asset asset = createAsset(); createRelatedData(asset.getId()); - createRelation(devices.get(0), asset.getId()); - createAlarm(devices.get(0), asset.getId()); + createRelation(firstDevice, asset.getId()); + createAlarm(firstDevice, asset.getId()); RuleChainMetaData ruleChainMetaData = createRuleChain(); RuleChainId ruleChainId = ruleChainMetaData.getRuleChainId(); @@ -261,7 +260,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { loginSysAdmin(); deleteDifferentTenant(); - await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(60, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { for (DeviceId deviceId : devices) { verifyNoRelatedData(deviceId); } @@ -277,7 +276,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } @Test - @Ignore // FIXME !!! public void whenTaskProcessingFails_thenReprocess() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); @@ -289,12 +287,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { int attempts = 2; await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, 0); - for (int i = 1; i <= attempts; i++) { + for (int i = 0; i <= attempts; i++) { int attempt = i; verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, - task -> task.getErrorsCount() > 0 && task.getAttempt() == attempt)), argThat(e -> e.getMessage().equals(error.getMessage()))); - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, attempt); + task -> task.getAttempt() == attempt)), argThat(e -> e.getMessage().equals(error.getMessage()))); } }); @@ -306,8 +302,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } @Test - @Ignore // FIXME !!! - public void whenReprocessingAttemptsExceeded_thenReprocessOnNextStartUp() throws Exception { + public void whenReprocessingAttemptsExceeded_thenDropTheTask() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); @@ -327,20 +322,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); TimeUnit.SECONDS.sleep(2); verify(housekeeperService, never()).processTask(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, null))); - - housekeeperReprocessingService.cycle.set(0); // imitating start-up - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { - verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, 6); - }); } private void verifyTaskProcessing(EntityId entityId, HousekeeperTaskType taskType, int expectedAttempt) throws Exception { - try { - verify(housekeeperService).processTask(argThat(getTaskMatcher(entityId, taskType, task -> task.getAttempt() == expectedAttempt))); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } + verify(housekeeperService).processTask(argThat(getTaskMatcher(entityId, taskType, task -> task.getAttempt() == expectedAttempt))); } private ArgumentMatcher getTaskMatcher(EntityId entityId, HousekeeperTaskType taskType, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java index 9bd8f29bb4..60a50916ef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java @@ -43,4 +43,9 @@ public class AlarmsDeletionHousekeeperTask extends HousekeeperTask { this.alarms = alarms; } + @Override + public String getDescription() { + return super.getDescription() + (alarms != null ? " (" + alarms + ")" : ""); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java index e29c3d3c51..d90fa6de94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java @@ -49,7 +49,7 @@ public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { @JsonIgnore @Override public String getDescription() { - return entityType.getNormalName().toLowerCase() + "s deletion"; + return entityType.getNormalName().toLowerCase() + "s deletion" + (entities != null ? " (" + entities + ")" : ""); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java index cef43756e0..4f0f56479e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java @@ -36,4 +36,9 @@ public class LatestTsDeletionHousekeeperTask extends HousekeeperTask { this.key = key; } + @Override + public String getDescription() { + return super.getDescription() + (key != null ? " for key '" + key + "'" : ""); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java index 80b8edc345..8f462f2dff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java @@ -36,4 +36,9 @@ public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask { this.key = key; } + @Override + public String getDescription() { + return super.getDescription() + (key != null ? " for key '" + key + "'" : ""); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java index 9ccfac6aff..3626e09c0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java @@ -22,8 +22,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; -import java.util.concurrent.TimeUnit; - @Data @Builder public class TaskProcessingFailureTrigger implements NotificationRuleTrigger { @@ -49,17 +47,7 @@ public class TaskProcessingFailureTrigger implements NotificationRuleTrigger { @Override public boolean deduplicate() { - return true; - } - - @Override - public String getDeduplicationKey() { - return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), task.getTaskType().toString()); - } - - @Override - public long getDefaultDeduplicationDuration() { - return TimeUnit.MINUTES.toMillis(30); + return false; } } From be045bcf4251541379efc1135b6b117e92cbb929 Mon Sep 17 00:00:00 2001 From: imbeacon Date: Tue, 16 Apr 2024 09:20:59 +0300 Subject: [PATCH 042/147] Updated due to comments --- .../server/common/data/DataConstants.java | 2 ++ .../AbstractGatewaySessionHandler.java | 6 ++---- .../session/GatewaySessionHandlerTest.java | 19 +++++++++++-------- .../service/DefaultTransportService.java | 8 +++++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 53b5d2ec63..b529edeb0c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -44,6 +44,8 @@ public class DataConstants { public static final String EDGE_ID = "edgeId"; public static final String DEVICE_ID = "deviceId"; public static final String GATEWAY_PARAMETER = "gateway"; + + public static final String OVERWRITE_ACTIVITY_TIME_PARAMETER = "overwriteActivityTime"; public static final String COAP_TRANSPORT_NAME = "COAP"; public static final String LWM2M_TRANSPORT_NAME = "LWM2M"; public static final String MQTT_TRANSPORT_NAME = "MQTT"; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index b881291756..24a9b665ae 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -95,8 +95,6 @@ public abstract class AbstractGatewaySessionHandler deviceProfileOpt) { log.trace("[{}][{}] onDeviceUpdate: [{}]", gateway.getTenantId(), gateway.getDeviceId(), device); JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); - if (deviceAdditionalInfo.has(GATEWAY_PROPERTY) && deviceAdditionalInfo.has(OVERWRITE_ACTIVITY_TIME)) { - overwriteDevicesActivity = deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).asBoolean(); + if (deviceAdditionalInfo.has(DataConstants.GATEWAY_PARAMETER) && deviceAdditionalInfo.has(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER)) { + overwriteDevicesActivity = deviceAdditionalInfo.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).asBoolean(); } } diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java index 9b52d57dc2..158acc2ce2 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java @@ -17,8 +17,10 @@ package org.thingsboard.server.transport.mqtt.session; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; @@ -34,6 +36,7 @@ import java.util.concurrent.ConcurrentHashMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,6 +44,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doNothing; +@ExtendWith(MockitoExtension.class) public class GatewaySessionHandlerTest { @Mock @@ -56,13 +60,12 @@ public class GatewaySessionHandlerTest { @BeforeEach public void setup() { - MockitoAnnotations.openMocks(this); - when(deviceSessionCtx.getSessionId()).thenReturn(UUID.randomUUID()); - doNothing().when(transportService).recordActivity(any()); - when(transportContext.getTransportService()).thenReturn(transportService); - when(deviceSessionCtx.getContext()).thenReturn(transportContext); + lenient().when(deviceSessionCtx.getSessionId()).thenReturn(UUID.randomUUID()); + lenient().doNothing().when(transportService).recordActivity(any()); + lenient().when(transportContext.getTransportService()).thenReturn(transportService); + lenient().when(deviceSessionCtx.getContext()).thenReturn(transportContext); handler = new GatewaySessionHandler(deviceSessionCtx, UUID.randomUUID(), true); - when(handler.getNodeId()).thenReturn("nodeId"); + lenient().when(handler.getNodeId()).thenReturn("nodeId"); } @Test @@ -77,10 +80,10 @@ public class GatewaySessionHandlerTest { deviceInfo.setDeviceType("default"); deviceInfo.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); deviceInfo.setAdditionalInfo("{\"gateway\": true, \"overwriteDeviceActivity\": true}"); - when(deviceSessionCtx.getDeviceInfo()).thenReturn(deviceInfo); + lenient().when(deviceSessionCtx.getDeviceInfo()).thenReturn(deviceInfo); GatewayDeviceSessionContext gatewayDeviceSessionContext = new GatewayDeviceSessionContext(handler, deviceInfo, null, null, transportService); devices.put("device1", gatewayDeviceSessionContext); - when(handler.getNodeId()).thenReturn("nodeId"); + lenient().when(handler.getNodeId()).thenReturn("nodeId"); Field devicesField = AbstractGatewaySessionHandler.class.getDeclaredField("devices"); devicesField.setAccessible(true); devicesField.set(handler, devices); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 2f822ccb1a..a756e7a992 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -133,7 +133,6 @@ import java.util.stream.Collectors; @TbTransportComponent public class DefaultTransportService extends TransportActivityManager implements TransportService { - public static final String OVERWRITE_ACTIVITY_TIME = "overwriteActivityTime"; public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_OPEN = TransportProtos.SessionEventMsg.newBuilder() .setSessionType(TransportProtos.SessionType.ASYNC) .setEvent(TransportProtos.SessionEvent.OPEN).build(); @@ -1049,8 +1048,11 @@ public class DefaultTransportService extends TransportActivityManager implements .setDeviceName(device.getName()) .setDeviceType(device.getType()).build(); JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); - if (deviceAdditionalInfo.has("gateway") && deviceAdditionalInfo.has(OVERWRITE_ACTIVITY_TIME)) { - md.setOverwriteActivityTime(deviceAdditionalInfo.get(OVERWRITE_ACTIVITY_TIME).asBoolean()); + if (deviceAdditionalInfo.has(DataConstants.GATEWAY_PARAMETER) + && deviceAdditionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean() + && deviceAdditionalInfo.has(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER) + && deviceAdditionalInfo.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).isBoolean()) { + md.setOverwriteActivityTime(deviceAdditionalInfo.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).asBoolean()); } md.setSessionInfo(newSessionInfo); transportCallbackExecutor.submit(() -> md.getListener().onDeviceUpdate(newSessionInfo, device, Optional.ofNullable(newDeviceProfile))); From d8515383c044dc7265982a33ed875376d08cdfa1 Mon Sep 17 00:00:00 2001 From: imbeacon Date: Tue, 16 Apr 2024 10:07:41 +0300 Subject: [PATCH 043/147] Updated MqttTransportHandler --- .../server/transport/mqtt/MqttTransportHandler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 2ecea63e42..b48cb39ca0 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -1083,11 +1083,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement try { JsonNode infoNode = context.getMapper().readTree(device.getAdditionalInfo()); if (infoNode != null) { - JsonNode gatewayNode = infoNode.get("gateway"); + JsonNode gatewayNode = infoNode.get(DataConstants.GATEWAY_PARAMETER); if (gatewayNode != null && gatewayNode.asBoolean()) { boolean overwriteDevicesActivity = false; - if (infoNode.has(DefaultTransportService.OVERWRITE_ACTIVITY_TIME) && infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).isBoolean()) { - overwriteDevicesActivity = infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).asBoolean(); + if (infoNode.has(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER) + && infoNode.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).isBoolean()) { + overwriteDevicesActivity = infoNode.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).asBoolean(); sessionMetaData.setOverwriteActivityTime(overwriteDevicesActivity); } gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId, overwriteDevicesActivity); From a80d4650ea6d4bfdef531be167a7f573055982f6 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 16 Apr 2024 13:00:17 +0300 Subject: [PATCH 044/147] Delete customers last on tenant deletion; tests fixes --- .../controller/AlarmControllerTest.java | 74 +++++++++---------- .../controller/AssetControllerTest.java | 14 +++- .../server/dao/tenant/TenantServiceImpl.java | 17 ++--- 3 files changed, 51 insertions(+), 54 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 5290b429c0..3d0b0f6ea6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -53,9 +53,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; -import java.util.stream.Collectors; -import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -584,7 +583,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); - beforeAssignmentTs = System.currentTimeMillis(); + long afterAssignmentTs = System.currentTimeMillis(); loginSysAdmin(); @@ -592,12 +591,12 @@ public class AlarmControllerTest extends AbstractControllerTest { loginDifferentTenant(); - foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); - - Assert.assertNotNull(foundAlarm); - Assert.assertNull(foundAlarm.getAssigneeId()); - Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + AlarmInfo alarmInfo = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(alarmInfo); + Assert.assertNull(alarmInfo.getAssigneeId()); + Assert.assertTrue(alarmInfo.getAssignTs() >= afterAssignmentTs); + }); } @Test @@ -631,16 +630,16 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); - beforeAssignmentTs = System.currentTimeMillis(); + long afterAssignmentTs = System.currentTimeMillis(); doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); - foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); - - Assert.assertNotNull(foundAlarm); - Assert.assertNull(foundAlarm.getAssigneeId()); - Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + AlarmInfo alarmInfo = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(alarmInfo); + Assert.assertNull(alarmInfo.getAssigneeId()); + Assert.assertTrue(alarmInfo.getAssignTs() >= afterAssignmentTs); + }); } @Test @@ -684,18 +683,18 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); - beforeAssignmentTs = System.currentTimeMillis(); + long afterAssignmentTs = System.currentTimeMillis(); Mockito.reset(tbClusterService, auditLogService); doDelete("/api/customer/" + differentTenantCustomerId.getId()).andExpect(status().isOk()); - foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); - - Assert.assertNotNull(foundAlarm); - Assert.assertNull(foundAlarm.getAssigneeId()); - Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + AlarmInfo alarmInfo = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(alarmInfo); + Assert.assertNull(alarmInfo.getAssigneeId()); + Assert.assertTrue(alarmInfo.getAssignTs() >= afterAssignmentTs); + }); } @Test @@ -1003,27 +1002,20 @@ public class AlarmControllerTest extends AbstractControllerTest { types.remove(0); } - foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { - }) - .getData() - .stream() - .map(EntitySubtype::getType) - .collect(Collectors.toList()); - - Assert.assertEquals(types.size(), foundTypes.size()); - Assert.assertEquals(types, foundTypes); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + List actualTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() {}) + .getData().stream().map(EntitySubtype::getType).toList(); + Assert.assertEquals(types.size(), actualTypes.size()); + Assert.assertEquals(types, actualTypes); + }); doDelete("/api/device/" + devices.get(0).getId()).andExpect(status().isOk()); - foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { - }) - .getData() - .stream() - .map(EntitySubtype::getType) - .sorted() - .collect(Collectors.toList()); - - Assert.assertTrue(foundTypes.isEmpty()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + List actualTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() {}) + .getData().stream().map(EntitySubtype::getType).sorted().toList(); + Assert.assertTrue(actualTypes.isEmpty()); + }); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index 06d15ddb44..a250b95082 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.PageData; @@ -55,8 +56,10 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -312,8 +315,9 @@ public class AssetControllerTest extends AbstractControllerTest { alarm = doPost("/api/alarm", alarm, Alarm.class); Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); - AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarmId, AlarmInfo.class); Assert.assertNotNull(foundAlarm); doDelete("/api/asset/" + savedAsset.getId().getId().toString()) @@ -324,9 +328,11 @@ public class AssetControllerTest extends AbstractControllerTest { .andExpect(status().isNotFound()) .andExpect(statusReason(containsString(msgErrorNoFound("Asset", assetIdStr)))); - doGet("/api/alarm/info/" + alarm.getId()) - .andExpect(status().isNotFound()) - .andExpect(statusReason(containsString(msgErrorNoFound("Alarm", alarm.getId().getId().toString())))); + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + doGet("/api/alarm/info/" + alarmId) + .andExpect(status().isNotFound()) + .andExpect(statusReason(containsString(msgErrorNoFound("Alarm", alarmId.getId().toString())))); + }); } @Test diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index da96dee67b..97411e82f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -150,7 +150,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Tue, 16 Apr 2024 13:01:04 +0300 Subject: [PATCH 045/147] Single entity deletion event when tenant is deleted --- .../service/edge/EdgeEventSourcingListener.java | 14 +++++++++++--- .../entitiy/EntityStateSourcingListener.java | 6 ++++++ .../server/dao/edge/EdgeServiceImpl.java | 1 - 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index d975217e1f..32db644275 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -44,6 +45,7 @@ import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserServiceImpl; import javax.annotation.PostConstruct; @@ -69,6 +71,7 @@ public class EdgeEventSourcingListener { private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; + private final TenantService tenantService; @PostConstruct public void init() { @@ -95,19 +98,24 @@ public class EdgeEventSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(DeleteEntityEvent event) { + TenantId tenantId = event.getTenantId(); EntityType entityType = event.getEntityId().getEntityType(); + if (!tenantService.tenantExists(tenantId)) { + log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); + return; + } try { if (EntityType.EDGE.equals(entityType) || EntityType.TENANT.equals(entityType)) { return; } - log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); + log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); EdgeEventType type = getEdgeEventTypeForEntityEvent(event.getEntity()); EdgeEventActionType actionType = getEdgeEventActionTypeForEntityEvent(event.getEntity()); - tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), null, event.getEntityId(), + tbClusterService.sendNotificationMsgToEdge(tenantId, null, event.getEntityId(), JacksonUtil.toString(event.getEntity()), type, actionType, edgeSynchronizationManager.getEdgeId().get()); } catch (Exception e) { - log.error("[{}] failed to process DeleteEntityEvent: {}", event.getTenantId(), event, e); + log.error("[{}] failed to process DeleteEntityEvent: {}", tenantId, event, e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index bc7dc62e78..877e252151 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNoti import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.tenant.TenantService; import javax.annotation.PostConstruct; import java.util.Set; @@ -61,6 +62,7 @@ import java.util.Set; public class EntityStateSourcingListener { private final TbClusterService tbClusterService; + private final TenantService tenantService; @PostConstruct public void init() { @@ -121,6 +123,10 @@ public class EntityStateSourcingListener { TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); EntityType entityType = entityId.getEntityType(); + if (entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) { + log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); + return; + } log.debug("[{}][{}][{}] Handling entity deletion event: {}", tenantId, entityType, entityId, event); switch (entityType) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 466a591394..66cf74ba9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -231,7 +231,6 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Date: Tue, 16 Apr 2024 14:01:11 +0300 Subject: [PATCH 046/147] Minor refactoring --- application/src/test/resources/logback-test.xml | 1 + .../org/thingsboard/server/queue/TbQueueConsumer.java | 2 -- .../server/common/data/housekeeper/HousekeeperTask.java | 9 --------- .../queue/common/AbstractTbQueueConsumerTemplate.java | 5 ----- .../server/queue/memory/InMemoryTbQueueConsumer.java | 5 ----- .../grafana/provisioning/dashboards/housekeeper.json | 6 +++--- 6 files changed, 4 insertions(+), 24 deletions(-) diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index d72bccb7a6..4651f2f359 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -31,6 +31,7 @@ + diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java index 3747e6d026..51456ab0e2 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java @@ -36,8 +36,6 @@ public interface TbQueueConsumer { void commit(); - boolean isCommitSupported(); - boolean isStopped(); List getFullTopicNames(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index 344edc3f0d..8354b2b861 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -60,19 +60,10 @@ public class HousekeeperTask implements Serializable { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_ATTRIBUTES); } - /** - * Task for telemetry deletion, processed by TelemetryDeletionTaskProcessor. - * The processor will submit separate tasks for each key for latest and ts history deletion, - * processed by LatestTsDeletionTaskProcessor and TsHistoryDeletionTaskProcessor accordingly. - * */ public static HousekeeperTask deleteTelemetry(TenantId tenantId, EntityId entityId) { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_TELEMETRY); } - /** - * Task for events deletion, processed by EventsDeletionTaskProcessor. - * All types of events will be dropped for this entity. - * */ public static HousekeeperTask deleteEvents(TenantId tenantId, EntityId entityId) { return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_EVENTS); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 89840d81ad..ac263c10c8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -201,11 +201,6 @@ public abstract class AbstractTbQueueConsumerTemplate i return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); } - @Override - public boolean isCommitSupported() { - return true; - } - protected boolean isLongPollingSupported() { return false; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java index 269924087b..de3833d124 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java @@ -114,9 +114,4 @@ public class InMemoryTbQueueConsumer implements TbQueueCon return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); } - @Override - public boolean isCommitSupported() { - return false; - } - } diff --git a/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json b/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json index 8c68cc9428..b604b96135 100644 --- a/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json +++ b/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json @@ -188,7 +188,7 @@ "uid": "9BonzvTSz" }, "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"failedProcessing\"}[1m]))", + "expr": "sum by (taskType) (increase(housekeeper_total{statsName=\"failedProcessing\"}[1m]))", "interval": "", "legendFormat": "{{taskType}}", "refId": "A" @@ -274,7 +274,7 @@ "uid": "9BonzvTSz" }, "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"reprocessed\"}[1m]))", + "expr": "sum by (taskType) (increase(housekeeper_total{statsName=\"reprocessed\"}[1m]))", "interval": "", "legendFormat": "{{taskType}}", "refId": "A" @@ -360,7 +360,7 @@ "uid": "9BonzvTSz" }, "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"failedReprocessing\"}[1m]))", + "expr": "sum by (taskType) (increase(housekeeper_total{statsName=\"failedReprocessing\"}[1m]))", "interval": "", "legendFormat": "{{taskType}}", "refId": "A" From 9e5c6cf446d8b0c6460c809c0357d35d9c46da47 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 16 Apr 2024 14:16:07 +0300 Subject: [PATCH 047/147] Remove housekeeper.json --- .../provisioning/dashboards/housekeeper.json | 390 ------------------ 1 file changed, 390 deletions(-) delete mode 100644 docker/monitoring/grafana/provisioning/dashboards/housekeeper.json diff --git a/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json b/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json deleted file mode 100644 index b604b96135..0000000000 --- a/docker/monitoring/grafana/provisioning/dashboards/housekeeper.json +++ /dev/null @@ -1,390 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 2, - "links": [], - "liveNow": false, - "panels": [ - { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 10, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "9BonzvTSz" - }, - "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{instance=\"192.168.3.27:8080\",statsName=\"processed\"}[1m]))", - "interval": "", - "legendFormat": "{{taskType}}", - "refId": "A" - } - ], - "title": "Processed", - "type": "timeseries" - }, - { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 10, - "w": 12, - "x": 12, - "y": 0 - }, - "id": 4, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "9BonzvTSz" - }, - "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{statsName=\"failedProcessing\"}[1m]))", - "interval": "", - "legendFormat": "{{taskType}}", - "refId": "A" - } - ], - "title": "Processing failures", - "type": "timeseries" - }, - { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "9BonzvTSz" - }, - "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{statsName=\"reprocessed\"}[1m]))", - "interval": "", - "legendFormat": "{{taskType}}", - "refId": "A" - } - ], - "title": "Reprocessed", - "type": "timeseries" - }, - { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 10 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "9BonzvTSz" - }, - "exemplar": true, - "expr": "sum by (taskType) (increase(housekeeper_total{statsName=\"failedReprocessing\"}[1m]))", - "interval": "", - "legendFormat": "{{taskType}}", - "refId": "A" - } - ], - "title": "Reprocessing failures", - "type": "timeseries" - } - ], - "refresh": "10s", - "schemaVersion": 35, - "style": "dark", - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "Housekeeper", - "uid": "JFJb7voIz", - "version": 5, - "weekStart": "" -} \ No newline at end of file From 49a1a37b0669a3261c1385b668e1fe58765226d6 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 17 Apr 2024 13:07:50 +0300 Subject: [PATCH 048/147] Refactor deletion events handling --- .../server/service/edge/EdgeEventSourcingListener.java | 2 +- .../server/service/entitiy/EntityStateSourcingListener.java | 2 +- .../org/thingsboard/server/dao/tenant/TenantServiceImpl.java | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 32db644275..651339e3a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -100,7 +100,7 @@ public class EdgeEventSourcingListener { public void handleEvent(DeleteEntityEvent event) { TenantId tenantId = event.getTenantId(); EntityType entityType = event.getEntityId().getEntityType(); - if (!tenantService.tenantExists(tenantId)) { + if (!tenantId.isSysTenantId() && !tenantService.tenantExists(tenantId)) { log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); return; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 877e252151..c013650fa7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -123,7 +123,7 @@ public class EntityStateSourcingListener { TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); EntityType entityType = entityId.getEntityType(); - if (entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) { + if (!tenantId.isSysTenantId() && entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) { log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); return; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 97411e82f1..83f1a636c9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -160,9 +160,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Wed, 17 Apr 2024 15:03:02 +0300 Subject: [PATCH 049/147] Processing time stats for Housekeeper --- .../HousekeeperReprocessingService.java | 4 +- .../housekeeper/HousekeeperService.java | 2 +- .../stats/HousekeeperStatsService.java | 18 ++++-- .../common/stats/DefaultStatsFactory.java | 28 +++++++--- .../server/common/stats/StatsFactory.java | 2 + .../server/common/stats/StatsTimer.java | 55 +++++++++++++++++++ 6 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index cdc0d3f880..ae16f925a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -77,6 +77,8 @@ public class HousekeeperReprocessingService { } private void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { + Thread.sleep(config.getTaskReprocessingDelay()); + for (TbProtoQueueMsg msg : msgs) { log.trace("Reprocessing task: {}", msg); try { @@ -89,8 +91,6 @@ public class HousekeeperReprocessingService { } } consumer.commit(); - - Thread.sleep(config.getTaskReprocessingDelay()); } public void submitForReprocessing(ToHousekeeperServiceMsg msg, Throwable error) { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 1f5ed1be58..898ae74d3f 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -130,7 +130,7 @@ public class HousekeeperService { if (log.isDebugEnabled()) { log.debug("[{}] Processed {} in {} ms (attempt {})", task.getTenantId(), task.getDescription(), timing, msg.getTask().getAttempt()); } - statsService.ifPresent(statsService -> statsService.reportProcessed(taskType, msg)); + statsService.ifPresent(statsService -> statsService.reportProcessed(taskType, msg, timing)); } catch (InterruptedException e) { throw e; } catch (Throwable e) { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java index da586aa820..6723d51625 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsTimer; import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; @@ -53,14 +54,14 @@ public class HousekeeperStatsService { String statsStr = stats.values().stream().map(stats -> { String countersStr = stats.getCounters().stream() .filter(counter -> counter.get() > 0) - .map(counter -> counter.getName() + " = " + counter.get()) - .collect(Collectors.joining(", ")); + .map(counter -> counter.getName() + " = [" + counter.get() + "]") + .collect(Collectors.joining(" ")); if (countersStr.isEmpty()) { return null; } else { - return stats.getTaskType() + " {" + countersStr + "}"; + return stats.getTaskType() + " " + countersStr + " avgProcessingTime [" + stats.getProcessingTimer().getAvg() + " ms]"; } - }).filter(Objects::nonNull).collect(Collectors.joining("; ")); + }).filter(Objects::nonNull).collect(Collectors.joining(", ")); if (!statsStr.isEmpty()) { stats.values().forEach(HousekeeperStats::reset); @@ -68,14 +69,14 @@ public class HousekeeperStatsService { } } - public void reportProcessed(HousekeeperTaskType taskType, ToHousekeeperServiceMsg msg) { - // todo: report timings + public void reportProcessed(HousekeeperTaskType taskType, ToHousekeeperServiceMsg msg, long timing) { HousekeeperStats stats = this.stats.get(taskType); if (msg.getTask().getErrorsCount() == 0) { stats.getProcessedCounter().increment(); } else { stats.getReprocessedCounter().increment(); } + stats.getProcessingTimer().record(timing); } public void reportFailure(HousekeeperTaskType taskType, ToHousekeeperServiceMsg msg) { @@ -97,12 +98,15 @@ public class HousekeeperStatsService { private final StatsCounter reprocessedCounter; private final StatsCounter failedReprocessingCounter; + private final StatsTimer processingTimer; + public HousekeeperStats(HousekeeperTaskType taskType, StatsFactory statsFactory) { this.taskType = taskType; this.processedCounter = register("processed", statsFactory); this.failedProcessingCounter = register("failedProcessing", statsFactory); this.reprocessedCounter = register("reprocessed", statsFactory); this.failedReprocessingCounter = register("failedReprocessing", statsFactory); + this.processingTimer = statsFactory.createTimer(StatsType.HOUSEKEEPER, "processingTime", "taskType", taskType.name()); } private StatsCounter register(String statsName, StatsFactory statsFactory) { @@ -113,7 +117,9 @@ public class HousekeeperStatsService { public void reset() { counters.forEach(DefaultCounter::clear); + processingTimer.reset(); } + } } diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index be0c4fe19f..ca97792186 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -19,13 +19,13 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; +import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.StringUtils; -import jakarta.annotation.PostConstruct; import java.util.concurrent.atomic.AtomicInteger; @Service @@ -63,13 +63,7 @@ public class DefaultStatsFactory implements StatsFactory { @Override public StatsCounter createStatsCounter(String key, String statsName, String... otherTags) { - String[] tags = new String[]{STATS_NAME_TAG, statsName}; - if (otherTags.length > 0) { - if (otherTags.length % 2 != 0) { - throw new IllegalArgumentException("Invalid tags array size"); - } - tags = ArrayUtils.addAll(tags, otherTags); - } + String[] tags = getTags(statsName, otherTags); return new StatsCounter( new AtomicInteger(0), metricsEnabled ? meterRegistry.counter(key, tags) : STUB_COUNTER, @@ -111,6 +105,24 @@ public class DefaultStatsFactory implements StatsFactory { return timerBuilder.register(meterRegistry); } + @Override + public StatsTimer createTimer(StatsType type, String name, String... tags) { + return new StatsTimer(name, Timer.builder(type.getName()) + .tags(getTags(name, tags)) + .register(meterRegistry)); + } + + private static String[] getTags(String statsName, String[] otherTags) { + String[] tags = new String[]{STATS_NAME_TAG, statsName}; + if (otherTags.length > 0) { + if (otherTags.length % 2 != 0) { + throw new IllegalArgumentException("Invalid tags array size"); + } + tags = ArrayUtils.addAll(tags, otherTags); + } + return tags; + } + private static class StubCounter implements Counter { @Override public void increment(double amount) { diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java index b9bf0b97fb..a6cc4aa1d4 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java @@ -29,4 +29,6 @@ public interface StatsFactory { Timer createTimer(String key, String... tags); + StatsTimer createTimer(StatsType type, String name, String... tags); + } diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java new file mode 100644 index 0000000000..ca74444619 --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java @@ -0,0 +1,55 @@ +/** + * 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.stats; + +import io.micrometer.core.instrument.Timer; +import lombok.Getter; + +import java.util.concurrent.TimeUnit; + +public class StatsTimer { + + @Getter + private final String name; + private final Timer timer; + + private int count; + private long totalTime; + + public StatsTimer(String name, Timer micrometerTimer) { + this.name = name; + this.timer = micrometerTimer; + } + + public void record(long timeMs) { + count++; + totalTime += timeMs; + timer.record(timeMs, TimeUnit.MILLISECONDS); + } + + public double getAvg() { + if (count == 0) { + return 0.0; + } + return (double) totalTime / count; + } + + public void reset() { + count = 0; + totalTime = 0; + } + +} From fd7c7e9b519443b2fac6cd9d73ae0e9145d437c9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 17 Apr 2024 18:30:49 +0300 Subject: [PATCH 050/147] UI: Improve time series chart tooltip configuration - added font/color settings for tooltip label. --- ...art-with-labels-basic-config.component.html | 15 +++++++++++++++ ...chart-with-labels-basic-config.component.ts | 8 ++++++++ .../range-chart-basic-config.component.html | 18 ++++++++++++++++-- .../range-chart-basic-config.component.ts | 8 ++++++++ ...me-series-chart-basic-config.component.html | 15 +++++++++++++++ ...time-series-chart-basic-config.component.ts | 9 +++++++++ .../bar-chart-with-labels-widget.models.ts | 11 +++++++++++ .../widget/lib/chart/echarts-widget.models.ts | 15 ++++++++------- .../lib/chart/range-chart-widget.models.ts | 11 +++++++++++ .../lib/chart/time-series-chart.models.ts | 9 +++++++++ ...-with-labels-widget-settings.component.html | 15 +++++++++++++++ ...rt-with-labels-widget-settings.component.ts | 6 ++++++ .../range-chart-widget-settings.component.html | 15 +++++++++++++++ .../range-chart-widget-settings.component.ts | 6 ++++++ ...series-chart-widget-settings.component.html | 15 +++++++++++++++ ...e-series-chart-widget-settings.component.ts | 7 +++++++ .../assets/locale/locale.constant-en_US.json | 1 + 17 files changed, 175 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index fa0932175c..6f263f05d3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -239,10 +239,24 @@ +
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
-
-
+
{{ 'tooltip.date' | translate }} @@ -321,6 +334,7 @@ {{ 'tooltip.trigger-axis' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
item.piece) }, showTooltip: settings.showTooltip, + tooltipLabelFont: settings.tooltipLabelFont, + tooltipLabelColor: settings.tooltipLabelColor, tooltipValueFont: settings.tooltipValueFont, tooltipValueColor: settings.tooltipValueColor, tooltipShowDate: settings.tooltipShowDate, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index bfe65a0d7c..4fdc8063aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -752,6 +752,15 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { timeSeriesChartNoAggregationBarWidthDefaultSettings), showTooltip: true, tooltipTrigger: EChartsTooltipTrigger.axis, + tooltipLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + tooltipLabelColor: 'rgba(0, 0, 0, 0.76)', tooltipValueFont: { family: 'Roboto', size: 12, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 4123df9c9f..04b2bff29c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -160,10 +160,24 @@ +
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
{{ 'tooltip.trigger-axis' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
Date: Thu, 18 Apr 2024 10:05:13 +0300 Subject: [PATCH 051/147] UI: Improve time series charts ticks generator function. --- .../widget/lib/chart/time-series-chart.models.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 4fdc8063aa..e03c2a7158 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -974,18 +974,19 @@ export const createTimeSeriesYAxis = (units: string, ticksFormatter = parseFunction(settings.ticksFormatter, ['value']); } } + let configuredTicksGenerator: TimeSeriesChartTicksGenerator; let ticksGenerator: TimeSeriesChartTicksGenerator; let minInterval: number; let interval: number; let splitNumber: number; if (settings.ticksGenerator) { if (isFunction(settings.ticksGenerator)) { - ticksGenerator = settings.ticksGenerator as TimeSeriesChartTicksGenerator; + configuredTicksGenerator = settings.ticksGenerator as TimeSeriesChartTicksGenerator; } else if (settings.ticksGenerator.length) { - ticksGenerator = parseFunction(settings.ticksGenerator, ['extent', 'interval', 'niceTickExtent', 'intervalPrecision']); + configuredTicksGenerator = parseFunction(settings.ticksGenerator, ['extent', 'interval', 'niceTickExtent', 'intervalPrecision']); } } - if (!ticksGenerator) { + if (!configuredTicksGenerator) { interval = settings.interval; if (isUndefinedOrNull(interval)) { if (isDefinedAndNotNull(settings.splitNumber)) { @@ -994,6 +995,11 @@ export const createTimeSeriesYAxis = (units: string, minInterval = (1 / Math.pow(10, decimals)); } } + } else { + ticksGenerator = (extent, ticksInterval, niceTickExtent, intervalPrecision) => { + const ticks = configuredTicksGenerator(extent, ticksInterval, niceTickExtent, intervalPrecision); + return ticks?.filter(tick => tick.value >= extent[0] && tick.value <= extent[1]); + }; } return { id: settings.id, From 558d2421e6c08ea9be813884a87d36cce6e04a3b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 18 Apr 2024 12:12:12 +0300 Subject: [PATCH 052/147] Added details description of EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC, EDGES_RPC_KEEP_ALIVE_TIME_SEC, EDGES_RPC_KEEP_ALIVE_TIMEOUT_SEC --- application/src/main/resources/thingsboard.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 33146b1b82..3623d93291 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1291,11 +1291,17 @@ edges: rpc: # RPC port bind port: "${EDGES_RPC_PORT:7070}" - # Maximum time of keeping alive client RPC connection. Time in seconds + # Specifies the minimum amount of time that should elapse between keepalive pings sent by the client + # This prevents clients from sending pings too frequently, which can be a nuisance to the server (potentially even a denial-of-service attack vector if abused) + # If a client sends pings more frequently than this interval, the server may terminate the connection. client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:1}" - # Maximum time of keep-alive connection. Time in seconds + # Sets the time of inactivity (no read operations on the connection) after which the server will send a keepalive ping to the client. + # This is used to ensure that the connection is still alive and to prevent network intermediaries from dropping connections due to inactivity. + # It's a way for the server to proactively check if the client is still responsive. keep_alive_time_sec: "${EDGES_RPC_KEEP_ALIVE_TIME_SEC:10}" - # Timeout of keep alive RPC connection. Time in seconds + # Specifies the maximum amount of time the server waits for a response to its keepalive ping. + # If the ping is not acknowledged within this time frame, the server considers the connection dead and may close it. + # This timeout helps detect unresponsive clients. keep_alive_timeout_sec: "${EDGES_RPC_KEEP_ALIVE_TIMEOUT_SEC:5}" ssl: # Enable/disable SSL support From 6f2907355634c0a90f95a10eac14f99d2f35e735 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 18 Apr 2024 13:20:04 +0300 Subject: [PATCH 053/147] OAuth2 fix: Detect client registration method according to new spring security API. --- .../server/dao/oauth2/HybridClientRegistrationRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 4c90583e19..004c89bd54 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -52,7 +52,8 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep .userInfoUri(registration.getUserInfoUri()) .userNameAttributeName(registration.getUserNameAttributeName()) .jwkSetUri(registration.getJwkSetUri()) - .clientAuthenticationMethod(new ClientAuthenticationMethod(registration.getClientAuthenticationMethod())) + .clientAuthenticationMethod(registration.getClientAuthenticationMethod().equals("POST") ? + ClientAuthenticationMethod.CLIENT_SECRET_POST : ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .redirectUri(defaultRedirectUriTemplate) .build(); } From 3803081b578355ffe479e62258bdcdf10725a9a5 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 18 Apr 2024 13:25:40 +0300 Subject: [PATCH 054/147] Refactor alarms deletion; separate tenant entities deletion task --- .../AlarmsDeletionTaskProcessor.java | 16 ++--- .../EntitiesDeletionTaskProcessor.java | 27 ++------ .../TenantEntitiesDeletionTaskProcessor.java | 63 +++++++++++++++++++ .../housekeeper/HousekeeperServiceTest.java | 6 +- .../server/dao/alarm/AlarmService.java | 4 +- .../EntitiesDeletionHousekeeperTask.java | 9 +-- .../data/housekeeper/HousekeeperTask.java | 5 +- .../data/housekeeper/HousekeeperTaskType.java | 5 +- ...TenantEntitiesDeletionHousekeeperTask.java | 45 +++++++++++++ .../server/dao/alarm/AlarmDao.java | 3 +- .../server/dao/alarm/BaseAlarmService.java | 6 +- .../dao/housekeeper/CleanUpService.java | 2 +- .../server/dao/sql/alarm/AlarmRepository.java | 21 +++++-- .../server/dao/sql/alarm/JpaAlarmDao.java | 17 ++++- .../dao/service/AbstractServiceTest.java | 6 +- 15 files changed, 174 insertions(+), 61 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TenantEntitiesDeletionTaskProcessor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index 09fa0977d5..cc6cb9bbab 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -24,12 +24,11 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.alarm.AlarmService; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -46,16 +45,19 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor alarms = alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, entityId, last, 128); + List> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128); if (alarms.isEmpty()) { break; } - housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream() - .map(UUIDBased::getId).collect(Collectors.toList()))); - last = alarms.get(alarms.size() - 1); + housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList())); + + TbPair last = alarms.get(alarms.size() - 1); + lastId = new AlarmId(last.getFirst()); + lastCreatedTime = last.getSecond(); log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); } } else { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java index 00c8a7cc61..186a290528 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EntitiesDeletionTaskProcessor.java @@ -24,12 +24,9 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.Dao; -import org.thingsboard.server.dao.entity.EntityDaoRegistry; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; -import java.util.List; import java.util.UUID; @Component @@ -37,35 +34,19 @@ import java.util.UUID; @Slf4j public class EntitiesDeletionTaskProcessor extends HousekeeperTaskProcessor { - private final EntityDaoRegistry entityDaoRegistry; private final EntityServiceRegistry entityServiceRegistry; @Override public void process(EntitiesDeletionHousekeeperTask task) throws Exception { EntityType entityType = task.getEntityType(); TenantId tenantId = task.getTenantId(); - Dao entityDao = entityDaoRegistry.getDao(entityType); EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); - if (task.getEntities() == null) { - UUID last = null; - while (true) { - List entities = entityDao.findIdsByTenantIdAndIdOffset(tenantId, last, 128); - if (entities.isEmpty()) { - break; - } - - housekeeperClient.submitTask(new EntitiesDeletionHousekeeperTask(tenantId, entityType, entities)); - last = entities.get(entities.size() - 1); - log.debug("[{}] Submitted task for deleting {} {}s", tenantId, entities.size(), entityType.getNormalName().toLowerCase()); - } - } else { - for (UUID entityUuid : task.getEntities()) { - EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); - entityService.deleteEntity(tenantId, entityId, true); - } - log.debug("[{}] Deleted {} {}s", tenantId, task.getEntities().size(), entityType.getNormalName().toLowerCase()); + for (UUID entityUuid : task.getEntities()) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); + entityService.deleteEntity(tenantId, entityId, true); } + log.debug("[{}] Deleted {} {}s", tenantId, task.getEntities().size(), entityType.getNormalName().toLowerCase()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TenantEntitiesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TenantEntitiesDeletionTaskProcessor.java new file mode 100644 index 0000000000..386afb1517 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/TenantEntitiesDeletionTaskProcessor.java @@ -0,0 +1,63 @@ +/** + * 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.housekeeper.processor; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.housekeeper.EntitiesDeletionHousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.TenantEntitiesDeletionHousekeeperTask; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.entity.EntityDaoRegistry; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TenantEntitiesDeletionTaskProcessor extends HousekeeperTaskProcessor { + + private final EntityDaoRegistry entityDaoRegistry; + + @Override + public void process(TenantEntitiesDeletionHousekeeperTask task) throws Exception { + EntityType entityType = task.getEntityType(); + TenantId tenantId = task.getTenantId(); + Dao entityDao = entityDaoRegistry.getDao(entityType); + + UUID last = null; + while (true) { + List entities = entityDao.findIdsByTenantIdAndIdOffset(tenantId, last, 128); + if (entities.isEmpty()) { + break; + } + + housekeeperClient.submitTask(new EntitiesDeletionHousekeeperTask(tenantId, entityType, entities)); + last = entities.get(entities.size() - 1); + log.debug("[{}] Submitted task for deleting {} {}s", tenantId, entities.size(), entityType.getNormalName().toLowerCase()); + } + } + + @Override + public HousekeeperTaskType getTaskType() { + return HousekeeperTaskType.DELETE_TENANT_ENTITIES; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index e66d3ed86a..e8f1a43ad1 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -360,7 +360,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { } private void verifyNoAlarms(EntityId entityId) { - assertThat(alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, entityId, null, 10)).isEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, 0, null, 10)).isEmpty(); } private void createAttribute(EntityId entityId, AttributeScope scope, String key) throws Exception { @@ -402,7 +402,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { List entityAlarms = alarmDao.findEntityAlarmRecords(tenantId, alarm.getId()); assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(deviceId) && entityAlarm.getAlarmType().equals(alarm.getType())); assertThat(entityAlarms).anyMatch(entityAlarm -> entityAlarm.getEntityId().equals(propagatedEntityId) && entityAlarm.getAlarmType().equals(alarm.getType())); - assertThat(alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, deviceId, null, 10)).isNotEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); } private void createAlarm(DeviceId deviceId) { @@ -412,7 +412,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { .severity(AlarmSeverity.CRITICAL) .type("test alarm for " + deviceId + " " + RandomStringUtils.randomAlphabetic(10)) .build(), Alarm.class); - assertThat(alarmService.findAlarmIdsByOriginatorIdAndIdOffset(tenantId, deviceId, null, 10)).isNotEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); } private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index ee584ff856..5bf6b95853 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -38,11 +38,13 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.UUID; public interface AlarmService extends EntityDaoService { @@ -107,7 +109,7 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); - List findAlarmIdsByOriginatorIdAndIdOffset(TenantId tenantId, EntityId originatorId, AlarmId idOffset, int limit); + List> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit); int deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java index d90fa6de94..b98af48d42 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.housekeeper; import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -30,16 +29,12 @@ import java.util.UUID; @Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { private EntityType entityType; private List entities; - protected EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { - this(tenantId, entityType, null); - } - public EntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType, List entities) { super(tenantId, tenantId, HousekeeperTaskType.DELETE_ENTITIES); this.entityType = entityType; @@ -49,7 +44,7 @@ public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { @JsonIgnore @Override public String getDescription() { - return entityType.getNormalName().toLowerCase() + "s deletion" + (entities != null ? " (" + entities + ")" : ""); + return entityType.getNormalName().toLowerCase() + "s deletion (" + entities + ")"; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java index 8354b2b861..8757b1121e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java @@ -36,6 +36,7 @@ import java.io.Serializable; @JsonSubTypes({ @Type(name = "DELETE_TS_HISTORY", value = TsHistoryDeletionHousekeeperTask.class), @Type(name = "DELETE_LATEST_TS", value = LatestTsDeletionHousekeeperTask.class), + @Type(name = "DELETE_TENANT_ENTITIES", value = TenantEntitiesDeletionHousekeeperTask.class), @Type(name = "DELETE_ENTITIES", value = EntitiesDeletionHousekeeperTask.class), @Type(name = "DELETE_ALARMS", value = AlarmsDeletionHousekeeperTask.class), @Type(name = "UNASSIGN_ALARMS", value = AlarmsUnassignHousekeeperTask.class) @@ -76,8 +77,8 @@ public class HousekeeperTask implements Serializable { return new AlarmsDeletionHousekeeperTask(tenantId, entityId); } - public static HousekeeperTask deleteEntities(TenantId tenantId, EntityType entityType) { - return new EntitiesDeletionHousekeeperTask(tenantId, entityType); + public static HousekeeperTask deleteTenantEntities(TenantId tenantId, EntityType entityType) { + return new TenantEntitiesDeletionHousekeeperTask(tenantId, entityType); } @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java index 572e85f2cf..edd13b3d5d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java @@ -22,14 +22,15 @@ import lombok.RequiredArgsConstructor; @Getter public enum HousekeeperTaskType { - DELETE_ENTITIES("entities deletion"), DELETE_ATTRIBUTES("attributes deletion"), DELETE_TELEMETRY("telemetry deletion"), DELETE_LATEST_TS("latest telemetry deletion"), DELETE_TS_HISTORY("timeseries history deletion"), DELETE_EVENTS("events deletion"), DELETE_ALARMS("alarms deletion"), - UNASSIGN_ALARMS("alarms unassigning"); + UNASSIGN_ALARMS("alarms unassigning"), + DELETE_TENANT_ENTITIES("tenant entities deletion"), + DELETE_ENTITIES("entities deletion"); private final String description; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java new file mode 100644 index 0000000000..f6c6d5030c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java @@ -0,0 +1,45 @@ +/** + * 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.housekeeper; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class TenantEntitiesDeletionHousekeeperTask extends HousekeeperTask { + + private EntityType entityType; + + public TenantEntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { + super(tenantId, tenantId, HousekeeperTaskType.DELETE_TENANT_ENTITIES); + this.entityType = entityType; + } + + @JsonIgnore + @Override + public String getDescription() { + return entityType.getNormalName().toLowerCase() + "s deletion"; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 03038b3bfe..570256e98c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.Dao; import java.util.Collection; @@ -80,7 +81,7 @@ public interface AlarmDao extends Dao { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink); - PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, AlarmId idOffset, PageLink pageLink); + PageData> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit); void createEntityAlarmRecord(EntityAlarm entityAlarm); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 9d0940a605..f0fc18c827 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; @@ -75,6 +76,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -313,9 +315,9 @@ public class BaseAlarmService extends AbstractCachedEntityService findAlarmIdsByOriginatorIdAndIdOffset(TenantId tenantId, EntityId originatorId, AlarmId idOffset, int limit) { + public List> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit) { log.trace("[{}] Executing findAlarmIdsByOriginatorIdAndIdOffset [{}][{}]", tenantId, originatorId, idOffset); - return alarmDao.findAlarmIdsByOriginatorId(tenantId, originatorId, idOffset, new PageLink(limit)).getData(); + return alarmDao.findAlarmIdsByOriginatorId(tenantId, originatorId, createdTimeOffset, idOffset, limit).getData(); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 48531aab90..082a60f44a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -77,7 +77,7 @@ public class CleanUpService { public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) { for (EntityType entityType : entityTypes) { - housekeeperClient.submitTask(HousekeeperTask.deleteEntities(tenantId, entityType)); + housekeeperClient.submitTask(HousekeeperTask.deleteTenantEntities(tenantId, entityType)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index ff9a2ed7d6..7bb16a9798 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -24,6 +24,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -334,12 +335,20 @@ public interface AlarmRepository extends JpaRepository { Page findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId, @Param("assigneeId") UUID assigneeId, Pageable pageable); // using Slice so that count query is not executed - @Query(value = "SELECT id FROM alarm WHERE tenant_id = :tenantId AND originator_id = :originatorId " + - "AND (cast(:idOffset as uuid) IS NULL OR id > cast(:idOffset as uuid))", nativeQuery = true) - Slice findAlarmIdsByOriginatorId(@Param("tenantId") UUID tenantId, - @Param("originatorId") UUID originatorId, - @Param("idOffset") UUID idOffset, - Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.util.TbPair(a.id, a.createdTime) " + + "FROM AlarmEntity a WHERE a.originatorId = :originatorId " + + "AND (a.createdTime > :createdTimeOffset OR " + + "(a.createdTime = :createdTimeOffset AND a.id > :idOffset))") + Slice> findAlarmIdsByOriginatorId(@Param("originatorId") UUID originatorId, + @Param("createdTimeOffset") long createdTimeOffset, + @Param("idOffset") UUID idOffset, + Pageable pageable); + + // using Slice so that count query is not executed + @Query("SELECT new org.thingsboard.server.common.data.util.TbPair(a.id, a.createdTime) " + + "FROM AlarmEntity a WHERE a.originatorId = :originatorId") + Slice> findAlarmIdsByOriginatorId(@Param("originatorId") UUID originatorId, + Pageable pageable); @Query(value = "SELECT create_or_update_active_alarm(:t_id, :c_id, :a_id, :a_created_ts, :a_o_id, :a_o_type, :a_type, :a_severity, " + ":a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index da603a6c6f..c3e6e123f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -21,6 +21,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; @@ -48,9 +50,11 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.model.ModelConstants; @@ -69,6 +73,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static org.thingsboard.server.common.data.page.SortOrder.Direction.ASC; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; import static org.thingsboard.server.dao.DaoUtil.toPageable; @@ -297,9 +302,15 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, AlarmId idOffset, PageLink pageLink) { - return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByOriginatorId(tenantId.getId(), originatorId.getId(), idOffset != null ? idOffset.getId() : null, DaoUtil.toPageable(pageLink))) - .mapData(AlarmId::new); + public PageData> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit) { + Slice> result; + Pageable pageRequest = toPageable(new PageLink(limit), List.of(SortOrder.of("createdTime", ASC), SortOrder.of("id", ASC))); + if (idOffset == null) { + result = alarmRepository.findAlarmIdsByOriginatorId(originatorId.getId(), pageRequest); + } else { + result = alarmRepository.findAlarmIdsByOriginatorId(originatorId.getId(), createdTimeOffset, idOffset.getId(), pageRequest); + } + return DaoUtil.pageToPageData(result); } @Override diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index f1e1fd0e76..6127a4192a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -42,7 +42,7 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; -import org.thingsboard.server.common.data.housekeeper.EntitiesDeletionHousekeeperTask; +import org.thingsboard.server.common.data.housekeeper.TenantEntitiesDeletionHousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -142,8 +142,8 @@ public abstract class AbstractServiceTest { @Bean public HousekeeperClient housekeeperClient() { return task -> { - if (task.getTaskType() == HousekeeperTaskType.DELETE_ENTITIES) { - EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(((EntitiesDeletionHousekeeperTask) task).getEntityType()); + if (task.getTaskType() == HousekeeperTaskType.DELETE_TENANT_ENTITIES) { + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(((TenantEntitiesDeletionHousekeeperTask) task).getEntityType()); entityService.deleteByTenantId(task.getTenantId()); } }; From f0cd43db5c2d91c2aed6594a314146264a1cf9a9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 18 Apr 2024 13:34:47 +0300 Subject: [PATCH 055/147] LWM2M: Catch client serialization error. --- .../lwm2m/server/store/TbRedisLwM2MClientStore.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java index 542327cccc..17f9b4a978 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java @@ -94,9 +94,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { if (client.getState().equals(LwM2MClientState.UNREGISTERED)) { log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception()); } else { - byte[] clientSerialized = serialize(client); - try (var connection = connectionFactory.getConnection()) { - connection.getSet(getKey(client.getEndpoint()), clientSerialized); + try { + byte[] clientSerialized = serialize(client); + try (var connection = connectionFactory.getConnection()) { + connection.getSet(getKey(client.getEndpoint()), clientSerialized); + } + } catch (Exception e) { + log.warn("Failed to serialize client: {}", client, e); } } } From e82cfe67c31e6dc4dfc7f433eb45c841f42a6e6f Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 17:07:21 +0200 Subject: [PATCH 056/147] Kafka 3.7.0 patch updated for NetworkReceive class --- .../kafka/common/network/NetworkReceive.java | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/apache/kafka/common/network/NetworkReceive.java b/application/src/main/java/org/apache/kafka/common/network/NetworkReceive.java index 38764d08de..80192520ca 100644 --- a/application/src/main/java/org/apache/kafka/common/network/NetworkReceive.java +++ b/application/src/main/java/org/apache/kafka/common/network/NetworkReceive.java @@ -38,10 +38,10 @@ import java.util.stream.Collectors; */ public class NetworkReceive implements Receive { - public final static String UNKNOWN_SOURCE = ""; - public final static int UNLIMITED = -1; - public final static int TB_MAX_REQUESTED_BUFFER_SIZE = 100 * 1024 * 1024; - public final static int TB_LOG_REQUESTED_BUFFER_SIZE = 10 * 1024 * 1024; + public static final String UNKNOWN_SOURCE = ""; + public static final int UNLIMITED = -1; + public static final int TB_MAX_REQUESTED_BUFFER_SIZE = 100 * 1024 * 1024; + public static final int TB_LOG_REQUESTED_BUFFER_SIZE = 10 * 1024 * 1024; private static final Logger log = LoggerFactory.getLogger(NetworkReceive.class); private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); @@ -54,27 +54,16 @@ public class NetworkReceive implements Receive { public NetworkReceive(String source, ByteBuffer buffer) { - this.source = source; + this(TB_MAX_REQUESTED_BUFFER_SIZE, source); this.buffer = buffer; - this.size = null; - this.maxSize = TB_MAX_REQUESTED_BUFFER_SIZE; - this.memoryPool = MemoryPool.NONE; } public NetworkReceive(String source) { - this.source = source; - this.size = ByteBuffer.allocate(4); - this.buffer = null; - this.maxSize = TB_MAX_REQUESTED_BUFFER_SIZE; - this.memoryPool = MemoryPool.NONE; + this(TB_MAX_REQUESTED_BUFFER_SIZE, source); } public NetworkReceive(int maxSize, String source) { - this.source = source; - this.size = ByteBuffer.allocate(4); - this.buffer = null; - this.maxSize = getMaxSize(maxSize); - this.memoryPool = MemoryPool.NONE; + this(maxSize, source, MemoryPool.NONE); } public NetworkReceive(int maxSize, String source, MemoryPool memoryPool) { From a3e7e0b4a1625d107cfbe56bee0f5eb68e16fdbf Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 17:11:29 +0200 Subject: [PATCH 057/147] major dependency update --- common/cluster-api/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/message/pom.xml | 2 +- .../server/common/msg/tools/TbRateLimits.java | 4 +- dao/pom.xml | 10 +- msa/black-box-tests/pom.xml | 2 +- pom.xml | 253 +++++++++++------- 7 files changed, 167 insertions(+), 108 deletions(-) diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index dc4a829504..1ea925804f 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -61,7 +61,7 @@ jakarta.annotation-api - com.github.fge + com.github.java-json-tools json-schema-validator diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index dbc9251ba3..0aad8d7183 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -57,7 +57,7 @@ jakarta.annotation-api - com.github.fge + com.github.java-json-tools json-schema-validator diff --git a/common/message/pom.xml b/common/message/pom.xml index e6b61916b0..8a3e278a4e 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -61,7 +61,7 @@ logback-classic - com.github.vladimir-bukhtoyarov + com.bucket4j bucket4j-core diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java index d93f8982c6..9ed1ac4384 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java @@ -16,7 +16,7 @@ package org.thingsboard.server.common.msg.tools; import io.github.bucket4j.Bandwidth; -import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Bucket; import io.github.bucket4j.Refill; import io.github.bucket4j.local.LocalBucket; import io.github.bucket4j.local.LocalBucketBuilder; @@ -38,7 +38,7 @@ public class TbRateLimits { } public TbRateLimits(String limitsConfiguration, boolean refillIntervally) { - LocalBucketBuilder builder = Bucket4j.builder(); + LocalBucketBuilder builder = Bucket.builder(); boolean initialized = false; for (String limitSrc : limitsConfiguration.split(",")) { long capacity = Long.parseLong(limitSrc.split(":")[0]); diff --git a/dao/pom.xml b/dao/pom.xml index ab62d8f013..c34f47f2d6 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -113,8 +113,8 @@ commons-lang3 - commons-collections - commons-collections + org.apache.commons + commons-collections4 com.fasterxml.jackson.core @@ -125,8 +125,8 @@ hibernate-validator - org.glassfish - jakarta.el + jakarta.el + jakarta.el-api org.springframework @@ -222,7 +222,7 @@ org.elasticsearch.client - rest + elasticsearch-rest-client org.eclipse.leshan diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index cbbd76a1f6..f1b7525438 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -85,7 +85,7 @@ org.hamcrest - hamcrest-all + hamcrest test diff --git a/pom.xml b/pom.xml index 332a88b758..ab29084d79 100755 --- a/pom.xml +++ b/pom.xml @@ -38,129 +38,133 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} - 2.1.1 - 4.0.0 + 3.0.0 + 4.0.2 2.4.0-b180830.0359 - 4.0.2 - 3.1.0 - 3.1.0 - 6.0.9 - 6.1.0 - 6.1.0 - 3.1.0 - 4.3.2 - 0.9.1 - 2.0.7 - 2.19.0 - 1.4.14 - 0.10 - 4.15.0 - 4.0.5 - 3.11.14 - 31.1-jre - 2.6.1 - 3.12.0 - 1.15 - 2.11.0 - 1.2 - 1.4 - 4.5.13 - 4.4.14 - 2.8.1 - 2.15.3 - 2.15.3 - 1.3.4 - 4.2.1 - 2.2.6 + 4.0.5 + 3.2.4 + 3.2.5 + 3.2.5 + 6.1.6 + 6.2.4 + 6.2.4 + 5.1.2 + 0.12.5 + 2.0.13 + 2.23.1 + 1.5.5 + 0.10 + 4.17.0 + 4.2.25 + 3.11.17 + 33.1.0-jre + 3.1.8 + 3.14.0 + 1.16.1 + 2.16.1 + 1.3.1 + 1.10.0 + 5.3.1 + 5.2.4 + 4.5.14 + 4.4.16 + 2.12.7 + 2.17.0 + 2.17.0 + 1.7.0 + 4.4.0 + 2.2.14 3.11.0 2.0.0-M14 - 2.9.0 - 2.3.30 + 2.10.1 + 2.3.32 2.0.1 - 5.5.0 + 5.6.0 3.9.2 - 3.21.9 - 1.42.1 + 3.25.3 + 1.63.0 1.2.0 - 1.18.26 - 1.2.4 + 1.18.32 + 1.2.5 1.2.5 - 4.1.93.Final - 2.0.61.Final - 1.1.7 - 1.7.0 - 4.8.0 - 3.0.0-M9 - 3.0.2 + 4.1.109.Final + 2.0.65.Final + 1.1.18 + 1.7.1 + 5.21.0 + 3.2.5 + 3.4.0 2.4.0TB - 2.2.20 - 0.7 - 1.18.2 - 1.69 + 2.2.21 + 0.8 + 1.19.0 + 1.70 2.0.1 42.7.3 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* - 5.0.2 + 8.13.2 0.4.2 15.4 - 3.2.0 - 4.1.1 - 2.7.7 + 3.7.0 + 8.10.1 + 3.5.3 2.2 - 1.11.747 - 1.105.0 - 2.1.0 + 1.12.701 + 1.128.1 + 2.37.1 3.6.7 - 1.5.0 - 1.5.4 + 1.6.4 + 1.6.1 1.9.4 - 3.2.2 - 1.11.0 + 4.4 + 1.12.5 1.0.4TB - 3.4.0 - 9.6.1 - 8.0.0.Final - 3.5.2 - 4.0.2 + 3.7.1 + 10.1.3 + 8.0.1.Final + 3.7.3 + 6.0.0 3.0.2 - 1.7.3 - 2.8.5 + 1.7.5 + 3.8.0 + 2.9.0 - 4.2.0 - 2.7.2 - 1.5.2 - 5.9.3 - 2.6.0 + 4.2.1 + 2.7.3 + 1.5.6 + 5.10.2 5.15.0 1.3.0 1.2.7 5.0.0 - 7.6.1 - 3.23.1 - 5.2.0 - 1.3 - 1.18.3 + 7.10.1 + 3.25.3 + 5.4.0 + 2.2 + 1.19.7 1.12 - 3.0.0 - 6.1.0.202203080745-r + 4.19.1 + 5.8.0 + 2.27.0 + 2.12.0 + + 3.0.2 + 6.9.0.202403050737-r 0.4.8 1.0.0 - 4.6.0 - 5.2.0 - 2.21.0 - 2.12.0 - 1.12.1 - 6.4.2 - 1.34.1 + + 1.39.0 + 6.6.0 + 1.35.0 1.17 2.19.0 - 8.0.1 + 9.2.0 @@ -1432,6 +1436,16 @@ commons-csv ${commons-csv.version} + + org.apache.httpcomponents.client5 + httpclient5 + ${apache-httpclient5.version} + + + org.apache.httpcomponents.core5 + httpcore5 + ${apache-httpcore5.version} + org.apache.httpcomponents httpclient @@ -1498,7 +1512,7 @@ ${auth0-jwt.version} - com.github.fge + com.github.java-json-tools json-schema-validator ${json-schema-validator.version} @@ -1627,6 +1641,51 @@ grpc-protobuf ${grpc.version} + + io.grpc + grpc-protobuf-lite + ${grpc.version} + + + io.grpc + grpc-util + ${grpc.version} + + + io.grpc + grpc-googleapis + ${grpc.version} + + + io.grpc + grpc-xds + ${grpc.version} + + + io.grpc + grpc-services + ${grpc.version} + + + io.grpc + grpc-context + ${grpc.version} + + + io.grpc + grpc-inprocess + ${grpc.version} + + + io.grpc + grpc-core + ${grpc.version} + + + io.grpc + grpc-grpclb + ${grpc.version} + io.grpc grpc-stub @@ -1736,7 +1795,7 @@ org.hamcrest - hamcrest-all + hamcrest ${hamcrest.version} test @@ -1836,7 +1895,7 @@ org.elasticsearch.client - rest + elasticsearch-rest-client ${elasticsearch.version} @@ -1845,7 +1904,7 @@ ${delight-nashorn-sandbox.version} - com.github.vladimir-bukhtoyarov + com.bucket4j bucket4j-core ${bucket4j.version} @@ -1910,8 +1969,8 @@ ${commons-beanutils.version} - commons-collections - commons-collections + org.apache.commons + commons-collections4 ${commons-collections.version} @@ -1979,8 +2038,8 @@ ${hypersistence-utils.version} - org.glassfish - jakarta.el + jakarta.el + jakarta.el-api ${jakarta.el.version} From 60ccd97832b9fe48e9a15404dd9174d99d5047d2 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 17:22:14 +0200 Subject: [PATCH 058/147] org.apache.commons.collections replaced with org.apache.commons.collections4 --- .../server/actors/device/DeviceActorMessageProcessor.java | 2 +- .../server/controller/NotificationTargetController.java | 2 +- .../server/service/device/DeviceBulkImportService.java | 2 +- .../rule/trigger/DeviceActivityTriggerProcessor.java | 2 +- .../rule/trigger/EdgeCommunicationFailureTriggerProcessor.java | 2 +- .../rule/trigger/EdgeConnectionTriggerProcessor.java | 2 +- .../RuleEngineComponentLifecycleEventTriggerProcessor.java | 2 +- .../service/sync/ie/exporting/impl/DashboardExportService.java | 2 +- .../sync/ie/importing/impl/NotificationTargetImportService.java | 2 +- .../thingsboard/server/service/ws/DefaultWebSocketService.java | 2 +- .../dao/notification/DefaultNotificationTargetService.java | 2 +- .../org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java | 2 +- .../org/thingsboard/server/dao/rule/BaseRuleChainService.java | 2 +- .../server/dao/service/validator/RuleChainDataValidator.java | 2 +- .../rule/engine/metadata/TbAbstractGetAttributesNode.java | 2 +- .../rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java | 2 +- .../rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 87d5c8a63a..253169a51d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -23,7 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.LinkedHashMapRemoveEldest; import org.thingsboard.server.actors.ActorSystemContext; diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java index 8f0a2fb3cd..8436e8fe35 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 289d6b2058..0c3146a6c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java index 7380b8857a..e2d5484f59 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.notification.rule.trigger; import lombok.RequiredArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceId; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java index 38d8ae9805..2bc9a65cb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.notification.rule.trigger; import lombok.RequiredArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.notification.info.EdgeCommunicationFailureNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java index ed1ee74622..61b5c8ed56 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.notification.rule.trigger; import lombok.RequiredArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.notification.info.EdgeConnectionNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java index 4c92e9b92b..c17805e1b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.notification.rule.trigger; import lombok.RequiredArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.stereotype.Service; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java index 8acb8990b8..3b2cb626ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.sync.ie.exporting.impl; import com.fasterxml.jackson.databind.JsonNode; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java index 22ab41dd8a..4e0fdc33b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.sync.ie.importing.impl; import lombok.RequiredArgsConstructor; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 424f739377..e41d1821c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -24,7 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.socket.CloseStatus; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index 90f54a316a..94c339335b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -52,7 +52,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -import static org.apache.commons.collections.CollectionUtils.isNotEmpty; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; @Service @Slf4j diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 6535b54a2c..5bb7ae163e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.oauth2; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.BaseData; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 8e1af4a531..cb9e9efdee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 222aaa3eeb..9f1eda9528 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.service.validator; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index 2992bf27d5..c7ef41d909 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -21,7 +21,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java index 79c18f9a8c..17f19e3b85 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.data.DeviceRelationsQuery; import org.thingsboard.server.common.data.device.DeviceSearchQuery; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java index eadef5a013..82026f97dd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.id.EntityId; From 767edd4608fd894ec17629115480910552facac3 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 17:31:22 +0200 Subject: [PATCH 059/147] ElasticsearchAuditLogSink refactored performRequestAsync to be compatible with 8.x.x version --- .../dao/audit/sink/ElasticsearchAuditLogSink.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java index 4d2c10a7cd..5d143ef009 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java @@ -25,6 +25,7 @@ import org.apache.http.client.CredentialsProvider; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseListener; import org.elasticsearch.client.RestClient; @@ -124,12 +125,10 @@ public class ElasticsearchAuditLogSink implements AuditLogSink { jsonContent, ContentType.APPLICATION_JSON); - restClient.performRequestAsync( - HttpMethod.POST.name(), - String.format("/%s/%s", getIndexName(auditLogEntry.getTenantId()), INDEX_TYPE), - Collections.emptyMap(), - entity, - responseListener); + Request request = new Request(HttpMethod.POST.name(),String.format("/%s/%s", getIndexName(auditLogEntry.getTenantId()), INDEX_TYPE)); + request.setEntity(entity); + + restClient.performRequestAsync(request, responseListener); } private String createElasticJsonRecord(AuditLog auditLog) { From 90c93245fa9ee9ac0c83c9da5771f97b438efbda Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 17:35:11 +0200 Subject: [PATCH 060/147] collection util static imports replaced org.apache.commons.collections4 --- .../rule/trigger/AlarmAssignmentTriggerProcessor.java | 2 +- .../notification/rule/trigger/AlarmCommentTriggerProcessor.java | 2 +- .../notification/rule/trigger/AlarmTriggerProcessor.java | 2 +- .../rule/trigger/EntitiesLimitTriggerProcessor.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java index 9cc7416e34..36c2d91b8e 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger; -import static org.apache.commons.collections.CollectionUtils.isEmpty; +import static org.apache.commons.collections4.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @Service diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java index 392f68dc9e..bd2bccecb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Notif import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; import org.thingsboard.server.dao.entity.EntityService; -import static org.apache.commons.collections.CollectionUtils.isEmpty; +import static org.apache.commons.collections4.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @Service diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java index 99d58d40a9..b9047cac6b 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; -import static org.apache.commons.collections.CollectionUtils.isNotEmpty; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @Service diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java index e1db043188..24000760b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; -import static org.apache.commons.collections.CollectionUtils.isNotEmpty; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; @Service @RequiredArgsConstructor From 1f5c95a375ab694ff08709cf9096dd773e0b90c7 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 17:40:20 +0200 Subject: [PATCH 061/147] Jwts.parser().build() refactored for a newer version --- .../server/service/security/model/token/JwtTokenFactory.java | 3 ++- .../service/security/model/token/OAuth2AppTokenFactory.java | 2 +- .../org/thingsboard/server/controller/AbstractWebTest.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 08622f578a..7270398c25 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -183,7 +183,7 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - Claims claims = Jwts.claims().setSubject(principal.getValue()); + Claims claims = Jwts.claims().setSubject(principal.getValue()).build(); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(SCOPES, scopes); if (securityUser.getSessionId() != null) { @@ -204,6 +204,7 @@ public class JwtTokenFactory { try { return Jwts.parser() .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) + .build() .parseClaimsJws(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index 7f956f6970..dd30e4bc28 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -40,7 +40,7 @@ public class OAuth2AppTokenFactory { public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { Jws jwsClaims; try { - jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); + jwsClaims = Jwts.parser().setSigningKey(appSecret).build().parseClaimsJws(appToken); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new IllegalArgumentException("Invalid Application token: ", ex); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index e14b041e62..89d35f0756 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -563,7 +563,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { int i = token.lastIndexOf('.'); Assert.assertTrue(i > 0); String withoutSignature = token.substring(0, i + 1); - Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); + Jwt jwsClaims = Jwts.parser().build().parseClaimsJwt(withoutSignature); Claims claims = jwsClaims.getBody(); String subject = claims.getSubject(); Assert.assertEquals(username, subject); From c4918d97c9c6dde873990e5b1377ec6c44f1ec62 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 20:24:51 +0200 Subject: [PATCH 062/147] hibernate UUID cast removed for AlarmRepository and DeviceRepository --- .../server/dao/sql/alarm/AlarmRepository.java | 40 +++++++++---------- .../server/dao/sql/alarm/JpaAlarmDao.java | 14 +++---- .../dao/sql/device/DeviceRepository.java | 12 +++--- .../server/dao/sql/device/JpaDeviceDao.java | 6 +-- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 83827973ba..735234ffaa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -57,7 +57,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true)) " @@ -74,7 +74,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true))") @@ -87,7 +87,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -106,7 +106,7 @@ public interface AlarmRepository extends JpaRepository { // "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true)) " @@ -127,7 +127,7 @@ public interface AlarmRepository extends JpaRepository { // "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true))") @@ -142,7 +142,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -153,7 +153,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true)) ", @@ -165,7 +165,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true))") @@ -176,7 +176,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -191,7 +191,7 @@ public interface AlarmRepository extends JpaRepository { // "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true)) ", @@ -207,7 +207,7 @@ public interface AlarmRepository extends JpaRepository { // "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true))") @@ -220,7 +220,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -231,7 +231,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true)) " @@ -244,7 +244,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true))") @@ -256,7 +256,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -271,7 +271,7 @@ public interface AlarmRepository extends JpaRepository { // "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true)) " @@ -288,7 +288,7 @@ public interface AlarmRepository extends JpaRepository { // "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + "AND (:searchText IS NULL OR (ilike(a.type, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.severity, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.status, CONCAT('%', :searchText, '%')) = true))") @@ -302,7 +302,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -314,7 +314,7 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + - "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId))") + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId)") Set findAlarmSeverities(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, @@ -322,7 +322,7 @@ public interface AlarmRepository extends JpaRepository { @Param("clearFilter") boolean clearFilter, @Param("ackFilterEnabled") boolean ackFilterEnabled, @Param("ackFilter") boolean ackFilter, - @Param("assigneeId") String assigneeId); + @Param("assigneeId") UUID assigneeId); @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.createdTime < :time AND a.endTs < :time") Page findAlarmsIdsByEndTsBeforeAndTenantId(@Param("time") Long time, @Param("tenantId") UUID tenantId, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index cbadbb4951..a8b74c2b1b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -154,7 +154,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - DaoUtil.getStringId(query.getAssigneeId()), + DaoUtil.getId(query.getAssigneeId()), query.getPageLink().getTextSearch(), DaoUtil.toPageable(query.getPageLink()) ) @@ -169,7 +169,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - DaoUtil.getStringId(query.getAssigneeId()), + DaoUtil.getId(query.getAssigneeId()), query.getPageLink().getTextSearch(), DaoUtil.toPageable(query.getPageLink()) ) @@ -191,7 +191,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - DaoUtil.getStringId(query.getAssigneeId()), + DaoUtil.getId(query.getAssigneeId()), query.getPageLink().getTextSearch(), DaoUtil.toPageable(query.getPageLink()) ) @@ -219,7 +219,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - DaoUtil.getStringId(query.getAssigneeId()), + DaoUtil.getId(query.getAssigneeId()), query.getPageLink().getTextSearch(), DaoUtil.toPageable(query.getPageLink()) ) @@ -236,7 +236,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - DaoUtil.getStringId(query.getAssigneeId()), + DaoUtil.getId(query.getAssigneeId()), query.getPageLink().getTextSearch(), DaoUtil.toPageable(query.getPageLink()) ) @@ -262,7 +262,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - DaoUtil.getStringId(query.getAssigneeId()), + DaoUtil.getId(query.getAssigneeId()), query.getPageLink().getTextSearch(), DaoUtil.toPageable(query.getPageLink()) ) @@ -281,7 +281,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - assigneeId); + UUID.fromString(assigneeId)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index bfc310cddd..289da44626 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -126,20 +126,20 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceInfoEntity d " + "WHERE d.tenantId = :tenantId " + - "AND (:customerId IS NULL OR d.customerId = uuid(:customerId)) " + - "AND (:edgeId IS NULL OR d.id IN (SELECT re.toId FROM RelationEntity re WHERE re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' AND re.relationType = 'Contains' AND re.fromType = 'EDGE' AND re.fromId = uuid(:edgeId))) " + + "AND (:customerId IS NULL OR d.customerId = :customerId) " + + "AND (:edgeId IS NULL OR d.id IN (SELECT re.toId FROM RelationEntity re WHERE re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' AND re.relationType = 'Contains' AND re.fromType = 'EDGE' AND re.fromId = :edgeId)) " + "AND ((:deviceType) IS NULL OR d.type = :deviceType) " + - "AND (:deviceProfileId IS NULL OR d.deviceProfileId = uuid(:deviceProfileId)) " + + "AND (:deviceProfileId IS NULL OR d.deviceProfileId = :deviceProfileId) " + "AND ((:filterByActive) = FALSE OR d.active = :deviceActive) " + "AND (:textSearch IS NULL OR ilike(d.name, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(d.label, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(d.type, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(d.customerTitle, CONCAT('%', :textSearch, '%')) = true)") Page findDeviceInfosByFilter(@Param("tenantId") UUID tenantId, - @Param("customerId") String customerId, - @Param("edgeId") String edgeId, + @Param("customerId") UUID customerId, + @Param("edgeId") UUID edgeId, @Param("deviceType") String type, - @Param("deviceProfileId") String deviceProfileId, + @Param("deviceProfileId") UUID deviceProfileId, @Param("filterByActive") boolean filterByActive, @Param("deviceActive") boolean active, @Param("textSearch") String textSearch, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 9aa9a48fab..64f37f3838 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -110,10 +110,10 @@ public class JpaDeviceDao extends JpaAbstractDao implement return DaoUtil.toPageData( deviceRepository.findDeviceInfosByFilter( filter.getTenantId().getId(), - DaoUtil.getStringId(filter.getCustomerId()), - DaoUtil.getStringId(filter.getEdgeId()), + DaoUtil.getId(filter.getCustomerId()), + DaoUtil.getId(filter.getEdgeId()), filter.getType(), - DaoUtil.getStringId(filter.getDeviceProfileId()), + DaoUtil.getId(filter.getDeviceProfileId()), filter.getActive() != null, Boolean.TRUE.equals(filter.getActive()), pageLink.getTextSearch(), From 38f3a16a31e81b6b8c54b449bb8c3e4abc0a0522 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 20:29:34 +0200 Subject: [PATCH 063/147] ThingsboardPostgreSQLDialect fixed with type cast to text. SqlExceptionHelper - ERROR: operator does not exist: character varying ~~* bytea. Hint: No operator matches the given name and argument types. You might need to add explicit type casts. --- .../thingsboard/server/dao/ThingsboardPostgreSQLDialect.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ThingsboardPostgreSQLDialect.java b/dao/src/main/java/org/thingsboard/server/dao/ThingsboardPostgreSQLDialect.java index 8acb13b5bb..5da1c32616 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ThingsboardPostgreSQLDialect.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ThingsboardPostgreSQLDialect.java @@ -15,23 +15,26 @@ */ package org.thingsboard.server.dao; +import lombok.extern.slf4j.Slf4j; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.type.BasicTypeRegistry; import org.hibernate.type.StandardBasicTypes; +@Slf4j public class ThingsboardPostgreSQLDialect extends PostgreSQLDialect { @Override public void initializeFunctionRegistry(FunctionContributions functionContributions) { + log.trace("initializeFunctionRegistry [{}]", functionContributions); super.initializeFunctionRegistry(functionContributions); BasicTypeRegistry basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry(); functionRegistry.registerPattern( "ilike", - "(?1 ILIKE ?2)", + "(?1::text ILIKE ?2::text)", basicTypeRegistry.resolve(StandardBasicTypes.BOOLEAN)); } } From baed75173cac89a4033ecba28d87c64679d37025 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 20:49:31 +0200 Subject: [PATCH 064/147] rollback jakarta.el.version --- dao/pom.xml | 4 ++-- pom.xml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dao/pom.xml b/dao/pom.xml index c34f47f2d6..cc34d77c85 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -125,8 +125,8 @@ hibernate-validator - jakarta.el - jakarta.el-api + org.glassfish + jakarta.el org.springframework diff --git a/pom.xml b/pom.xml index ab29084d79..0655a66c86 100755 --- a/pom.xml +++ b/pom.xml @@ -128,7 +128,7 @@ 10.1.3 8.0.1.Final 3.7.3 - 6.0.0 + 4.0.2 3.0.2 1.7.5 3.8.0 @@ -2038,8 +2038,8 @@ ${hypersistence-utils.version} - jakarta.el - jakarta.el-api + org.glassfish + jakarta.el ${jakarta.el.version} From b0b54b26ddf0ba61ff17c5e6be68ffdb8d22cbe1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 21:07:49 +0200 Subject: [PATCH 065/147] alarm repository UUID.fromString for non-null assigneeId --- .../java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index a8b74c2b1b..57de807d77 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -281,7 +281,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - UUID.fromString(assigneeId)); + assigneeId == null ? null : UUID.fromString(assigneeId)); } @Override From f471f1351e4684791b086e4e4e651eadcd24fe2d Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 21:10:44 +0200 Subject: [PATCH 066/147] alarm repository UUID.fromString for non-blank assigneeId --- .../java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 57de807d77..90a5b1f8d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -281,7 +281,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A asf.hasClearFilter() && asf.getClearFilter(), asf.hasAckFilter(), asf.hasAckFilter() && asf.getAckFilter(), - assigneeId == null ? null : UUID.fromString(assigneeId)); + StringUtils.isNotBlank(assigneeId) ? UUID.fromString(assigneeId) : null); } @Override From 8dc455edc1a7531adc046d5c01cbdbc512bdd39b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Apr 2024 22:00:14 +0200 Subject: [PATCH 067/147] JWT factory immutable and test fixed with > 512bit key --- .../security/model/token/JwtTokenFactory.java | 20 ++++++++++--------- .../security/auth/JwtTokenFactoryTest.java | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 7270398c25..f55f84e539 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.security.model.token; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtBuilder; @@ -183,20 +184,21 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - Claims claims = Jwts.claims().setSubject(principal.getValue()).build(); - claims.put(USER_ID, securityUser.getId().getId().toString()); - claims.put(SCOPES, scopes); + ClaimsBuilder claims = Jwts.claims() + .subject(principal.getValue()) + .add(USER_ID, securityUser.getId().getId().toString()) + .add(SCOPES, scopes); if (securityUser.getSessionId() != null) { - claims.put(SESSION_ID, securityUser.getSessionId()); + claims.add(SESSION_ID, securityUser.getSessionId()); } ZonedDateTime currentTime = ZonedDateTime.now(); return Jwts.builder() - .setClaims(claims) - .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) - .setIssuedAt(Date.from(currentTime.toInstant())) - .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) + .claims(claims.build()) + .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) + .issuedAt(Date.from(currentTime.toInstant())) + .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey()); } @@ -205,7 +207,7 @@ public class JwtTokenFactory { return Jwts.parser() .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) .build() - .parseClaimsJws(token); + .parseSignedClaims(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 2d39dd9905..44f7b53478 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -66,7 +66,7 @@ public class JwtTokenFactoryTest { public void beforeEach() { jwtSettings = new JwtSettings(); jwtSettings.setTokenIssuer("tb"); - jwtSettings.setTokenSigningKey("abewafaf"); + jwtSettings.setTokenSigningKey("abewafaf".repeat(11)); //48*11 bits key > 512 bits jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); From df08a94bee7e8b8ba2accc4add443220d4532270 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 09:41:41 +0200 Subject: [PATCH 068/147] Revert "JWT factory immutable and test fixed with > 512bit key" This reverts commit 8dc455edc1a7531adc046d5c01cbdbc512bdd39b. --- .../security/model/token/JwtTokenFactory.java | 20 +++++++++---------- .../security/auth/JwtTokenFactoryTest.java | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index f55f84e539..7270398c25 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.model.token; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtBuilder; @@ -184,21 +183,20 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - ClaimsBuilder claims = Jwts.claims() - .subject(principal.getValue()) - .add(USER_ID, securityUser.getId().getId().toString()) - .add(SCOPES, scopes); + Claims claims = Jwts.claims().setSubject(principal.getValue()).build(); + claims.put(USER_ID, securityUser.getId().getId().toString()); + claims.put(SCOPES, scopes); if (securityUser.getSessionId() != null) { - claims.add(SESSION_ID, securityUser.getSessionId()); + claims.put(SESSION_ID, securityUser.getSessionId()); } ZonedDateTime currentTime = ZonedDateTime.now(); return Jwts.builder() - .claims(claims.build()) - .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) - .issuedAt(Date.from(currentTime.toInstant())) - .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) + .setClaims(claims) + .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) + .setIssuedAt(Date.from(currentTime.toInstant())) + .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey()); } @@ -207,7 +205,7 @@ public class JwtTokenFactory { return Jwts.parser() .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) .build() - .parseSignedClaims(token); + .parseClaimsJws(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 44f7b53478..2d39dd9905 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -66,7 +66,7 @@ public class JwtTokenFactoryTest { public void beforeEach() { jwtSettings = new JwtSettings(); jwtSettings.setTokenIssuer("tb"); - jwtSettings.setTokenSigningKey("abewafaf".repeat(11)); //48*11 bits key > 512 bits + jwtSettings.setTokenSigningKey("abewafaf"); jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); From bdf9da8bab3d6c65b52f67904d001d9b81e1c9fc Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 09:42:06 +0200 Subject: [PATCH 069/147] Revert "Jwts.parser().build() refactored for a newer version" This reverts commit 1f5c95a375ab694ff08709cf9096dd773e0b90c7. --- .../server/service/security/model/token/JwtTokenFactory.java | 3 +-- .../service/security/model/token/OAuth2AppTokenFactory.java | 2 +- .../org/thingsboard/server/controller/AbstractWebTest.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 7270398c25..08622f578a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -183,7 +183,7 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - Claims claims = Jwts.claims().setSubject(principal.getValue()).build(); + Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(SCOPES, scopes); if (securityUser.getSessionId() != null) { @@ -204,7 +204,6 @@ public class JwtTokenFactory { try { return Jwts.parser() .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) - .build() .parseClaimsJws(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index dd30e4bc28..7f956f6970 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -40,7 +40,7 @@ public class OAuth2AppTokenFactory { public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { Jws jwsClaims; try { - jwsClaims = Jwts.parser().setSigningKey(appSecret).build().parseClaimsJws(appToken); + jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new IllegalArgumentException("Invalid Application token: ", ex); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 89d35f0756..e14b041e62 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -563,7 +563,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { int i = token.lastIndexOf('.'); Assert.assertTrue(i > 0); String withoutSignature = token.substring(0, i + 1); - Jwt jwsClaims = Jwts.parser().build().parseClaimsJwt(withoutSignature); + Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); Claims claims = jwsClaims.getBody(); String subject = claims.getSubject(); Assert.assertEquals(username, subject); From c5aef4c8be93434c94413f454742c3605a57252a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 10:04:33 +0200 Subject: [PATCH 070/147] rollback jjwt version to 0.9.1 as the upgrade to the latest requires an extra effort with a separate PR --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0655a66c86..36d0d9dc71 100755 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 6.2.4 6.2.4 5.1.2 - 0.12.5 + 0.9.1 2.0.13 2.23.1 1.5.5 From c07f432b7dc1b96c5714a7f9f8c0f0769c3e7c6e Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 19 Apr 2024 12:21:41 +0300 Subject: [PATCH 071/147] lwm2m: fix bug API getLwm2mObjects --- ui-ngx/src/app/core/http/device-profile.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 91ca903c7a..7a9dfd13ba 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -60,7 +60,7 @@ export class DeviceProfileService { public getLwm2mObjects(sortOrder: SortOrder, objectIds?: string[], searchText?: string, config?: RequestConfig): Observable> { - let url = `/api/resource/lwm2m/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; + let url = `/api/resource/lwm2m?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; if (isDefinedAndNotNull(objectIds) && objectIds.length > 0) { url += `&objectIds=${objectIds}`; } From 9a8e39856b8eea3ab65485c0cdfccde465b1fd29 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 18:17:33 +0200 Subject: [PATCH 072/147] CoapAttributesUpdatesIntegrationTest.java: unused spy wraparound removed --- .../updates/CoapAttributesUpdatesIntegrationTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java index 9ac2d5fc11..a1bf7932e9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java @@ -34,8 +34,6 @@ import static org.mockito.Mockito.spy; @DaoSqlTest public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributesIntegrationTest { - CoapTransportResource coapTransportResource; - @Autowired DefaultCoapServerService defaultCoapServerService; @@ -44,10 +42,6 @@ public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributes @Before public void beforeTest() throws Exception { - Resource api = defaultCoapServerService.getCoapServer().getRoot().getChild("api"); - coapTransportResource = spy( (CoapTransportResource) api.getChild("v1") ); - api.delete(api.getChild("v1") ); - api.add(coapTransportResource); CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() .deviceName("Test Subscribe to attribute updates") .build(); From 291cabef4bb78cb8d75a4ce8a5f895a658223c45 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 19 Apr 2024 19:36:27 +0300 Subject: [PATCH 073/147] UI: Implement Status widget. --- .../widget_bundles/status_indicators.json | 6 +- .../system/widget_types/status_widget.json | 26 ++ .../basic/basic-widget-config.module.ts | 12 +- .../status-widget-basic-config.component.html | 101 ++++++++ .../status-widget-basic-config.component.ts | 119 +++++++++ .../indicator/status-widget.component.html | 33 +++ .../indicator/status-widget.component.scss | 99 +++++++ .../lib/indicator/status-widget.component.ts | 241 ++++++++++++++++++ .../lib/indicator/status-widget.models.ts | 204 +++++++++++++++ ...tatus-widget-state-settings.component.html | 111 ++++++++ .../status-widget-state-settings.component.ts | 158 ++++++++++++ .../common/widget-settings-common.module.ts | 5 + .../widget/widget-components.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 19 ++ .../widget/status-widget/center-layout.svg | 20 ++ .../widget/status-widget/default-layout.svg | 20 ++ .../widget/status-widget/icon-layout.svg | 18 ++ 17 files changed, 1191 insertions(+), 8 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/status_widget.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts create mode 100644 ui-ngx/src/assets/widget/status-widget/center-layout.svg create mode 100644 ui-ngx/src/assets/widget/status-widget/default-layout.svg create mode 100644 ui-ngx/src/assets/widget/status-widget/icon-layout.svg diff --git a/application/src/main/data/json/system/widget_bundles/status_indicators.json b/application/src/main/data/json/system/widget_bundles/status_indicators.json index 3c8192493b..135fd69938 100644 --- a/application/src/main/data/json/system/widget_bundles/status_indicators.json +++ b/application/src/main/data/json/system/widget_bundles/status_indicators.json @@ -2,15 +2,15 @@ "widgetsBundle": { "alias": "status_indicators", "title": "Status indicators", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC", + "image": "tb-image:c3RhdHVzX2luZGljYXRvcnNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IlN0YXR1cyBpbmRpY2F0b3JzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC", "description": "Contains widgets displaying battery level and signal strength.", "order": 9000, - "externalId": null, "name": "Status indicators" }, "widgetTypeFqns": [ "battery_level", "signal_strength", - "progress_bar" + "progress_bar", + "status_widget" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/status_widget.json b/application/src/main/data/json/system/widget_types/status_widget.json new file mode 100644 index 0000000000..8431091ac5 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/status_widget.json @@ -0,0 +1,26 @@ +{ + "fqn": "status_widget", + "name": "Status widget", + "deprecated": false, + "image": "tb-image:c3RhdHVzLXdpZGdldF8oMykuc3Zn:IlN0YXR1cyB3aWRnZXQiIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiByeD0iNS40NTY2OSIgZmlsbD0iIzNGNTJERCIvPgo8cGF0aCBkPSJNNTIuNDE2NiAyMC43NUgxNy41ODMzVjE3LjU4MzNINTIuNDE2NlYyMC43NVpNMTkuMTY2NiA1MC44MzMzSDI1LjQ5OTlDMjUuNDk5OSA0Ni4wODMzIDIyLjMzMzMgNDIuOTE2NiAyMi4zMzMzIDQyLjkxNjZDMzEuODMzMyAzNi41ODMzIDMzLjQxNjYgMjIuMzMzMyAzMy40MTY2IDIyLjMzMzNIMTkuMTY2NlY1MC44MzMzWk01MC44MzMzIDIyLjMzMzNIMzYuNTgzM0MzNi41ODMzIDIyLjMzMzMgMzguMTY2NiAzNi41ODMzIDQ3LjY2NjYgNDIuOTE2NkM0Ny42NjY2IDQyLjkxNjYgNDQuNDk5OSA0Ni4wODMzIDQ0LjQ5OTkgNTAuODMzM0g1MC44MzMzVjIyLjMzMzNaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTkuNTA2OCAxMTIuNzY1TDIxLjQ4OTMgMTA1LjA0N0gyMi41MjgzTDIyLjQxMjEgMTA3LjAzNkwyMC4yOTMgMTE1SDE5LjIxMjlMMTkuNTA2OCAxMTIuNzY1Wk0xOC4wMjM0IDEwNS4wNDdMMTkuNjI5OSAxMTIuNzAzTDE5Ljc3MzQgMTE1SDE4LjYyNUwxNi4zMjEzIDEwNS4wNDdIMTguMDIzNFpNMjQuNzc3MyAxMTIuNjgzTDI2LjM2MzMgMTA1LjA0N0gyOC4wNzIzTDI1Ljc2ODYgMTE1SDI0LjYyMDFMMjQuNzc3MyAxMTIuNjgzWk0yMi45MzE2IDEwNS4wNDdMMjQuODkzNiAxMTIuNzg1TDI1LjE4MDcgMTE1SDI0LjEwMDZMMjIuMDE1NiAxMDcuMDM2TDIxLjkwNjIgMTA1LjA0N0gyMi45MzE2Wk0zMS4xOTM0IDEwNy42MDRWMTE1SDI5LjUzOTFWMTA3LjYwNEgzMS4xOTM0Wk0yOS40Mjk3IDEwNS42NjJDMjkuNDI5NyAxMDUuNDExIDI5LjUxMTcgMTA1LjIwNCAyOS42NzU4IDEwNS4wNEMyOS44NDQ0IDEwNC44NzEgMzAuMDc2OCAxMDQuNzg3IDMwLjM3MyAxMDQuNzg3QzMwLjY2NDcgMTA0Ljc4NyAzMC44OTQ5IDEwNC44NzEgMzEuMDYzNSAxMDUuMDRDMzEuMjMyMSAxMDUuMjA0IDMxLjMxNjQgMTA1LjQxMSAzMS4zMTY0IDEwNS42NjJDMzEuMzE2NCAxMDUuOTA4IDMxLjIzMjEgMTA2LjExMyAzMS4wNjM1IDEwNi4yNzdDMzAuODk0OSAxMDYuNDQxIDMwLjY2NDcgMTA2LjUyMyAzMC4zNzMgMTA2LjUyM0MzMC4wNzY4IDEwNi41MjMgMjkuODQ0NCAxMDYuNDQxIDI5LjY3NTggMTA2LjI3N0MyOS41MTE3IDEwNi4xMTMgMjkuNDI5NyAxMDUuOTA4IDI5LjQyOTcgMTA1LjY2MlpNMzQuODgxOCAxMDkuMTgzVjExNUgzMy4yMzQ0VjEwNy42MDRIMzQuNzg2MUwzNC44ODE4IDEwOS4xODNaTTM0LjU4NzkgMTExLjAyOEwzNC4wNTQ3IDExMS4wMjFDMzQuMDU5MiAxMTAuNDk3IDM0LjEzMjIgMTEwLjAxNyAzNC4yNzM0IDEwOS41NzlDMzQuNDE5MyAxMDkuMTQyIDM0LjYxOTggMTA4Ljc2NiAzNC44NzUgMTA4LjQ1MUMzNS4xMzQ4IDEwOC4xMzcgMzUuNDQ0NyAxMDcuODk1IDM1LjgwNDcgMTA3LjcyN0MzNi4xNjQ3IDEwNy41NTMgMzYuNTY1OCAxMDcuNDY3IDM3LjAwNzggMTA3LjQ2N0MzNy4zNjMzIDEwNy40NjcgMzcuNjg0NiAxMDcuNTE3IDM3Ljk3MTcgMTA3LjYxN0MzOC4yNjMzIDEwNy43MTMgMzguNTExNyAxMDcuODcgMzguNzE2OCAxMDguMDg5QzM4LjkyNjQgMTA4LjMwOCAzOS4wODU5IDEwOC41OTIgMzkuMTk1MyAxMDguOTQzQzM5LjMwNDcgMTA5LjI5IDM5LjM1OTQgMTA5LjcxNiAzOS4zNTk0IDExMC4yMjJWMTE1SDM3LjcwNTFWMTEwLjIxNUMzNy43MDUxIDEwOS44NTkgMzcuNjUyNyAxMDkuNTc5IDM3LjU0NzkgMTA5LjM3NEMzNy40NDc2IDEwOS4xNjQgMzcuMjk5NSAxMDkuMDE2IDM3LjEwMzUgMTA4LjkzQzM2LjkxMjEgMTA4LjgzOSAzNi42NzI5IDEwOC43OTMgMzYuMzg1NyAxMDguNzkzQzM2LjEwMzIgMTA4Ljc5MyAzNS44NTAzIDEwOC44NTIgMzUuNjI3IDEwOC45NzFDMzUuNDAzNiAxMDkuMDg5IDM1LjIxNDUgMTA5LjI1MSAzNS4wNTk2IDEwOS40NTZDMzQuOTA5MiAxMDkuNjYxIDM0Ljc5MyAxMDkuODk4IDM0LjcxMDkgMTEwLjE2N0MzNC42Mjg5IDExMC40MzYgMzQuNTg3OSAxMTAuNzIzIDM0LjU4NzkgMTExLjAyOFpNNDUuODAyNyAxMTMuNDY5VjEwNC41SDQ3LjQ1N1YxMTVINDUuOTZMNDUuODAyNyAxMTMuNDY5Wk00MC45OTAyIDExMS4zODRWMTExLjI0QzQwLjk5MDIgMTEwLjY4IDQxLjA1NjMgMTEwLjE2OSA0MS4xODg1IDEwOS43MDlDNDEuMzIwNiAxMDkuMjQ0IDQxLjUxMiAxMDguODQ1IDQxLjc2MjcgMTA4LjUxM0M0Mi4wMTMzIDEwOC4xNzUgNDIuMzE4NyAxMDcuOTE4IDQyLjY3ODcgMTA3Ljc0QzQzLjAzODcgMTA3LjU1OCA0My40NDQzIDEwNy40NjcgNDMuODk1NSAxMDcuNDY3QzQ0LjM0MjEgMTA3LjQ2NyA0NC43MzQgMTA3LjU1MyA0NS4wNzEzIDEwNy43MjdDNDUuNDA4NSAxMDcuOSA0NS42OTU2IDEwOC4xNDggNDUuOTMyNiAxMDguNDcyQzQ2LjE2OTYgMTA4Ljc5MSA0Ni4zNTg3IDEwOS4xNzQgNDYuNSAxMDkuNjJDNDYuNjQxMyAxMTAuMDYyIDQ2Ljc0MTUgMTEwLjU1NCA0Ni44MDA4IDExMS4wOTdWMTExLjU1NUM0Ni43NDE1IDExMi4wODMgNDYuNjQxMyAxMTIuNTY2IDQ2LjUgMTEzLjAwNEM0Ni4zNTg3IDExMy40NDEgNDYuMTY5NiAxMTMuODIgNDUuOTMyNiAxMTQuMTM5QzQ1LjY5NTYgMTE0LjQ1OCA0NS40MDYyIDExNC43MDQgNDUuMDY0NSAxMTQuODc3QzQ0LjcyNzIgMTE1LjA1IDQ0LjMzMyAxMTUuMTM3IDQzLjg4MTggMTE1LjEzN0M0My40MzUyIDExNS4xMzcgNDMuMDMxOSAxMTUuMDQzIDQyLjY3MTkgMTE0Ljg1NkM0Mi4zMTY0IDExNC42NyA0Mi4wMTMzIDExNC40MDggNDEuNzYyNyAxMTQuMDdDNDEuNTEyIDExMy43MzMgNDEuMzIwNiAxMTMuMzM3IDQxLjE4ODUgMTEyLjg4MUM0MS4wNTYzIDExMi40MjEgNDAuOTkwMiAxMTEuOTIyIDQwLjk5MDIgMTExLjM4NFpNNDIuNjM3NyAxMTEuMjRWMTExLjM4NEM0Mi42Mzc3IDExMS43MjEgNDIuNjY3MyAxMTIuMDM1IDQyLjcyNjYgMTEyLjMyN0M0Mi43OTA0IDExMi42MTkgNDIuODg4MyAxMTIuODc2IDQzLjAyMDUgMTEzLjFDNDMuMTUyNyAxMTMuMzE4IDQzLjMyMzYgMTEzLjQ5MiA0My41MzMyIDExMy42MTlDNDMuNzQ3NCAxMTMuNzQyIDQ0LjAwMjYgMTEzLjgwNCA0NC4yOTg4IDExMy44MDRDNDQuNjcyNSAxMTMuODA0IDQ0Ljk4MDEgMTEzLjcyMiA0NS4yMjE3IDExMy41NThDNDUuNDYzMiAxMTMuMzk0IDQ1LjY1MjMgMTEzLjE3MyA0NS43ODkxIDExMi44OTVDNDUuOTMwMyAxMTIuNjEyIDQ2LjAyNiAxMTIuMjk4IDQ2LjA3NjIgMTExLjk1MVYxMTAuNzE0QzQ2LjA0ODggMTEwLjQ0NSA0NS45OTE5IDExMC4xOTQgNDUuOTA1MyAxMDkuOTYyQzQ1LjgyMzIgMTA5LjcyOSA0NS43MTE2IDEwOS41MjcgNDUuNTcwMyAxMDkuMzU0QzQ1LjQyOSAxMDkuMTc2IDQ1LjI1MzYgMTA5LjAzOSA0NS4wNDM5IDEwOC45NDNDNDQuODM4OSAxMDguODQzIDQ0LjU5NTEgMTA4Ljc5MyA0NC4zMTI1IDEwOC43OTNDNDQuMDExNyAxMDguNzkzIDQzLjc1NjUgMTA4Ljg1NyA0My41NDY5IDEwOC45ODRDNDMuMzM3MiAxMDkuMTEyIDQzLjE2NDEgMTA5LjI4NyA0My4wMjczIDEwOS41MTFDNDIuODk1MiAxMDkuNzM0IDQyLjc5NzIgMTA5Ljk5NCA0Mi43MzM0IDExMC4yOUM0Mi42Njk2IDExMC41ODYgNDIuNjM3NyAxMTAuOTAzIDQyLjYzNzcgMTExLjI0Wk00OS4xMTUyIDExMS4zODRWMTExLjIyN0M0OS4xMTUyIDExMC42OTMgNDkuMTkyNyAxMTAuMTk5IDQ5LjM0NzcgMTA5Ljc0M0M0OS41MDI2IDEwOS4yODMgNDkuNzI1OSAxMDguODg0IDUwLjAxNzYgMTA4LjU0N0M1MC4zMTM4IDEwOC4yMDUgNTAuNjczOCAxMDcuOTQxIDUxLjA5NzcgMTA3Ljc1NEM1MS41MjYgMTA3LjU2MiA1Mi4wMDkxIDEwNy40NjcgNTIuNTQ2OSAxMDcuNDY3QzUzLjA4OTIgMTA3LjQ2NyA1My41NzIzIDEwNy41NjIgNTMuOTk2MSAxMDcuNzU0QzU0LjQyNDUgMTA3Ljk0MSA1NC43ODY4IDEwOC4yMDUgNTUuMDgzIDEwOC41NDdDNTUuMzc5MiAxMDguODg0IDU1LjYwNDggMTA5LjI4MyA1NS43NTk4IDEwOS43NDNDNTUuOTE0NyAxMTAuMTk5IDU1Ljk5MjIgMTEwLjY5MyA1NS45OTIyIDExMS4yMjdWMTExLjM4NEM1NS45OTIyIDExMS45MTcgNTUuOTE0NyAxMTIuNDExIDU1Ljc1OTggMTEyLjg2N0M1NS42MDQ4IDExMy4zMjMgNTUuMzc5MiAxMTMuNzIyIDU1LjA4MyAxMTQuMDYzQzU0Ljc4NjggMTE0LjQwMSA1NC40MjY4IDExNC42NjUgNTQuMDAyOSAxMTQuODU2QzUzLjU3OTEgMTE1LjA0MyA1My4wOTgzIDExNS4xMzcgNTIuNTYwNSAxMTUuMTM3QzUyLjAxODIgMTE1LjEzNyA1MS41MzI5IDExNS4wNDMgNTEuMTA0NSAxMTQuODU2QzUwLjY4MDcgMTE0LjY2NSA1MC4zMjA2IDExNC40MDEgNTAuMDI0NCAxMTQuMDYzQzQ5LjcyODIgMTEzLjcyMiA0OS41MDI2IDExMy4zMjMgNDkuMzQ3NyAxMTIuODY3QzQ5LjE5MjcgMTEyLjQxMSA0OS4xMTUyIDExMS45MTcgNDkuMTE1MiAxMTEuMzg0Wk01MC43NjI3IDExMS4yMjdWMTExLjM4NEM1MC43NjI3IDExMS43MTYgNTAuNzk2OSAxMTIuMDMxIDUwLjg2NTIgMTEyLjMyN0M1MC45MzM2IDExMi42MjMgNTEuMDQwNyAxMTIuODgzIDUxLjE4NjUgMTEzLjEwNkM1MS4zMzI0IDExMy4zMyA1MS41MTkyIDExMy41MDUgNTEuNzQ3MSAxMTMuNjMzQzUxLjk3NDkgMTEzLjc2IDUyLjI0NjEgMTEzLjgyNCA1Mi41NjA1IDExMy44MjRDNTIuODY1OSAxMTMuODI0IDUzLjEzMDIgMTEzLjc2IDUzLjM1MzUgMTEzLjYzM0M1My41ODE0IDExMy41MDUgNTMuNzY4MiAxMTMuMzMgNTMuOTE0MSAxMTMuMTA2QzU0LjA1OTkgMTEyLjg4MyA1NC4xNjcgMTEyLjYyMyA1NC4yMzU0IDExMi4zMjdDNTQuMzA4MyAxMTIuMDMxIDU0LjM0NDcgMTExLjcxNiA1NC4zNDQ3IDExMS4zODRWMTExLjIyN0M1NC4zNDQ3IDExMC44OTggNTQuMzA4MyAxMTAuNTg5IDU0LjIzNTQgMTEwLjI5N0M1NC4xNjcgMTEwLjAwMSA1NC4wNTc2IDEwOS43MzkgNTMuOTA3MiAxMDkuNTExQzUzLjc2MTQgMTA5LjI4MyA1My41NzQ1IDEwOS4xMDUgNTMuMzQ2NyAxMDguOTc4QzUzLjEyMzQgMTA4Ljg0NSA1Mi44NTY4IDEwOC43NzkgNTIuNTQ2OSAxMDguNzc5QzUyLjIzNyAxMDguNzc5IDUxLjk2ODEgMTA4Ljg0NSA1MS43NDAyIDEwOC45NzhDNTEuNTE2OSAxMDkuMTA1IDUxLjMzMjQgMTA5LjI4MyA1MS4xODY1IDEwOS41MTFDNTEuMDQwNyAxMDkuNzM5IDUwLjkzMzYgMTEwLjAwMSA1MC44NjUyIDExMC4yOTdDNTAuNzk2OSAxMTAuNTg5IDUwLjc2MjcgMTEwLjg5OCA1MC43NjI3IDExMS4yMjdaTTU5LjYzMjggMTEzLjM1OUw2MS4zNDE4IDEwNy42MDRINjIuMzk0NUw2Mi4xMDc0IDEwOS4zMjZMNjAuMzg0OCAxMTVINTkuNDQxNEw1OS42MzI4IDExMy4zNTlaTTU4LjYyNzkgMTA3LjYwNEw1OS45NjA5IDExMy4zODdMNjAuMDcwMyAxMTVINTkuMDE3Nkw1Ny4wMTQ2IDEwNy42MDRINTguNjI3OVpNNjMuOTk0MSAxMTMuMzE4TDY1LjI4NjEgMTA3LjYwNEg2Ni44OTI2TDY0Ljg5NjUgMTE1SDYzLjg0MzhMNjMuOTk0MSAxMTMuMzE4Wk02Mi41NzIzIDEwNy42MDRMNjQuMjYwNyAxMTMuMjkxTDY0LjQ3MjcgMTE1SDYzLjUyOTNMNjEuNzg2MSAxMDkuMzE5TDYxLjQ5OSAxMDcuNjA0SDYyLjU3MjNaTTczLjc2MzcgMTA0LjVWMTE1SDcyLjEwOTRWMTA0LjVINzMuNzYzN1pNNzkuMDc5MSAxMTUuMTM3Qzc4LjUzMjIgMTE1LjEzNyA3OC4wMzc4IDExNS4wNDggNzcuNTk1NyAxMTQuODdDNzcuMTU4MiAxMTQuNjg4IDc2Ljc4NDUgMTE0LjQzNSA3Ni40NzQ2IDExNC4xMTFDNzYuMTY5MyAxMTMuNzg4IDc1LjkzNDYgMTEzLjQwNyA3NS43NzA1IDExMi45N0M3NS42MDY0IDExMi41MzIgNzUuNTI0NCAxMTIuMDYxIDc1LjUyNDQgMTExLjU1NVYxMTEuMjgxQzc1LjUyNDQgMTEwLjcwMiA3NS42MDg3IDExMC4xNzggNzUuNzc3MyAxMDkuNzA5Qzc1Ljk0NiAxMDkuMjQgNzYuMTgwNyAxMDguODM5IDc2LjQ4MTQgMTA4LjUwNkM3Ni43ODIyIDEwOC4xNjkgNzcuMTM3NyAxMDcuOTExIDc3LjU0NzkgMTA3LjczM0M3Ny45NTggMTA3LjU1NiA3OC40MDIzIDEwNy40NjcgNzguODgwOSAxMDcuNDY3Qzc5LjQwOTUgMTA3LjQ2NyA3OS44NzIxIDEwNy41NTYgODAuMjY4NiAxMDcuNzMzQzgwLjY2NSAxMDcuOTExIDgwLjk5MzIgMTA4LjE2MiA4MS4yNTI5IDEwOC40ODVDODEuNTE3MyAxMDguODA0IDgxLjcxMzIgMTA5LjE4NSA4MS44NDA4IDEwOS42MjdDODEuOTczIDExMC4wNjkgODIuMDM5MSAxMTAuNTU3IDgyLjAzOTEgMTExLjA5VjExMS43OTRINzYuMzI0MlYxMTAuNjExSDgwLjQxMjFWMTEwLjQ4MUM4MC40MDMgMTEwLjE4NSA4MC4zNDM4IDEwOS45MDcgODAuMjM0NCAxMDkuNjQ3QzgwLjEyOTYgMTA5LjM4OCA3OS45Njc4IDEwOS4xNzggNzkuNzQ5IDEwOS4wMTlDNzkuNTMwMyAxMDguODU5IDc5LjIzODYgMTA4Ljc3OSA3OC44NzQgMTA4Ljc3OUM3OC42MDA2IDEwOC43NzkgNzguMzU2OCAxMDguODM5IDc4LjE0MjYgMTA4Ljk1N0M3Ny45MzI5IDEwOS4wNzEgNzcuNzU3NSAxMDkuMjM3IDc3LjYxNjIgMTA5LjQ1NkM3Ny40NzQ5IDEwOS42NzUgNzcuMzY1NiAxMDkuOTM5IDc3LjI4ODEgMTEwLjI0OUM3Ny4yMTUyIDExMC41NTQgNzcuMTc4NyAxMTAuODk4IDc3LjE3ODcgMTExLjI4MVYxMTEuNTU1Qzc3LjE3ODcgMTExLjg3OCA3Ny4yMjIgMTEyLjE3OSA3Ny4zMDg2IDExMi40NTdDNzcuMzk5NyAxMTIuNzMgNzcuNTMxOSAxMTIuOTcgNzcuNzA1MSAxMTMuMTc1Qzc3Ljg3ODMgMTEzLjM4IDc4LjA4NzkgMTEzLjU0MiA3OC4zMzQgMTEzLjY2Qzc4LjU4MDEgMTEzLjc3NCA3OC44NjA0IDExMy44MzEgNzkuMTc0OCAxMTMuODMxQzc5LjU3MTMgMTEzLjgzMSA3OS45MjQ1IDExMy43NTEgODAuMjM0NCAxMTMuNTkyQzgwLjU0NDMgMTEzLjQzMiA4MC44MTMyIDExMy4yMDcgODEuMDQxIDExMi45MTVMODEuOTA5MiAxMTMuNzU2QzgxLjc0OTcgMTEzLjk4OCA4MS41NDIzIDExNC4yMTIgODEuMjg3MSAxMTQuNDI2QzgxLjAzMTkgMTE0LjYzNSA4MC43MTk3IDExNC44MDYgODAuMzUwNiAxMTQuOTM4Qzc5Ljk4NiAxMTUuMDcxIDc5LjU2MjIgMTE1LjEzNyA3OS4wNzkxIDExNS4xMzdaTTg1LjgwMjcgMTE1SDg0LjE1NTNWMTA2Ljg5M0M4NC4xNTUzIDEwNi4zNDEgODQuMjU3OCAxMDUuODc5IDg0LjQ2MjkgMTA1LjUwNUM4NC42NzI1IDEwNS4xMjcgODQuOTcxIDEwNC44NDIgODUuMzU4NCAxMDQuNjVDODUuNzQ1OCAxMDQuNDU0IDg2LjIwMzggMTA0LjM1NiA4Ni43MzI0IDEwNC4zNTZDODYuODk2NSAxMDQuMzU2IDg3LjA1ODMgMTA0LjM2OCA4Ny4yMTc4IDEwNC4zOTFDODcuMzc3MyAxMDQuNDA5IDg3LjUzMjIgMTA0LjQzOCA4Ny42ODI2IDEwNC40NzlMODcuNjQxNiAxMDUuNzUxQzg3LjU1MDUgMTA1LjcyOCA4Ny40NTAyIDEwNS43MTIgODcuMzQwOCAxMDUuNzAzQzg3LjIzNiAxMDUuNjk0IDg3LjEyMjEgMTA1LjY4OSA4Ni45OTkgMTA1LjY4OUM4Ni43NDg0IDEwNS42ODkgODYuNTMxOSAxMDUuNzM3IDg2LjM0OTYgMTA1LjgzM0M4Ni4xNzE5IDEwNS45MjQgODYuMDM1MiAxMDYuMDU5IDg1LjkzOTUgMTA2LjIzNkM4NS44NDgzIDEwNi40MTQgODUuODAyNyAxMDYuNjMzIDg1LjgwMjcgMTA2Ljg5M1YxMTVaTTg3LjMyNzEgMTA3LjYwNFYxMDguODA3SDgzLjAyMDVWMTA3LjYwNEg4Ny4zMjcxWk05Mi4xNjQxIDEwNy42MDRWMTA4LjgwN0g4Ny45OTQxVjEwNy42MDRIOTIuMTY0MVpNODkuMTk3MyAxMDUuNzkySDkwLjg0NDdWMTEyLjk1NkM5MC44NDQ3IDExMy4xODQgOTAuODc2NiAxMTMuMzU5IDkwLjk0MDQgMTEzLjQ4MkM5MS4wMDg4IDExMy42MDEgOTEuMTAyMiAxMTMuNjgxIDkxLjIyMDcgMTEzLjcyMkM5MS4zMzkyIDExMy43NjMgOTEuNDc4MiAxMTMuNzgzIDkxLjYzNzcgMTEzLjc4M0M5MS43NTE2IDExMy43ODMgOTEuODYxIDExMy43NzYgOTEuOTY1OCAxMTMuNzYzQzkyLjA3MDYgMTEzLjc0OSA5Mi4xNTQ5IDExMy43MzUgOTIuMjE4OCAxMTMuNzIyTDkyLjIyNTYgMTE0Ljk3OUM5Mi4wODg5IDExNS4wMjEgOTEuOTI5NCAxMTUuMDU3IDkxLjc0NzEgMTE1LjA4OUM5MS41NjkzIDExNS4xMjEgOTEuMzY0MyAxMTUuMTM3IDkxLjEzMTggMTE1LjEzN0M5MC43NTM2IDExNS4xMzcgOTAuNDE4NiAxMTUuMDcxIDkwLjEyNyAxMTQuOTM4Qzg5LjgzNTMgMTE0LjgwMiA4OS42MDc0IDExNC41ODEgODkuNDQzNCAxMTQuMjc1Qzg5LjI3OTMgMTEzLjk3IDg5LjE5NzMgMTEzLjU2NCA4OS4xOTczIDExMy4wNTlWMTA1Ljc5MlpNMTAwLjQ0MyAxMTMuODI0QzEwMC43MTIgMTEzLjgyNCAxMDAuOTU0IDExMy43NzIgMTAxLjE2OCAxMTMuNjY3QzEwMS4zODcgMTEzLjU1OCAxMDEuNTYyIDExMy40MDcgMTAxLjY5NCAxMTMuMjE2QzEwMS44MzEgMTEzLjAyNCAxMDEuOTA2IDExMi44MDMgMTAxLjkyIDExMi41NTNIMTAzLjQ3MkMxMDMuNDYzIDExMy4wMzEgMTAzLjMyMSAxMTMuNDY2IDEwMy4wNDggMTEzLjg1OEMxMDIuNzc0IDExNC4yNSAxMDIuNDEyIDExNC41NjIgMTAxLjk2MSAxMTQuNzk1QzEwMS41MSAxMTUuMDIzIDEwMS4wMTEgMTE1LjEzNyAxMDAuNDY0IDExNS4xMzdDOTkuODk4OCAxMTUuMTM3IDk5LjQwNjYgMTE1LjA0MSA5OC45ODczIDExNC44NUM5OC41NjggMTE0LjY1NCA5OC4yMTk0IDExNC4zODUgOTcuOTQxNCAxMTQuMDQzQzk3LjY2MzQgMTEzLjcwMSA5Ny40NTM4IDExMy4zMDcgOTcuMzEyNSAxMTIuODZDOTcuMTc1OCAxMTIuNDE0IDk3LjEwNzQgMTExLjkzNSA5Ny4xMDc0IDExMS40MjVWMTExLjE4NkM5Ny4xMDc0IDExMC42NzUgOTcuMTc1OCAxMTAuMTk3IDk3LjMxMjUgMTA5Ljc1Qzk3LjQ1MzggMTA5LjI5OSA5Ny42NjM0IDEwOC45MDIgOTcuOTQxNCAxMDguNTYxQzk4LjIxOTQgMTA4LjIxOSA5OC41NjggMTA3Ljk1MiA5OC45ODczIDEwNy43NjFDOTkuNDA2NiAxMDcuNTY1IDk5Ljg5NjUgMTA3LjQ2NyAxMDAuNDU3IDEwNy40NjdDMTAxLjA0OSAxMDcuNDY3IDEwMS41NjkgMTA3LjU4NSAxMDIuMDE2IDEwNy44MjJDMTAyLjQ2MiAxMDguMDU1IDEwMi44MTMgMTA4LjM4MSAxMDMuMDY4IDEwOC44QzEwMy4zMjggMTA5LjIxNSAxMDMuNDYzIDEwOS42OTggMTAzLjQ3MiAxMTAuMjQ5SDEwMS45MkMxMDEuOTA2IDEwOS45NzYgMTAxLjgzOCAxMDkuNzI5IDEwMS43MTUgMTA5LjUxMUMxMDEuNTk2IDEwOS4yODcgMTAxLjQyOCAxMDkuMTEgMTAxLjIwOSAxMDguOTc4QzEwMC45OTUgMTA4Ljg0NSAxMDAuNzM3IDEwOC43NzkgMTAwLjQzNyAxMDguNzc5QzEwMC4xMDQgMTA4Ljc3OSA5OS44MjgxIDEwOC44NDggOTkuNjA5NCAxMDguOTg0Qzk5LjM5MDYgMTA5LjExNyA5OS4yMTk3IDEwOS4yOTkgOTkuMDk2NyAxMDkuNTMxQzk4Ljk3MzYgMTA5Ljc1OSA5OC44ODQ4IDExMC4wMTcgOTguODMwMSAxMTAuMzA0Qzk4Ljc3OTkgMTEwLjU4NiA5OC43NTQ5IDExMC44OCA5OC43NTQ5IDExMS4xODZWMTExLjQyNUM5OC43NTQ5IDExMS43MyA5OC43Nzk5IDExMi4wMjYgOTguODMwMSAxMTIuMzEzQzk4Ljg4MDIgMTEyLjYwMSA5OC45NjY4IDExMi44NTggOTkuMDg5OCAxMTMuMDg2Qzk5LjIxNzQgMTEzLjMwOSA5OS4zOTA2IDExMy40ODkgOTkuNjA5NCAxMTMuNjI2Qzk5LjgyODEgMTEzLjc1OCAxMDAuMTA2IDExMy44MjQgMTAwLjQ0MyAxMTMuODI0Wk0xMDQuNjcyIDExMS4zODRWMTExLjIyN0MxMDQuNjcyIDExMC42OTMgMTA0Ljc0OSAxMTAuMTk5IDEwNC45MDQgMTA5Ljc0M0MxMDUuMDU5IDEwOS4yODMgMTA1LjI4MyAxMDguODg0IDEwNS41NzQgMTA4LjU0N0MxMDUuODcgMTA4LjIwNSAxMDYuMjMgMTA3Ljk0MSAxMDYuNjU0IDEwNy43NTRDMTA3LjA4MyAxMDcuNTYyIDEwNy41NjYgMTA3LjQ2NyAxMDguMTA0IDEwNy40NjdDMTA4LjY0NiAxMDcuNDY3IDEwOS4xMjkgMTA3LjU2MiAxMDkuNTUzIDEwNy43NTRDMTA5Ljk4MSAxMDcuOTQxIDExMC4zNDMgMTA4LjIwNSAxMTAuNjQgMTA4LjU0N0MxMTAuOTM2IDEwOC44ODQgMTExLjE2MSAxMDkuMjgzIDExMS4zMTYgMTA5Ljc0M0MxMTEuNDcxIDExMC4xOTkgMTExLjU0OSAxMTAuNjkzIDExMS41NDkgMTExLjIyN1YxMTEuMzg0QzExMS41NDkgMTExLjkxNyAxMTEuNDcxIDExMi40MTEgMTExLjMxNiAxMTIuODY3QzExMS4xNjEgMTEzLjMyMyAxMTAuOTM2IDExMy43MjIgMTEwLjY0IDExNC4wNjNDMTEwLjM0MyAxMTQuNDAxIDEwOS45ODMgMTE0LjY2NSAxMDkuNTYgMTE0Ljg1NkMxMDkuMTM2IDExNS4wNDMgMTA4LjY1NSAxMTUuMTM3IDEwOC4xMTcgMTE1LjEzN0MxMDcuNTc1IDExNS4xMzcgMTA3LjA5IDExNS4wNDMgMTA2LjY2MSAxMTQuODU2QzEwNi4yMzcgMTE0LjY2NSAxMDUuODc3IDExNC40MDEgMTA1LjU4MSAxMTQuMDYzQzEwNS4yODUgMTEzLjcyMiAxMDUuMDU5IDExMy4zMjMgMTA0LjkwNCAxMTIuODY3QzEwNC43NDkgMTEyLjQxMSAxMDQuNjcyIDExMS45MTcgMTA0LjY3MiAxMTEuMzg0Wk0xMDYuMzE5IDExMS4yMjdWMTExLjM4NEMxMDYuMzE5IDExMS43MTYgMTA2LjM1NCAxMTIuMDMxIDEwNi40MjIgMTEyLjMyN0MxMDYuNDkgMTEyLjYyMyAxMDYuNTk3IDExMi44ODMgMTA2Ljc0MyAxMTMuMTA2QzEwNi44ODkgMTEzLjMzIDEwNy4wNzYgMTEzLjUwNSAxMDcuMzA0IDExMy42MzNDMTA3LjUzMiAxMTMuNzYgMTA3LjgwMyAxMTMuODI0IDEwOC4xMTcgMTEzLjgyNEMxMDguNDIzIDExMy44MjQgMTA4LjY4NyAxMTMuNzYgMTA4LjkxIDExMy42MzNDMTA5LjEzOCAxMTMuNTA1IDEwOS4zMjUgMTEzLjMzIDEwOS40NzEgMTEzLjEwNkMxMDkuNjE3IDExMi44ODMgMTA5LjcyNCAxMTIuNjIzIDEwOS43OTIgMTEyLjMyN0MxMDkuODY1IDExMi4wMzEgMTA5LjkwMSAxMTEuNzE2IDEwOS45MDEgMTExLjM4NFYxMTEuMjI3QzEwOS45MDEgMTEwLjg5OCAxMDkuODY1IDExMC41ODkgMTA5Ljc5MiAxMTAuMjk3QzEwOS43MjQgMTEwLjAwMSAxMDkuNjE0IDEwOS43MzkgMTA5LjQ2NCAxMDkuNTExQzEwOS4zMTggMTA5LjI4MyAxMDkuMTMxIDEwOS4xMDUgMTA4LjkwMyAxMDguOTc4QzEwOC42OCAxMDguODQ1IDEwOC40MTMgMTA4Ljc3OSAxMDguMTA0IDEwOC43NzlDMTA3Ljc5NCAxMDguNzc5IDEwNy41MjUgMTA4Ljg0NSAxMDcuMjk3IDEwOC45NzhDMTA3LjA3NCAxMDkuMTA1IDEwNi44ODkgMTA5LjI4MyAxMDYuNzQzIDEwOS41MTFDMTA2LjU5NyAxMDkuNzM5IDEwNi40OSAxMTAuMDAxIDEwNi40MjIgMTEwLjI5N0MxMDYuMzU0IDExMC41ODkgMTA2LjMxOSAxMTAuODk4IDEwNi4zMTkgMTExLjIyN1pNMTE0LjgzNCAxMDkuMDEyVjExNUgxMTMuMTg3VjEwNy42MDRIMTE0Ljc1OUwxMTQuODM0IDEwOS4wMTJaTTExNy4wOTcgMTA3LjU1NkwxMTcuMDgzIDEwOS4wODdDMTE2Ljk4MyAxMDkuMDY5IDExNi44NzMgMTA5LjA1NSAxMTYuNzU1IDEwOS4wNDZDMTE2LjY0MSAxMDkuMDM3IDExNi41MjcgMTA5LjAzMiAxMTYuNDEzIDEwOS4wMzJDMTE2LjEzMSAxMDkuMDMyIDExNS44ODIgMTA5LjA3MyAxMTUuNjY4IDEwOS4xNTVDMTE1LjQ1NCAxMDkuMjMzIDExNS4yNzQgMTA5LjM0NyAxMTUuMTI4IDEwOS40OTdDMTE0Ljk4NyAxMDkuNjQzIDExNC44NzcgMTA5LjgyMSAxMTQuOCAxMTAuMDNDMTE0LjcyMiAxMTAuMjQgMTE0LjY3NyAxMTAuNDc1IDExNC42NjMgMTEwLjczNEwxMTQuMjg3IDExMC43NjJDMTE0LjI4NyAxMTAuMjk3IDExNC4zMzMgMTA5Ljg2NiAxMTQuNDI0IDEwOS40N0MxMTQuNTE1IDEwOS4wNzMgMTE0LjY1MiAxMDguNzI1IDExNC44MzQgMTA4LjQyNEMxMTUuMDIxIDEwOC4xMjMgMTE1LjI1MyAxMDcuODg4IDExNS41MzEgMTA3LjcyQzExNS44MTQgMTA3LjU1MSAxMTYuMTQgMTA3LjQ2NyAxMTYuNTA5IDEwNy40NjdDMTE2LjYwOSAxMDcuNDY3IDExNi43MTYgMTA3LjQ3NiAxMTYuODMgMTA3LjQ5NEMxMTYuOTQ5IDEwNy41MTIgMTE3LjAzNyAxMDcuNTMzIDExNy4wOTcgMTA3LjU1NlpNMTIwLjAxMyAxMDkuMTgzVjExNUgxMTguMzY1VjEwNy42MDRIMTE5LjkxN0wxMjAuMDEzIDEwOS4xODNaTTExOS43MTkgMTExLjAyOEwxMTkuMTg2IDExMS4wMjFDMTE5LjE5IDExMC40OTcgMTE5LjI2MyAxMTAuMDE3IDExOS40MDQgMTA5LjU3OUMxMTkuNTUgMTA5LjE0MiAxMTkuNzUxIDEwOC43NjYgMTIwLjAwNiAxMDguNDUxQzEyMC4yNjYgMTA4LjEzNyAxMjAuNTc2IDEwNy44OTUgMTIwLjkzNiAxMDcuNzI3QzEyMS4yOTYgMTA3LjU1MyAxMjEuNjk3IDEwNy40NjcgMTIyLjEzOSAxMDcuNDY3QzEyMi40OTQgMTA3LjQ2NyAxMjIuODE1IDEwNy41MTcgMTIzLjEwMyAxMDcuNjE3QzEyMy4zOTQgMTA3LjcxMyAxMjMuNjQzIDEwNy44NyAxMjMuODQ4IDEwOC4wODlDMTI0LjA1NyAxMDguMzA4IDEyNC4yMTcgMTA4LjU5MiAxMjQuMzI2IDEwOC45NDNDMTI0LjQzNiAxMDkuMjkgMTI0LjQ5IDEwOS43MTYgMTI0LjQ5IDExMC4yMjJWMTE1SDEyMi44MzZWMTEwLjIxNUMxMjIuODM2IDEwOS44NTkgMTIyLjc4NCAxMDkuNTc5IDEyMi42NzkgMTA5LjM3NEMxMjIuNTc4IDEwOS4xNjQgMTIyLjQzIDEwOS4wMTYgMTIyLjIzNCAxMDguOTNDMTIyLjA0MyAxMDguODM5IDEyMS44MDQgMTA4Ljc5MyAxMjEuNTE3IDEwOC43OTNDMTIxLjIzNCAxMDguNzkzIDEyMC45ODEgMTA4Ljg1MiAxMjAuNzU4IDEwOC45NzFDMTIwLjUzNSAxMDkuMDg5IDEyMC4zNDUgMTA5LjI1MSAxMjAuMTkgMTA5LjQ1NkMxMjAuMDQgMTA5LjY2MSAxMTkuOTI0IDEwOS44OTggMTE5Ljg0MiAxMTAuMTY3QzExOS43NiAxMTAuNDM2IDExOS43MTkgMTEwLjcyMyAxMTkuNzE5IDExMS4wMjhaTTEyOS42ODMgMTE1LjEzN0MxMjkuMTM2IDExNS4xMzcgMTI4LjY0MSAxMTUuMDQ4IDEyOC4xOTkgMTE0Ljg3QzEyNy43NjIgMTE0LjY4OCAxMjcuMzg4IDExNC40MzUgMTI3LjA3OCAxMTQuMTExQzEyNi43NzMgMTEzLjc4OCAxMjYuNTM4IDExMy40MDcgMTI2LjM3NCAxMTIuOTdDMTI2LjIxIDExMi41MzIgMTI2LjEyOCAxMTIuMDYxIDEyNi4xMjggMTExLjU1NVYxMTEuMjgxQzEyNi4xMjggMTEwLjcwMiAxMjYuMjEyIDExMC4xNzggMTI2LjM4MSAxMDkuNzA5QzEyNi41NDkgMTA5LjI0IDEyNi43ODQgMTA4LjgzOSAxMjcuMDg1IDEwOC41MDZDMTI3LjM4NiAxMDguMTY5IDEyNy43NDEgMTA3LjkxMSAxMjguMTUxIDEwNy43MzNDMTI4LjU2MiAxMDcuNTU2IDEyOS4wMDYgMTA3LjQ2NyAxMjkuNDg0IDEwNy40NjdDMTMwLjAxMyAxMDcuNDY3IDEzMC40NzYgMTA3LjU1NiAxMzAuODcyIDEwNy43MzNDMTMxLjI2OSAxMDcuOTExIDEzMS41OTcgMTA4LjE2MiAxMzEuODU2IDEwOC40ODVDMTMyLjEyMSAxMDguODA0IDEzMi4zMTcgMTA5LjE4NSAxMzIuNDQ0IDEwOS42MjdDMTMyLjU3NiAxMTAuMDY5IDEzMi42NDMgMTEwLjU1NyAxMzIuNjQzIDExMS4wOVYxMTEuNzk0SDEyNi45MjhWMTEwLjYxMUgxMzEuMDE2VjExMC40ODFDMTMxLjAwNyAxMTAuMTg1IDEzMC45NDcgMTA5LjkwNyAxMzAuODM4IDEwOS42NDdDMTMwLjczMyAxMDkuMzg4IDEzMC41NzEgMTA5LjE3OCAxMzAuMzUzIDEwOS4wMTlDMTMwLjEzNCAxMDguODU5IDEyOS44NDIgMTA4Ljc3OSAxMjkuNDc4IDEwOC43NzlDMTI5LjIwNCAxMDguNzc5IDEyOC45NiAxMDguODM5IDEyOC43NDYgMTA4Ljk1N0MxMjguNTM2IDEwOS4wNzEgMTI4LjM2MSAxMDkuMjM3IDEyOC4yMiAxMDkuNDU2QzEyOC4wNzggMTA5LjY3NSAxMjcuOTY5IDEwOS45MzkgMTI3Ljg5MiAxMTAuMjQ5QzEyNy44MTkgMTEwLjU1NCAxMjcuNzgyIDExMC44OTggMTI3Ljc4MiAxMTEuMjgxVjExMS41NTVDMTI3Ljc4MiAxMTEuODc4IDEyNy44MjYgMTEyLjE3OSAxMjcuOTEyIDExMi40NTdDMTI4LjAwMyAxMTIuNzMgMTI4LjEzNSAxMTIuOTcgMTI4LjMwOSAxMTMuMTc1QzEyOC40ODIgMTEzLjM4IDEyOC42OTEgMTEzLjU0MiAxMjguOTM4IDExMy42NkMxMjkuMTg0IDExMy43NzQgMTI5LjQ2NCAxMTMuODMxIDEyOS43NzggMTEzLjgzMUMxMzAuMTc1IDExMy44MzEgMTMwLjUyOCAxMTMuNzUxIDEzMC44MzggMTEzLjU5MkMxMzEuMTQ4IDExMy40MzIgMTMxLjQxNyAxMTMuMjA3IDEzMS42NDUgMTEyLjkxNUwxMzIuNTEzIDExMy43NTZDMTMyLjM1MyAxMTMuOTg4IDEzMi4xNDYgMTE0LjIxMiAxMzEuODkxIDExNC40MjZDMTMxLjYzNSAxMTQuNjM1IDEzMS4zMjMgMTE0LjgwNiAxMzAuOTU0IDExNC45MzhDMTMwLjU5IDExNS4wNzEgMTMwLjE2NiAxMTUuMTM3IDEyOS42ODMgMTE1LjEzN1pNMTM1LjgzMiAxMDkuMDEyVjExNUgxMzQuMTg1VjEwNy42MDRIMTM1Ljc1N0wxMzUuODMyIDEwOS4wMTJaTTEzOC4wOTUgMTA3LjU1NkwxMzguMDgxIDEwOS4wODdDMTM3Ljk4MSAxMDkuMDY5IDEzNy44NzEgMTA5LjA1NSAxMzcuNzUzIDEwOS4wNDZDMTM3LjYzOSAxMDkuMDM3IDEzNy41MjUgMTA5LjAzMiAxMzcuNDExIDEwOS4wMzJDMTM3LjEyOSAxMDkuMDMyIDEzNi44OCAxMDkuMDczIDEzNi42NjYgMTA5LjE1NUMxMzYuNDUyIDEwOS4yMzMgMTM2LjI3MiAxMDkuMzQ3IDEzNi4xMjYgMTA5LjQ5N0MxMzUuOTg1IDEwOS42NDMgMTM1Ljg3NSAxMDkuODIxIDEzNS43OTggMTEwLjAzQzEzNS43MiAxMTAuMjQgMTM1LjY3NSAxMTAuNDc1IDEzNS42NjEgMTEwLjczNEwxMzUuMjg1IDExMC43NjJDMTM1LjI4NSAxMTAuMjk3IDEzNS4zMzEgMTA5Ljg2NiAxMzUuNDIyIDEwOS40N0MxMzUuNTEzIDEwOS4wNzMgMTM1LjY1IDEwOC43MjUgMTM1LjgzMiAxMDguNDI0QzEzNi4wMTkgMTA4LjEyMyAxMzYuMjUxIDEwNy44ODggMTM2LjUyOSAxMDcuNzJDMTM2LjgxMiAxMDcuNTUxIDEzNy4xMzggMTA3LjQ2NyAxMzcuNTA3IDEwNy40NjdDMTM3LjYwNyAxMDcuNDY3IDEzNy43MTQgMTA3LjQ3NiAxMzcuODI4IDEwNy40OTRDMTM3Ljk0NyAxMDcuNTEyIDEzOC4wMzUgMTA3LjUzMyAxMzguMDk1IDEwNy41NTZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjMuNjY5OSAxMzEuNVYxMzEuOTY5QzIzLjY2OTkgMTMyLjYxMyAyMy41ODU5IDEzMy4xOTEgMjMuNDE4IDEzMy43MDNDMjMuMjUgMTM0LjIxNSAyMy4wMDk4IDEzNC42NSAyMi42OTczIDEzNS4wMUMyMi4zODg3IDEzNS4zNjkgMjIuMDE3NiAxMzUuNjQ1IDIxLjU4NCAxMzUuODM2QzIxLjE1MDQgMTM2LjAyMyAyMC42Njk5IDEzNi4xMTcgMjAuMTQyNiAxMzYuMTE3QzE5LjYxOTEgMTM2LjExNyAxOS4xNDA2IDEzNi4wMjMgMTguNzA3IDEzNS44MzZDMTguMjc3MyAxMzUuNjQ1IDE3LjkwNDMgMTM1LjM2OSAxNy41ODc5IDEzNS4wMUMxNy4yNzE1IDEzNC42NSAxNy4wMjU0IDEzNC4yMTUgMTYuODQ5NiAxMzMuNzAzQzE2LjY3NzcgMTMzLjE5MSAxNi41OTE4IDEzMi42MTMgMTYuNTkxOCAxMzEuOTY5VjEzMS41QzE2LjU5MTggMTMwLjg1NSAxNi42Nzc3IDEzMC4yNzkgMTYuODQ5NiAxMjkuNzcxQzE3LjAyMTUgMTI5LjI2IDE3LjI2MzcgMTI4LjgyNCAxNy41NzYyIDEyOC40NjVDMTcuODkyNiAxMjguMTAyIDE4LjI2NTYgMTI3LjgyNiAxOC42OTUzIDEyNy42MzlDMTkuMTI4OSAxMjcuNDQ3IDE5LjYwNzQgMTI3LjM1MiAyMC4xMzA5IDEyNy4zNTJDMjAuNjU4MiAxMjcuMzUyIDIxLjEzODcgMTI3LjQ0NyAyMS41NzIzIDEyNy42MzlDMjIuMDA1OSAxMjcuODI2IDIyLjM3ODkgMTI4LjEwMiAyMi42OTE0IDEyOC40NjVDMjMuMDAzOSAxMjguODI0IDIzLjI0NDEgMTI5LjI2IDIzLjQxMjEgMTI5Ljc3MUMyMy41ODQgMTMwLjI3OSAyMy42Njk5IDEzMC44NTUgMjMuNjY5OSAxMzEuNVpNMjIuMTk5MiAxMzEuOTY5VjEzMS40ODhDMjIuMTk5MiAxMzEuMDEyIDIyLjE1MjMgMTMwLjU5MiAyMi4wNTg2IDEzMC4yMjlDMjEuOTY4OCAxMjkuODYxIDIxLjgzNCAxMjkuNTU1IDIxLjY1NDMgMTI5LjMwOUMyMS40Nzg1IDEyOS4wNTkgMjEuMjYxNyAxMjguODcxIDIxLjAwMzkgMTI4Ljc0NkMyMC43NDYxIDEyOC42MTcgMjAuNDU1MSAxMjguNTUzIDIwLjEzMDkgMTI4LjU1M0MxOS44MDY2IDEyOC41NTMgMTkuNTE3NiAxMjguNjE3IDE5LjI2MzcgMTI4Ljc0NkMxOS4wMDk4IDEyOC44NzEgMTguNzkzIDEyOS4wNTkgMTguNjEzMyAxMjkuMzA5QzE4LjQzNzUgMTI5LjU1NSAxOC4zMDI3IDEyOS44NjEgMTguMjA5IDEzMC4yMjlDMTguMTE1MiAxMzAuNTkyIDE4LjA2ODQgMTMxLjAxMiAxOC4wNjg0IDEzMS40ODhWMTMxLjk2OUMxOC4wNjg0IDEzMi40NDUgMTguMTE1MiAxMzIuODY3IDE4LjIwOSAxMzMuMjM0QzE4LjMwMjcgMTMzLjYwMiAxOC40Mzk1IDEzMy45MTIgMTguNjE5MSAxMzQuMTY2QzE4LjgwMjcgMTM0LjQxNiAxOS4wMjE1IDEzNC42MDUgMTkuMjc1NCAxMzQuNzM0QzE5LjUyOTMgMTM0Ljg1OSAxOS44MTg0IDEzNC45MjIgMjAuMTQyNiAxMzQuOTIyQzIwLjQ3MDcgMTM0LjkyMiAyMC43NjE3IDEzNC44NTkgMjEuMDE1NiAxMzQuNzM0QzIxLjI2OTUgMTM0LjYwNSAyMS40ODQ0IDEzNC40MTYgMjEuNjYwMiAxMzQuMTY2QzIxLjgzNTkgMTMzLjkxMiAyMS45Njg4IDEzMy42MDIgMjIuMDU4NiAxMzMuMjM0QzIyLjE1MjMgMTMyLjg2NyAyMi4xOTkyIDEzMi40NDUgMjIuMTk5MiAxMzEuOTY5Wk0yOC44ODA5IDEzMi44MThIMjYuNjYwMlYxMzEuNjUySDI4Ljg4MDlDMjkuMjY3NiAxMzEuNjUyIDI5LjU4MDEgMTMxLjU5IDI5LjgxODQgMTMxLjQ2NUMzMC4wNTY2IDEzMS4zNCAzMC4yMzA1IDEzMS4xNjggMzAuMzM5OCAxMzAuOTQ5QzMwLjQ1MzEgMTMwLjcyNyAzMC41MDk4IDEzMC40NzMgMzAuNTA5OCAxMzAuMTg4QzMwLjUwOTggMTI5LjkxOCAzMC40NTMxIDEyOS42NjYgMzAuMzM5OCAxMjkuNDMyQzMwLjIzMDUgMTI5LjE5MyAzMC4wNTY2IDEyOS4wMDIgMjkuODE4NCAxMjguODU3QzI5LjU4MDEgMTI4LjcxMyAyOS4yNjc2IDEyOC42NDEgMjguODgwOSAxMjguNjQxSDI3LjExMTNWMTM2SDI1LjY0MDZWMTI3LjQ2OUgyOC44ODA5QzI5LjU0MSAxMjcuNDY5IDMwLjEwMTYgMTI3LjU4NiAzMC41NjI1IDEyNy44MkMzMS4wMjczIDEyOC4wNTEgMzEuMzgwOSAxMjguMzcxIDMxLjYyMyAxMjguNzgxQzMxLjg2NTIgMTI5LjE4OCAzMS45ODYzIDEyOS42NTIgMzEuOTg2MyAxMzAuMTc2QzMxLjk4NjMgMTMwLjcyNyAzMS44NjUyIDEzMS4xOTkgMzEuNjIzIDEzMS41OTRDMzEuMzgwOSAxMzEuOTg4IDMxLjAyNzMgMTMyLjI5MSAzMC41NjI1IDEzMi41MDJDMzAuMTAxNiAxMzIuNzEzIDI5LjU0MSAxMzIuODE4IDI4Ljg4MDkgMTMyLjgxOFpNMzkuMzg4NyAxMzQuODM0VjEzNkgzNC44NTk0VjEzNC44MzRIMzkuMzg4N1pNMzUuMjc1NCAxMjcuNDY5VjEzNkgzMy44MDQ3VjEyNy40NjlIMzUuMjc1NFpNMzguNzk2OSAxMzEuMDMxVjEzMi4xOEgzNC44NTk0VjEzMS4wMzFIMzguNzk2OVpNMzkuMzU5NCAxMjcuNDY5VjEyOC42NDFIMzQuODU5NFYxMjcuNDY5SDM5LjM1OTRaTTQ3Ljg1NzQgMTI3LjQ2OVYxMzZINDYuMzg2N0w0Mi41NjA1IDEyOS44ODlWMTM2SDQxLjA4OThWMTI3LjQ2OUg0Mi41NjA1TDQ2LjM5ODQgMTMzLjU5MlYxMjcuNDY5SDQ3Ljg1NzRaTTU1LjY5MzQgMTM0LjgzNFYxMzZINTEuMTY0MVYxMzQuODM0SDU1LjY5MzRaTTUxLjU4MDEgMTI3LjQ2OVYxMzZINTAuMTA5NFYxMjcuNDY5SDUxLjU4MDFaTTU1LjEwMTYgMTMxLjAzMVYxMzIuMThINTEuMTY0MVYxMzEuMDMxSDU1LjEwMTZaTTU1LjY2NDEgMTI3LjQ2OVYxMjguNjQxSDUxLjE2NDFWMTI3LjQ2OUg1NS42NjQxWk01OS44NzMgMTM2SDU4LjA1MDhMNTguMDYyNSAxMzQuODM0SDU5Ljg3M0M2MC40MDA0IDEzNC44MzQgNjAuODQxOCAxMzQuNzE5IDYxLjE5NzMgMTM0LjQ4OEM2MS41NTY2IDEzNC4yNTggNjEuODI2MiAxMzMuOTI4IDYyLjAwNTkgMTMzLjQ5OEM2Mi4xODk1IDEzMy4wNjggNjIuMjgxMiAxMzIuNTU3IDYyLjI4MTIgMTMxLjk2M1YxMzEuNUM2Mi4yODEyIDEzMS4wMzkgNjIuMjI4NSAxMzAuNjMxIDYyLjEyMyAxMzAuMjc1QzYyLjAyMTUgMTI5LjkyIDYxLjg2OTEgMTI5LjYyMSA2MS42NjYgMTI5LjM3OUM2MS40NjY4IDEyOS4xMzcgNjEuMjIwNyAxMjguOTUzIDYwLjkyNzcgMTI4LjgyOEM2MC42Mzg3IDEyOC43MDMgNjAuMzA0NyAxMjguNjQxIDU5LjkyNTggMTI4LjY0MUg1OC4wMTU2VjEyNy40NjlINTkuOTI1OEM2MC40OTIyIDEyNy40NjkgNjEuMDA5OCAxMjcuNTY0IDYxLjQ3ODUgMTI3Ljc1NkM2MS45NDczIDEyNy45NDMgNjIuMzUxNiAxMjguMjE1IDYyLjY5MTQgMTI4LjU3QzYzLjAzNTIgMTI4LjkyNiA2My4yOTg4IDEyOS4zNTIgNjMuNDgyNCAxMjkuODQ4QzYzLjY2NiAxMzAuMzQ0IDYzLjc1NzggMTMwLjg5OCA2My43NTc4IDEzMS41MTJWMTMxLjk2M0M2My43NTc4IDEzMi41NzYgNjMuNjY2IDEzMy4xMzEgNjMuNDgyNCAxMzMuNjI3QzYzLjI5ODggMTM0LjEyMyA2My4wMzUyIDEzNC41NDkgNjIuNjkxNCAxMzQuOTA0QzYyLjM0NzcgMTM1LjI1NiA2MS45Mzc1IDEzNS41MjcgNjEuNDYwOSAxMzUuNzE5QzYwLjk4ODMgMTM1LjkwNiA2MC40NTkgMTM2IDU5Ljg3MyAxMzZaTTU4Ljg2NTIgMTI3LjQ2OVYxMzZINTcuMzk0NVYxMjcuNDY5SDU4Ljg2NTJaIiBmaWxsPSJ3aGl0ZSIgZmlsbC1vcGFjaXR5PSIwLjgiLz4KPC9zdmc+Cg==", + "description": "Displays current status.", + "descriptor": { + "type": "rpc", + "sizeX": 2, + "sizeY": 2, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '180px',\n previewHeight: '180px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-status-widget-basic-config", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Status widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":null}" + }, + "tags": [ + "status", + "status widget" + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 79ec4b03c7..132ca3d487 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -114,6 +114,9 @@ import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/ import { ComparisonKeysTableComponent } from '@home/components/widget/config/basic/chart/comparison-keys-table.component'; +import { + StatusWidgetBasicConfigComponent +} from '@home/components/widget/config/basic/indicator/status-widget-basic-config.component'; @NgModule({ declarations: [ @@ -151,7 +154,8 @@ import { ToggleButtonBasicConfigComponent, TimeSeriesChartBasicConfigComponent, ComparisonKeyRowComponent, - ComparisonKeysTableComponent + ComparisonKeysTableComponent, + StatusWidgetBasicConfigComponent ], imports: [ CommonModule, @@ -191,7 +195,8 @@ import { PowerButtonBasicConfigComponent, SliderBasicConfigComponent, ToggleButtonBasicConfigComponent, - TimeSeriesChartBasicConfigComponent + TimeSeriesChartBasicConfigComponent, + StatusWidgetBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -225,5 +230,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + +
+
widgets.status-widget.behavior
+
+
widgets.rpc-state.initial-state
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} + + +
+
+
+
widget-config.card-style
+ + {{ 'widgets.status-widget.on' | translate }} + {{ 'widgets.status-widget.off' | translate }} + +
+ + + + +
+
+
widget-config.card-appearance
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts new file mode 100644 index 0000000000..d2110026f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts @@ -0,0 +1,119 @@ +/// +/// 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { isUndefined } from '@core/utils'; +import { ValueType } from '@shared/models/constants'; +import { + statusWidgetDefaultSettings, + statusWidgetLayoutImages, + statusWidgetLayouts, + statusWidgetLayoutTranslations, + StatusWidgetSettings +} from '@home/components/widget/lib/indicator/status-widget.models'; + +@Component({ + selector: 'tb-status-widget-basic-config', + templateUrl: './status-widget-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.statusWidgetConfigForm.get('targetDevice').value; + } + + statusWidgetLayouts = statusWidgetLayouts; + + statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations; + statusWidgetLayoutImageMap = statusWidgetLayoutImages; + + valueType = ValueType; + + statusWidgetConfigForm: UntypedFormGroup; + + cardStyleMode = 'on'; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.statusWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: StatusWidgetSettings = {...statusWidgetDefaultSettings, ...(configData.config.settings || {})}; + this.statusWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + + initialState: [settings.initialState, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + + onState: [settings.onState, []], + offState: [settings.offState, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + + this.widgetConfig.config.settings.layout = config.layout; + + this.widgetConfig.config.settings.onState = config.onState; + this.widgetConfig.config.settings.offState = config.offState; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html new file mode 100644 index 0000000000..391ade6228 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html @@ -0,0 +1,33 @@ + +
+
+
+ +
+
+
+ {{ icon }} +
+
+
{{ label$ | async }}
+
{{ status$ | async }}
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss new file mode 100644 index 0000000000..8dcefddd2a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss @@ -0,0 +1,99 @@ +/** + * 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. + */ +.tb-status-widget-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + > div:not(.tb-status-widget-overlay), > tb-icon { + z-index: 1; + } + .tb-status-widget-overlay { + position: absolute; + inset: 12px; + } + > div.tb-status-widget-title-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + z-index: 2; + } + .tb-status-widget-content { + width: 100%; + height: 100%; + padding: 16px; + position: relative; + display: flex; + flex-direction: column; + .tb-status-widget-icon-container { + display: flex; + width: 100%; + flex-direction: column; + } + .tb-status-widget-labels-container { + display: flex; + width: 100%; + flex-direction: column; + .tb-status-widget-label { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } + .tb-status-widget-status { + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + &.default { + place-content: flex-start space-between; + align-items: flex-start; + } + &.center { + place-content: center flex-start; + align-items: center; + .tb-status-widget-icon-container { + flex: 1; + place-content: center; + align-items: center; + } + .tb-status-widget-labels-container { + flex-direction: column-reverse; + place-content: center flex-start; + align-items: center; + } + } + &.icon { + place-content: center; + align-items: center; + .tb-status-widget-icon-container { + flex: 1; + place-content: center; + align-items: center; + } + .tb-status-widget-labels-container { + display: none; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts new file mode 100644 index 0000000000..13b2324bf7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts @@ -0,0 +1,241 @@ +/// +/// 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, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + OnInit, + Renderer2, ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; +import { + statusWidgetDefaultSettings, + StatusWidgetLayout, + StatusWidgetSettings, StatusWidgetStateSettings +} from '@home/components/widget/lib/indicator/status-widget.models'; +import { Observable } from 'rxjs'; +import { + backgroundStyle, + ComponentStyle, + iconStyle, + overlayStyle, + textStyle +} from '@shared/models/widget-settings.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { UtilsService } from '@core/services/utils.service'; +import { ValueType } from '@shared/models/constants'; + +const initialStatusWidgetSize = 147; + +@Component({ + selector: 'tb-status-widget', + templateUrl: './status-widget.component.html', + styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class StatusWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('statusWidgetPanel', {static: false}) + statusWidgetPanel: ElementRef; + + @ViewChild('statusWidgetContent', {static: false}) + statusWidgetContent: ElementRef; + + settings: StatusWidgetSettings; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + overlayInset = '12px'; + borderRadius = ''; + + layout: StatusWidgetLayout; + + showLabel = true; + label$: Observable; + labelStyle: ComponentStyle = {}; + + showStatus = true; + status$: Observable; + statusStyle: ComponentStyle = {}; + + icon = ''; + iconStyle: ComponentStyle = {}; + + private panelResize$: ResizeObserver; + + private onLabel$: Observable; + private onStatus$: Observable; + private onBackground$: Observable; + private onBackgroundDisabled$: Observable; + + private offLabel$: Observable; + private offStatus$: Observable; + private offBackground$: Observable; + private offBackgroundDisabled$: Observable; + + private state = false; + private disabled = false; + private disabledState = false; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + private utils: UtilsService, + protected cd: ChangeDetectorRef, + private elementRef: ElementRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...statusWidgetDefaultSettings, ...this.ctx.settings}; + this.layout = this.settings.layout; + + this.onLabel$ = this.ctx.registerLabelPattern(this.settings.onState.label, this.onLabel$); + this.onStatus$ = this.ctx.registerLabelPattern(this.settings.onState.status, this.onStatus$); + this.onBackground$ = backgroundStyle(this.settings.onState.background, this.imagePipe, this.sanitizer); + this.onBackgroundDisabled$ = backgroundStyle(this.settings.onState.backgroundDisabled, this.imagePipe, this.sanitizer); + + this.offLabel$ = this.ctx.registerLabelPattern(this.settings.offState.label, this.offLabel$); + this.offStatus$ = this.ctx.registerLabelPattern(this.settings.offState.status, this.offStatus$); + this.offBackground$ = backgroundStyle(this.settings.offState.background, this.imagePipe, this.sanitizer); + this.offBackgroundDisabled$ = backgroundStyle(this.settings.offState.backgroundDisabled, this.imagePipe, this.sanitizer); + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; + this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onState(value) + }); + + const disabledStateSettings = + {...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')}; + this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onDisabled(value) + }); + + this.loading$.subscribe((loading) => { + this.updateDisabledState(loading || this.disabled); + }); + + this.updateStyle(this.state, this.disabled); + } + + ngAfterViewInit(): void { + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'overflow', 'visible'); + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'position', 'absolute'); + this.panelResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.panelResize$.observe(this.statusWidgetPanel.nativeElement); + if (this.showLabel) { + this.panelResize$.observe(this.statusWidgetPanel.nativeElement); + } + this.onResize(); + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.panelResize$) { + this.panelResize$.disconnect(); + } + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + this.borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}}; + this.cd.detectChanges(); + } + + private onState(value: boolean): void { + const newState = !!value; + if (this.state !== newState) { + this.state = newState; + this.updateStyle(this.state, this.disabled || this.disabledState); + } + } + + private onDisabled(value: boolean): void { + const newDisabled = !!value; + if (this.disabled !== newDisabled) { + this.updateDisabledState(this.disabled); + } + } + + private updateDisabledState(disabled: boolean) { + this.disabledState = disabled; + this.updateStyle(this.state, this.disabledState); + } + + private onResize() { + const panelWidth = this.statusWidgetPanel.nativeElement.getBoundingClientRect().width; + const panelHeight = this.statusWidgetPanel.nativeElement.getBoundingClientRect().height; + const targetSize = Math.min(panelWidth, panelHeight); + const scale = targetSize / initialStatusWidgetSize; + const width = initialStatusWidgetSize; + const height = initialStatusWidgetSize; + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'width', width + 'px'); + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'height', height + 'px'); + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'transform', `scale(${scale})`); + this.overlayInset = (Math.floor(12 * scale * 100) / 100) + 'px'; + this.cd.markForCheck(); + } + + private updateStyle(state: boolean, disabled: boolean) { + let stateSettings: StatusWidgetStateSettings; + if (state) { + this.label$ = this.onLabel$; + this.status$ = this.onStatus$; + this.backgroundStyle$ = disabled ? this.onBackgroundDisabled$ : this.onBackground$; + stateSettings = this.settings.onState; + } else { + this.label$ = this.offLabel$; + this.status$ = this.offStatus$; + this.backgroundStyle$ = disabled ? this.offBackgroundDisabled$ : this.offBackground$; + stateSettings = this.settings.offState; + } + this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon; + this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon; + this.icon = stateSettings.icon; + + const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor; + const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor; + + this.labelStyle = textStyle(stateSettings.labelFont); + this.labelStyle.color = primaryColor; + + this.statusStyle = textStyle(stateSettings.statusFont); + this.statusStyle.color = secondaryColor; + + this.iconStyle = iconStyle(stateSettings.iconSize, stateSettings.iconSizeUnit); + this.iconStyle.color = primaryColor; + + this.overlayStyle = overlayStyle(disabled ? stateSettings.backgroundDisabled.overlay : stateSettings.background.overlay); + if (this.borderRadius) { + this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}}; + } + this.cd.detectChanges(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts new file mode 100644 index 0000000000..2ef999c6f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts @@ -0,0 +1,204 @@ +/// +/// 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 { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; +import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; + +export enum StatusWidgetLayout { + default = 'default', + center = 'center', + icon = 'icon' +} + +export const statusWidgetLayouts = Object.keys(StatusWidgetLayout) as StatusWidgetLayout[]; + +export const statusWidgetLayoutTranslations = new Map( + [ + [StatusWidgetLayout.default, 'widgets.status-widget.layout-default'], + [StatusWidgetLayout.center, 'widgets.status-widget.layout-center'], + [StatusWidgetLayout.icon, 'widgets.status-widget.layout-icon'] + ] +); + +export const statusWidgetLayoutImages = new Map( + [ + [StatusWidgetLayout.default, 'assets/widget/status-widget/default-layout.svg'], + [StatusWidgetLayout.center, 'assets/widget/status-widget/center-layout.svg'], + [StatusWidgetLayout.icon, 'assets/widget/status-widget/icon-layout.svg'] + ] +); + +export interface StatusWidgetStateSettings { + showLabel: boolean; + label: string; + labelFont: Font; + showStatus: boolean; + status: string; + statusFont: Font; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; + primaryColor: string; + secondaryColor: string; + background: BackgroundSettings; + primaryColorDisabled: string; + secondaryColorDisabled: string; + backgroundDisabled: BackgroundSettings; +} + +export interface StatusWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + layout: StatusWidgetLayout; + onState: StatusWidgetStateSettings; + offState: StatusWidgetStateSettings; +} + +export const statusWidgetDefaultSettings: StatusWidgetSettings = { + initialState: { + action: GetValueAction.EXECUTE_RPC, + defaultValue: false, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + layout: StatusWidgetLayout.default, + onState: { + showLabel: true, + label: 'Window left corner', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + showStatus: true, + status: 'Opened', + statusFont: { + family: 'Roboto', + size: 10, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '20px' + }, + icon: 'mdi:curtains', + iconSize: 32, + iconSizeUnit: 'px', + primaryColor: '#fff', + secondaryColor: 'rgba(255, 255, 255, 0.80)', + background: { + type: BackgroundType.color, + color: '#3F52DD', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + primaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + backgroundDisabled: { + type: BackgroundType.color, + color: '#CACACA', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } + }, + offState: { + showLabel: true, + label: 'Window left corner', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + showStatus: true, + status: 'Closed', + statusFont: { + family: 'Roboto', + size: 10, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '20px' + }, + icon: 'mdi:curtains-closed', + iconSize: 32, + iconSizeUnit: 'px', + primaryColor: 'rgba(0, 0, 0, 0.87)', + secondaryColor: 'rgba(0, 0, 0, 0.54)', + background: { + type: BackgroundType.color, + color: '#FFF', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + primaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + backgroundDisabled: { + type: BackgroundType.color, + color: '#CACACA', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html new file mode 100644 index 0000000000..40f26c6e9c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html @@ -0,0 +1,111 @@ + + +
+ + {{ 'widgets.status-widget.label' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.status-widget.status' | translate }} + +
+ + + + + +
+
+
+
+ {{ 'widgets.status-widget.icon' | translate }} +
+
+ + + + + + +
+
+
+
{{ 'widgets.status-widget.color-palette' | translate }}
+
+
+
widgets.status-widget.primary
+ + +
+ +
+
widgets.status-widget.secondary
+ + +
+ +
+
widgets.status-widget.background
+ + +
+
+
+
+
{{ 'widgets.status-widget.disabled-color-palette' | translate }}
+
+
+
widgets.status-widget.primary
+ + +
+ +
+
widgets.status-widget.secondary
+ + +
+ +
+
widgets.status-widget.background
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts new file mode 100644 index 0000000000..8425264d89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts @@ -0,0 +1,158 @@ +/// +/// 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, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { merge } from 'rxjs'; +import { + StatusWidgetLayout, + StatusWidgetStateSettings +} from '@home/components/widget/lib/indicator/status-widget.models'; + +@Component({ + selector: 'tb-status-widget-state-settings', + templateUrl: './status-widget-state-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StatusWidgetStateSettingsComponent), + multi: true + } + ] +}) +export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, ControlValueAccessor { + + StatusWidgetLayout = StatusWidgetLayout; + + @Input() + disabled: boolean; + + @Input() + layout: StatusWidgetLayout; + + private modelValue: StatusWidgetStateSettings; + + private propagateChange = null; + + public stateSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit(): void { + this.stateSettingsFormGroup = this.fb.group({ + showLabel: [null, []], + label: [null, []], + labelFont: [null, []], + showStatus: [null, []], + status: [null, []], + statusFont: [null, []], + icon: [null, []], + iconSize: [null, []], + iconSizeUnit: [null, []], + primaryColor: [null, []], + secondaryColor: [null, []], + background: [null, []], + primaryColorDisabled: [null, []], + secondaryColorDisabled: [null, []], + backgroundDisabled: [null, []] + }); + this.stateSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + merge(this.stateSettingsFormGroup.get('showLabel').valueChanges, + this.stateSettingsFormGroup.get('showStatus').valueChanges) + .subscribe(() => { + this.updateValidators(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['layout'].includes(propName)) { + this.updateValidators(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.stateSettingsFormGroup.disable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: StatusWidgetStateSettings): void { + this.modelValue = value; + this.stateSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + } + + private updateValidators() { + if (this.layout === StatusWidgetLayout.icon) { + this.stateSettingsFormGroup.get('showLabel').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('label').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('showStatus').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('status').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColor').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColorDisabled').disable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.get('showLabel').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('showStatus').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColor').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColorDisabled').enable({emitEvent: false}); + const showLabel: boolean = this.stateSettingsFormGroup.get('showLabel').value; + const showStatus: boolean = this.stateSettingsFormGroup.get('showStatus').value; + if (showLabel) { + this.stateSettingsFormGroup.get('label').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('labelFont').enable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.get('label').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false}); + } + if (showStatus) { + this.stateSettingsFormGroup.get('status').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('statusFont').enable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.get('status').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false}); + } + } + } + + private updateModel() { + this.modelValue = this.stateSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 3742708e1a..ced092b8cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -145,6 +145,9 @@ import { import { TimeSeriesChartGridSettingsComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component'; +import { + StatusWidgetStateSettingsComponent +} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; @NgModule({ declarations: [ @@ -198,6 +201,7 @@ import { TimeSeriesChartStatesPanelComponent, TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, + StatusWidgetStateSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -257,6 +261,7 @@ import { TimeSeriesChartStatesPanelComponent, TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, + StatusWidgetStateSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 7537ef0bc4..3b59ef6ead 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -86,6 +86,7 @@ import { PowerButtonWidgetComponent } from '@home/components/widget/lib/rpc/powe import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component'; import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component'; import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component'; +import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component'; @NgModule({ declarations: @@ -138,7 +139,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char PowerButtonWidgetComponent, SliderWidgetComponent, ToggleButtonWidgetComponent, - TimeSeriesChartWidgetComponent + TimeSeriesChartWidgetComponent, + StatusWidgetComponent ], imports: [ CommonModule, @@ -195,7 +197,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char PowerButtonWidgetComponent, SliderWidgetComponent, ToggleButtonWidgetComponent, - TimeSeriesChartWidgetComponent + TimeSeriesChartWidgetComponent, + StatusWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 35f48dfc0c..9dc04a9fa8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5461,6 +5461,25 @@ "signal-strength-card-style": "Signal strength card style", "no-signal-rssi-value": "\"No signal\" rssi value" }, + "status-widget": { + "behavior": "Behavior", + "layout": "Layout", + "layout-default": "Default", + "layout-center": "Center", + "layout-icon": "Icon", + "on": "On", + "off": "Off", + "label": "Label", + "status": "Status", + "icon": "Icon", + "color-palette": "Color palette", + "disabled-color-palette": "Disabled color palette", + "primary": "Primary", + "primary-color-hint": "Color of icon and label", + "secondary": "Secondary", + "secondary-color-hint": "Color of status", + "background": "Background" + }, "chart": { "common-settings": "Common settings", "enable-stacking-mode": "Enable stacking mode", diff --git a/ui-ngx/src/assets/widget/status-widget/center-layout.svg b/ui-ngx/src/assets/widget/status-widget/center-layout.svg new file mode 100644 index 0000000000..27e174a619 --- /dev/null +++ b/ui-ngx/src/assets/widget/status-widget/center-layout.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/status-widget/default-layout.svg b/ui-ngx/src/assets/widget/status-widget/default-layout.svg new file mode 100644 index 0000000000..08cb74e595 --- /dev/null +++ b/ui-ngx/src/assets/widget/status-widget/default-layout.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/status-widget/icon-layout.svg b/ui-ngx/src/assets/widget/status-widget/icon-layout.svg new file mode 100644 index 0000000000..690c374ed9 --- /dev/null +++ b/ui-ngx/src/assets/widget/status-widget/icon-layout.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From da3b496a68c4ad5de6ff0458e6d3563ebca024ab Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 22:47:30 +0200 Subject: [PATCH 074/147] dependency updated to fix userSettingsDao.findByTypeAndPath: hypersistence-utils-hibernate-63:3.7.4 for hibernate-core:6.4.4. Test and notes added --- .../common/data/settings/UserSettings.java | 2 ++ dao/pom.xml | 2 +- .../dao/sql/user/JpaUserSettingsDao.java | 2 ++ .../dao/sql/user/JpaUserSettingsDaoTest.java | 32 +++++++++++++++++++ pom.xml | 4 +-- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java index 79556080ff..0956c256a3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.ToString; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -48,6 +49,7 @@ public class UserSettings implements Serializable { private transient JsonNode settings; @JsonIgnore + @ToString.Exclude private byte[] settingsBytes; public JsonNode getSettings() { diff --git a/dao/pom.xml b/dao/pom.xml index cc34d77c85..40421094f0 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -238,7 +238,7 @@
io.hypersistence - hypersistence-utils-hibernate-62 + hypersistence-utils-hibernate-63 org.apache.xmlgraphics diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java index 52a78c41fe..73b616e7ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -40,6 +40,7 @@ public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService i @Override public UserSettings save(TenantId tenantId, UserSettings userSettings) { + log.trace("save [{}][{}]", tenantId, userSettings); return DaoUtil.getData(userSettingsRepository.save(new UserSettingsEntity(userSettings))); } @@ -55,6 +56,7 @@ public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService i @Override public List findByTypeAndPath(TenantId tenantId, UserSettingsType type, String... path) { + log.trace("findByTypeAndPath [{}][{}][{}]", tenantId, type, path); return DaoUtil.convertDataList(userSettingsRepository.findByTypeAndPathExisting(type.name(), path)); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java index bf30ebb83b..2151f728d0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.user; import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -37,11 +38,14 @@ import org.thingsboard.server.dao.user.UserSettingsDao; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Index.atIndex; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.service.AbstractServiceTest.SYSTEM_TENANT_ID; +@Slf4j public class JpaUserSettingsDaoTest extends AbstractJpaDaoTest { private UUID tenantId; @@ -77,6 +81,34 @@ public class JpaUserSettingsDaoTest extends AbstractJpaDaoTest { assertNull(retrievedUserSettings2); } + // If Hibernate fail to bind JSON path please check the hypersistence-utils-hibernate-XX artifact name and version in the dependency management + // Example: java.lang.ClassCastException: class [Ljava.lang.String; cannot be cast to class [B ([Ljava.lang.String; and [B are in module java.base of loader 'bootstrap') + @Test + public void testFindByTypeAndJsonPath() { + UserSettings userSettings = createUserSettings(user.getId()); + log.warn("userSettings {}", userSettings); + + userSettings.setSettings(JacksonUtil.toJsonNode("{\"text\":\"bla1\",\"sessions\":{\"tenantFcmToken\":{\"fcmTokenTimestamp\":0}}}")); + + userSettingsDao.save(SYSTEM_TENANT_ID, userSettings); + + assertThat(userSettingsDao.findByTypeAndPath(SYSTEM_TENANT_ID, UserSettingsType.GENERAL, "text")) + .isNotEmpty().hasSize(1).contains(userSettings, atIndex(0)); + + assertThat(userSettingsDao.findByTypeAndPath(SYSTEM_TENANT_ID, UserSettingsType.GENERAL, "sessions", "tenantFcmToken")) + .isNotEmpty().hasSize(1).contains(userSettings, atIndex(0)); + + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.GENERAL, "mistery")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.GENERAL, "text", "text")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.GENERAL, "text", "lvl2")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.MOBILE, "text")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.MOBILE, "sessions", "1")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.MOBILE, "text", "text")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.MOBILE, "")).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.MOBILE, null)).isEmpty(); + assertThat(userSettingsDao.findByTypeAndPath(user.getTenantId(), UserSettingsType.MOBILE)).isEmpty(); + } + private UserSettings createUserSettings(UserId userId) { UserSettings userSettings = new UserSettings(); userSettings.setType(UserSettingsType.GENERAL); diff --git a/pom.xml b/pom.xml index 36d0d9dc71..2130dbb53d 100755 --- a/pom.xml +++ b/pom.xml @@ -127,7 +127,7 @@ 3.7.1 10.1.3 8.0.1.Final - 3.7.3 + 3.7.4 4.0.2 3.0.2 1.7.5 @@ -2034,7 +2034,7 @@ io.hypersistence - hypersistence-utils-hibernate-62 + hypersistence-utils-hibernate-63 ${hypersistence-utils.version} From 5769bfab0a2e1185a60f6ae256664dff6708c10b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 23:16:32 +0200 Subject: [PATCH 075/147] dependency: manage netty versions --- pom.xml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pom.xml b/pom.xml index 2130dbb53d..8b475704bf 100755 --- a/pom.xml +++ b/pom.xml @@ -1314,6 +1314,11 @@ netty-codec ${netty.version} + + io.netty + netty-codec-dns + ${netty.version} + io.netty netty-codec-http @@ -1354,6 +1359,27 @@ netty-resolver ${netty.version} + + io.netty + netty-resolver-dns + ${netty.version} + + + io.netty + netty-resolver-dns-classes-macos + ${netty.version} + + + io.netty + netty-resolver-dns-native-macos + ${netty.version} + + + io.netty + netty-resolver-dns-native-macos + ${netty.version} + osx-x86_64 + io.netty netty-transport From 9119352495194e042b2d670f61d76ceb0b7411e9 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 23:17:09 +0200 Subject: [PATCH 076/147] dependency: manage grpc-rls version --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 8b475704bf..13ff04d9df 100755 --- a/pom.xml +++ b/pom.xml @@ -1672,6 +1672,11 @@ grpc-protobuf-lite ${grpc.version} + + io.grpc + grpc-rls + ${grpc.version} + io.grpc grpc-util From e3f2974fcd8e5973043680f9f7f02b4cd439987c Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 23:25:15 +0200 Subject: [PATCH 077/147] dependency: bouncycastle jdk15on moved to jdk18on --- common/message/pom.xml | 2 +- common/transport/transport-api/pom.xml | 4 ++-- common/util/pom.xml | 4 ++-- dao/pom.xml | 4 ++-- pom.xml | 6 +++--- rule-engine/rule-engine-components/pom.xml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/message/pom.xml b/common/message/pom.xml index 8a3e278a4e..cb7e2d078a 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -42,7 +42,7 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on org.slf4j diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 255b9ad8e4..4e9ad1ba31 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -127,11 +127,11 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on diff --git a/common/util/pom.xml b/common/util/pom.xml index 1952e5a7ad..ce8d7ef36c 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -38,11 +38,11 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on org.springframework diff --git a/dao/pom.xml b/dao/pom.xml index 40421094f0..061c130a69 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -81,7 +81,7 @@ org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on org.springframework.boot @@ -190,7 +190,7 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on org.springframework.boot diff --git a/pom.xml b/pom.xml index 13ff04d9df..c95160f404 100755 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ 2.2.21 0.8 1.19.0 - 1.70 + 1.78.1 2.0.1 42.7.3 org/thingsboard/server/gen/**/*, @@ -1875,12 +1875,12 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on ${bouncycastle.version} org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on ${bouncycastle.version} diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 895da628b9..224aea3137 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -114,7 +114,7 @@ org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on org.locationtech.spatial4j From e532e0f2a94786acb13690f351ee5b414079e09f Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Apr 2024 23:49:35 +0200 Subject: [PATCH 078/147] fixed import org.bouncycastle.pqc.legacy.math.linearalgebra.ByteUtils, test added for EncryptionUtil --- .../server/common/msg/EncryptionUtil.java | 2 +- .../server/common/msg/EncryptionUtilTest.java | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 common/message/src/test/java/org/thingsboard/server/common/msg/EncryptionUtilTest.java diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index df5a4a728c..27c9bdecf8 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -17,7 +17,7 @@ package org.thingsboard.server.common.msg; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.crypto.digests.SHA3Digest; -import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; +import org.bouncycastle.pqc.legacy.math.linearalgebra.ByteUtils; /** * @author Valerii Sosliuk diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/EncryptionUtilTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/EncryptionUtilTest.java new file mode 100644 index 0000000000..d67fe74fea --- /dev/null +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/EncryptionUtilTest.java @@ -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.msg; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class EncryptionUtilTest { + + @Test + void getSha3Hash256Size() { + assertThat(EncryptionUtil.getSha3Hash("ThingsBoard")) + .isEqualTo("281c7ba06d0ac2ff651fa968572f26a38c96225ea54644522837b54b9c6144f7"); + } + +} From ee589386cdc47390df193f3a2751e46a021553de Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 20 Apr 2024 00:02:54 +0200 Subject: [PATCH 079/147] dependency management com.fasterxml.jackson.dataformat:jackson-dataformat-yaml --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index c95160f404..eacde2770a 100755 --- a/pom.xml +++ b/pom.xml @@ -1507,6 +1507,11 @@ jackson-dataformat-cbor ${jackson.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + com.fasterxml.jackson.datatype jackson-datatype-jdk8 From c0507e75f7bfb561c6f96d68b3b41a9272910961 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 20 Apr 2024 12:08:55 +0200 Subject: [PATCH 080/147] Spring 3.2 notes: Relocate launcher classes: org.springframework.boot.loader.launch.PropertiesLauncher https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes#nested-jar-support --- msa/monitoring/docker/start-tb-monitoring.sh | 2 +- msa/tb-node/docker/start-tb-node.sh | 6 +++--- msa/tb/docker/install-tb.sh | 2 +- msa/tb/docker/start-tb.sh | 2 +- msa/tb/docker/upgrade-tb.sh | 2 +- msa/transport/coap/docker/start-tb-coap-transport.sh | 2 +- msa/transport/http/docker/start-tb-http-transport.sh | 2 +- msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh | 2 +- msa/transport/mqtt/docker/start-tb-mqtt-transport.sh | 2 +- msa/transport/snmp/docker/start-tb-snmp-transport.sh | 2 +- msa/vc-executor-docker/docker/start-tb-vc-executor.sh | 2 +- packaging/java/scripts/install/install.sh | 2 +- packaging/java/scripts/install/install_dev_db.sh | 2 +- packaging/java/scripts/install/upgrade.sh | 2 +- packaging/java/scripts/install/upgrade_dev_db.sh | 2 +- packaging/java/scripts/windows/install.bat | 2 +- packaging/java/scripts/windows/install_dev_db.bat | 2 +- packaging/java/scripts/windows/upgrade.bat | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) diff --git a/msa/monitoring/docker/start-tb-monitoring.sh b/msa/monitoring/docker/start-tb-monitoring.sh index 692719b1f0..026fa58888 100755 --- a/msa/monitoring/docker/start-tb-monitoring.sh +++ b/msa/monitoring/docker/start-tb-monitoring.sh @@ -28,4 +28,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.monitoring.ThingsboardMonitoringApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=$CONF_FOLDER/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/tb-node/docker/start-tb-node.sh b/msa/tb-node/docker/start-tb-node.sh index 76ed35cc7c..c164954a82 100755 --- a/msa/tb-node/docker/start-tb-node.sh +++ b/msa/tb-node/docker/start-tb-node.sh @@ -41,7 +41,7 @@ if [ "$INSTALL_TB" == "true" ]; then -Dspring.jpa.hibernate.ddl-auto=none \ -Dinstall.upgrade=false \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher elif [ "$UPGRADE_TB" == "true" ]; then @@ -59,7 +59,7 @@ elif [ "$UPGRADE_TB" == "true" ]; then -Dinstall.upgrade=true \ -Dinstall.upgrade.from_version=${fromVersion} \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher else @@ -68,6 +68,6 @@ else exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardServerApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher fi diff --git a/msa/tb/docker/install-tb.sh b/msa/tb/docker/install-tb.sh index 75970a79ef..9567908316 100644 --- a/msa/tb/docker/install-tb.sh +++ b/msa/tb/docker/install-tb.sh @@ -53,6 +53,6 @@ java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardI -Dspring.jpa.hibernate.ddl-auto=none \ -Dinstall.upgrade=false \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher echo "${pkg.upgradeVersion}" > ${upgradeversion} diff --git a/msa/tb/docker/start-tb.sh b/msa/tb/docker/start-tb.sh index 49f2e3e5ae..21a62f5b6d 100755 --- a/msa/tb/docker/start-tb.sh +++ b/msa/tb/docker/start-tb.sh @@ -34,7 +34,7 @@ if [ -f ${firstlaunch} ]; then java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardServerApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=${CONF_FOLDER}/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher else echo "ERROR: ThingsBoard is not installed" fi diff --git a/msa/tb/docker/upgrade-tb.sh b/msa/tb/docker/upgrade-tb.sh index dbfdf68a32..545bf4b8d6 100644 --- a/msa/tb/docker/upgrade-tb.sh +++ b/msa/tb/docker/upgrade-tb.sh @@ -40,7 +40,7 @@ java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardI -Dinstall.upgrade=true \ -Dinstall.upgrade.from_version=${fromVersion} \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher echo "${pkg.upgradeVersion}" > ${upgradeversion} diff --git a/msa/transport/coap/docker/start-tb-coap-transport.sh b/msa/transport/coap/docker/start-tb-coap-transport.sh index f073494bb2..75b56c205d 100755 --- a/msa/transport/coap/docker/start-tb-coap-transport.sh +++ b/msa/transport/coap/docker/start-tb-coap-transport.sh @@ -30,4 +30,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.coap.ThingsboardCoapTransportApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/transport/http/docker/start-tb-http-transport.sh b/msa/transport/http/docker/start-tb-http-transport.sh index 3737523b39..eacb65e2ef 100755 --- a/msa/transport/http/docker/start-tb-http-transport.sh +++ b/msa/transport/http/docker/start-tb-http-transport.sh @@ -30,4 +30,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.http.ThingsboardHttpTransportApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh b/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh index e1e95fb78f..d88ad849f2 100755 --- a/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh +++ b/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh @@ -30,4 +30,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.lwm2m.ThingsboardLwm2mTransportApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/transport/mqtt/docker/start-tb-mqtt-transport.sh b/msa/transport/mqtt/docker/start-tb-mqtt-transport.sh index ea0f21ea24..7350965861 100755 --- a/msa/transport/mqtt/docker/start-tb-mqtt-transport.sh +++ b/msa/transport/mqtt/docker/start-tb-mqtt-transport.sh @@ -30,4 +30,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.mqtt.ThingsboardMqttTransportApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/transport/snmp/docker/start-tb-snmp-transport.sh b/msa/transport/snmp/docker/start-tb-snmp-transport.sh index 1d39301d38..20d4809958 100644 --- a/msa/transport/snmp/docker/start-tb-snmp-transport.sh +++ b/msa/transport/snmp/docker/start-tb-snmp-transport.sh @@ -30,4 +30,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.snmp.ThingsboardSnmpTransportApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/vc-executor-docker/docker/start-tb-vc-executor.sh b/msa/vc-executor-docker/docker/start-tb-vc-executor.sh index ec7bf39427..09aef7d97f 100755 --- a/msa/vc-executor-docker/docker/start-tb-vc-executor.sh +++ b/msa/vc-executor-docker/docker/start-tb-vc-executor.sh @@ -30,4 +30,4 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.vc.ThingsboardVersionControlExecutorApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dlogging.config=/config/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/packaging/java/scripts/install/install.sh b/packaging/java/scripts/install/install.sh index 159d612976..450064f6f2 100755 --- a/packaging/java/scripts/install/install.sh +++ b/packaging/java/scripts/install/install.sh @@ -52,7 +52,7 @@ su -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.s -Dspring.jpa.hibernate.ddl-auto=none \ -Dinstall.upgrade=false \ -Dlogging.config=${pkg.installFolder}/bin/install/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher" "$run_user" + org.springframework.boot.loader.launch.PropertiesLauncher" "$run_user" if [ $? -ne 0 ]; then echo "ThingsBoard installation failed!" diff --git a/packaging/java/scripts/install/install_dev_db.sh b/packaging/java/scripts/install/install_dev_db.sh index ca9affe596..78100554a9 100644 --- a/packaging/java/scripts/install/install_dev_db.sh +++ b/packaging/java/scripts/install/install_dev_db.sh @@ -36,7 +36,7 @@ sudo -u "$run_user" -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main= -Dspring.jpa.hibernate.ddl-auto=none \ -Dinstall.upgrade=false \ -Dlogging.config=logback.xml \ - org.springframework.boot.loader.PropertiesLauncher" + org.springframework.boot.loader.launch.PropertiesLauncher" if [ $? -ne 0 ]; then echo "ThingsBoard DB installation failed!" diff --git a/packaging/java/scripts/install/upgrade.sh b/packaging/java/scripts/install/upgrade.sh index 9011db73d2..2b87b26aa6 100755 --- a/packaging/java/scripts/install/upgrade.sh +++ b/packaging/java/scripts/install/upgrade.sh @@ -51,7 +51,7 @@ su -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.s -Dinstall.upgrade=true \ -Dinstall.upgrade.from_version=${fromVersion} \ -Dlogging.config=${pkg.installFolder}/bin/install/logback.xml \ - org.springframework.boot.loader.PropertiesLauncher" "$run_user" + org.springframework.boot.loader.launch.PropertiesLauncher" "$run_user" if [ $? -ne 0 ]; then echo "ThingsBoard upgrade failed!" diff --git a/packaging/java/scripts/install/upgrade_dev_db.sh b/packaging/java/scripts/install/upgrade_dev_db.sh index 43cd463706..d0c42eaaa6 100755 --- a/packaging/java/scripts/install/upgrade_dev_db.sh +++ b/packaging/java/scripts/install/upgrade_dev_db.sh @@ -57,7 +57,7 @@ sudo -u "$run_user" -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main= -Dinstall.upgrade=true \ -Dinstall.upgrade.from_version=${fromVersion} \ -Dlogging.config=logback.xml \ - org.springframework.boot.loader.PropertiesLauncher" + org.springframework.boot.loader.launch.PropertiesLauncher" if [ $? -ne 0 ]; then echo "ThingsBoard DB installation failed!" diff --git a/packaging/java/scripts/windows/install.bat b/packaging/java/scripts/windows/install.bat index b0f6c9b203..075755886e 100644 --- a/packaging/java/scripts/windows/install.bat +++ b/packaging/java/scripts/windows/install.bat @@ -34,7 +34,7 @@ java -cp "%jarfile%" -Dloader.main=org.thingsboard.server.ThingsboardInstallAppl -Dspring.jpa.hibernate.ddl-auto=none^ -Dinstall.upgrade=false^ -Dlogging.config="%BASE%\install\logback.xml"^ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher if errorlevel 1 ( @echo ThingsBoard installation failed! diff --git a/packaging/java/scripts/windows/install_dev_db.bat b/packaging/java/scripts/windows/install_dev_db.bat index 6abc80fef1..522d0bfc93 100644 --- a/packaging/java/scripts/windows/install_dev_db.bat +++ b/packaging/java/scripts/windows/install_dev_db.bat @@ -19,7 +19,7 @@ java -cp %jarfile% -Dloader.main=org.thingsboard.server.ThingsboardInstallApplic -Dspring.jpa.hibernate.ddl-auto=none^ -Dinstall.upgrade=false^ -Dlogging.config=%BASE%\windows\install\logback.xml^ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher if errorlevel 1 ( @echo ThingsBoard DB installation failed! diff --git a/packaging/java/scripts/windows/upgrade.bat b/packaging/java/scripts/windows/upgrade.bat index 704d4c170d..b86121b8f1 100644 --- a/packaging/java/scripts/windows/upgrade.bat +++ b/packaging/java/scripts/windows/upgrade.bat @@ -34,7 +34,7 @@ java -cp "%jarfile%" -Dloader.main=org.thingsboard.server.ThingsboardInstallAppl -Dinstall.upgrade=true^ -Dinstall.upgrade.from_version=%fromVersion%^ -Dlogging.config="%BASE%\install\logback.xml"^ - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.launch.PropertiesLauncher if errorlevel 1 ( @echo ThingsBoard upgrade failed! From 59daf9123bd1e914b515711151572f4f334f0c4b Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 22 Apr 2024 10:45:39 +0300 Subject: [PATCH 081/147] replaced the use of the deprecated method with the use of a static method --- .../controller/TelemetryController.java | 2 +- .../SequentialTimeseriesPersistenceTest.java | 4 ++-- .../server/common/adaptor/JsonConverter.java | 19 +++++++-------- .../server/common/adaptor/ProtoConverter.java | 5 ++-- .../common/adaptor/JsonConverterTest.java | 24 +++++++++---------- .../coap/adaptors/JsonCoapAdaptor.java | 8 +++---- .../coap/adaptors/ProtoCoapAdaptor.java | 6 ++--- .../transport/http/DeviceApiController.java | 6 ++--- .../server/LwM2mTransportServerHelper.java | 3 +-- .../mqtt/adaptors/JsonMqttAdaptor.java | 10 ++++---- .../mqtt/adaptors/ProtoMqttAdaptor.java | 6 ++--- .../session/SparkplugNodeSessionHandler.java | 2 +- .../common/transport/util/JsonUtils.java | 6 ++--- .../TbSaveToCustomCassandraTableNode.java | 3 +-- .../engine/geo/TbGpsGeofencingActionNode.java | 4 +--- 15 files changed, 49 insertions(+), 59 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index cea449af31..5f1de4e3e3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -647,7 +647,7 @@ public class TelemetryController extends BaseController { Map> telemetryRequest; JsonElement telemetryJson; try { - telemetryJson = new JsonParser().parse(requestBody); + telemetryJson = JsonParser.parseString(requestBody); } catch (Exception e) { return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST); } diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index 7f1df3fdd1..5cae75ef0d 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -147,7 +147,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest void checkDiffBetweenLatestTsForDevicesAndAsset(List devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException { TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ); Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent()); - JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); + JsonObject assetJsonObject = JsonParser.parseString(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); for (Device device : devices) { Long assetValue = assetJsonObject.get(device.getName()).getAsLong(); TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER); @@ -182,7 +182,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { JsonObject jsonObject = new JsonObject(); if (tsKvEntryOpt.isPresent()) { - jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); + jsonObject = JsonParser.parseString(tsKvEntryOpt.get()).getAsJsonObject(); } jsonObject.addProperty(key, value); return jsonObject; diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 42da1d740f..72ae3a0871 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -66,7 +66,6 @@ import java.util.stream.Collectors; public class JsonConverter { private static final Gson GSON = new Gson(); - private static final JsonParser JSON_PARSER = new JsonParser(); private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; private static final String DEVICE_PROPERTY = "device"; @@ -111,7 +110,7 @@ public class JsonConverter { public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) { long durationMs = 0L; if (json != null && !json.isEmpty()) { - return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json)); + return convertToClaimDeviceProto(deviceId, JsonParser.parseString(json)); } return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs); } @@ -160,7 +159,7 @@ public class JsonConverter { result.addProperty("id", msg.getRequestId()); } result.addProperty("method", msg.getMethodName()); - result.add("params", JSON_PARSER.parse(msg.getParams())); + result.add("params", JsonParser.parseString(msg.getParams())); return result; } @@ -367,7 +366,7 @@ public class JsonConverter { json.addProperty(name, entry.getLongV()); break; case JSON_V: - json.add(name, JSON_PARSER.parse(entry.getJsonV())); + json.add(name, JsonParser.parseString(entry.getJsonV())); break; } } @@ -388,7 +387,7 @@ public class JsonConverter { result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getStringV())); break; case JSON_V: - result.add(de.getKv().getKey(), JSON_PARSER.parse(de.getKv().getJsonV())); + result.add(de.getKv().getKey(), JsonParser.parseString(de.getKv().getJsonV())); break; default: throw new IllegalArgumentException("Unsupported data type: " + de.getKv().getType()); @@ -412,7 +411,7 @@ public class JsonConverter { result.add(de.getKey(), new JsonPrimitive(de.getStrValue().get())); break; case JSON: - result.add(de.getKey(), JSON_PARSER.parse(de.getJsonValue().get())); + result.add(de.getKey(), JsonParser.parseString(de.getJsonValue().get())); break; default: throw new IllegalArgumentException("Unsupported data type: " + de.getDataType()); @@ -422,7 +421,7 @@ public class JsonConverter { public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) { if (StringUtils.isEmpty(msg.getError())) { - return JSON_PARSER.parse(msg.getPayload()); + return JsonParser.parseString(msg.getPayload()); } else { JsonObject errorMsg = new JsonObject(); errorMsg.addProperty("error", msg.getError()); @@ -456,7 +455,7 @@ public class JsonConverter { result.addProperty("credentialsValue", payload.getCredentialsValue()); break; case MQTT_BASIC: - result.add("credentialsValue", JSON_PARSER.parse(payload.getCredentialsValue()).getAsJsonObject()); + result.add("credentialsValue", JsonParser.parseString(payload.getCredentialsValue()).getAsJsonObject()); break; case LWM2M_CREDENTIALS: break; @@ -572,7 +571,7 @@ public class JsonConverter { } public static JsonElement parse(String json) { - return JSON_PARSER.parse(json); + return JsonParser.parseString(json); } public static T parse(String json, Class clazz) { @@ -600,7 +599,7 @@ public class JsonConverter { } public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) { - JsonElement jsonElement = JSON_PARSER.parse(json); + JsonElement jsonElement = JsonParser.parseString(json); if (jsonElement.isJsonObject()) { return buildProvisionRequestMsg(jsonElement.getAsJsonObject()); } else { diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java index b90b5f054f..2aa73928e5 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java @@ -39,7 +39,6 @@ import java.util.List; public class ProtoConverter { public static final Gson GSON = new Gson(); - public static final JsonParser JSON_PARSER = new JsonParser(); public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload); @@ -165,7 +164,7 @@ public class ProtoConverter { break; case JSON_V: try { - JSON_PARSER.parse(keyValueProto.getJsonV()); + JsonParser.parseString(keyValueProto.getJsonV()); } catch (Exception e) { throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!"); } @@ -184,7 +183,7 @@ public class ProtoConverter { rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId()); String params = toDeviceRpcRequestMsg.getParams(); try { - JsonElement paramsElement = JSON_PARSER.parse(params); + JsonElement paramsElement = JsonParser.parseString(params); rpcRequestJson.add("params", paramsElement); DynamicMessage dynamicRpcRequest = DynamicProtoUtils.jsonToDynamicMessage(rpcRequestDynamicMessageBuilder, GSON.toJson(rpcRequestJson)); return dynamicRpcRequest.toByteArray(); diff --git a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java index 773ebbea3d..eb11a11f5c 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java @@ -26,8 +26,6 @@ import java.util.ArrayList; public class JsonConverterTest { - private final JsonParser JSON_PARSER = new JsonParser(); - @BeforeEach public void before() { JsonConverter.setTypeCastEnabled(true); @@ -35,55 +33,55 @@ public class JsonConverterTest { @Test public void testParseBigDecimalAsLong() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E+1}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E+1}"), 0L); Assert.assertEquals(10L, result.get(0L).get(0).getLongValue().get().longValue()); } @Test public void testParseBigDecimalAsDouble() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 101E-1}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 101E-1}"), 0L); Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); } @Test public void testParseAttributesBigDecimalAsLong() { - var result = new ArrayList<>(JsonConverter.convertToAttributes(JSON_PARSER.parse("{\"meterReadingDelta\": 1E1}"))); + var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"))); Assert.assertEquals(10L, result.get(0).getLongValue().get().longValue()); } @Test public void testParseAsDoubleWithZero() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 42.0}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 42.0}"), 0L); Assert.assertEquals(42.0, result.get(0L).get(0).getDoubleValue().get(), 0.0); } @Test public void testParseAsDouble() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1.1}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1.1}"), 0L); Assert.assertEquals(1.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); } @Test public void testParseAsLong() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 11}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 11}"), 0L); Assert.assertEquals(11L, result.get(0L).get(0).getLongValue().get().longValue()); } @Test public void testParseBigDecimalAsStringOutOfLongRange() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); Assert.assertEquals("99701010061400066000", result.get(0L).get(0).getStrValue().get()); } @Test public void testParseBigDecimalAsStringOutOfLongRange2() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 99701010061400066001}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 99701010061400066001}"), 0L); Assert.assertEquals("99701010061400066001", result.get(0L).get(0).getStrValue().get()); } @Test public void testParseBigDecimalAsStringOutOfLongRange3() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E19}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E19}"), 0L); Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get()); } @@ -91,7 +89,7 @@ public class JsonConverterTest { public void testParseBigDecimalOutOfLongRangeWithoutParsing() { JsonConverter.setTypeCastEnabled(false); Assertions.assertThrows(JsonSyntaxException.class, () -> { - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); + JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 89701010051400054084}"), 0L); }); } @@ -99,7 +97,7 @@ public class JsonConverterTest { public void testParseBigDecimalOutOfLongRangeWithoutParsing2() { JsonConverter.setTypeCastEnabled(false); Assertions.assertThrows(JsonSyntaxException.class, () -> { - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); }); } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index e7100cac30..50dadd8e87 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -45,7 +45,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { String payload = validatePayload(sessionId, inbound, false); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { throw new AdaptorException(ex); } @@ -55,7 +55,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { String payload = validatePayload(sessionId, inbound, false); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { throw new AdaptorException(ex); } @@ -70,7 +70,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { Optional requestId = CoapTransportResource.getRequestId(inbound); String payload = validatePayload(sessionId, inbound, false); - JsonObject response = new JsonParser().parse(payload).getAsJsonObject(); + JsonObject response = JsonParser.parseString(payload).getAsJsonObject(); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) .setPayload(response.toString()).build(); } @@ -78,7 +78,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { @Override public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException { String payload = validatePayload(sessionId, inbound, false); - return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0); + return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), 0); } @Override diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index f686dc63cf..189620afa1 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -45,7 +45,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { ProtoConverter.validateDescriptor(telemetryMsgDescriptor); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -55,7 +55,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { ProtoConverter.validateDescriptor(attributesMsgDescriptor); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -74,7 +74,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } else { ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor); try { - JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); + JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) .setPayload(response.toString()).build(); } catch (Exception e) { diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index e9674d767e..58a5629dae 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -185,7 +185,7 @@ public class DeviceApiController implements TbTransportService { transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); - transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)), + transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(JsonParser.parseString(json)), new HttpOkCallback(responseWriter)); })); return responseWriter; @@ -205,7 +205,7 @@ public class DeviceApiController implements TbTransportService { transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); - transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)), + transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(JsonParser.parseString(json)), new HttpOkCallback(responseWriter)); })); return responseWriter; @@ -301,7 +301,7 @@ public class DeviceApiController implements TbTransportService { DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { - JsonObject request = new JsonParser().parse(json).getAsJsonObject(); + JsonObject request = JsonParser.parseString(json).getAsJsonObject(); TransportService transportService = transportContext.getTransportService(); transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo), diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index a25ed11d94..86df96f40e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -50,7 +50,6 @@ import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType. public class LwM2mTransportServerHelper { private final LwM2mTransportContext context; - private final static JsonParser JSON_PARSER = new JsonParser(); public void sendParametersOnThingsboardAttribute(List result, SessionInfoProto sessionInfo) { PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder(); @@ -231,7 +230,7 @@ public class LwM2mTransportServerHelper { return kv.getStringV(); case JSON_V: try { - return JSON_PARSER.parse(kv.getJsonV()); + return JsonParser.parseString(kv.getJsonV()); } catch (Exception e) { return null; } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index c8524ee6ce..90174616e6 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -59,7 +59,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { log.debug("Failed to decode post telemetry request", ex); throw new AdaptorException(ex); @@ -70,7 +70,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { log.debug("Failed to decode post attributes request", ex); throw new AdaptorException(ex); @@ -162,7 +162,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { String payload = validatePayload(sessionId, payloadData, false); try { - return new JsonParser().parse(payload); + return JsonParser.parseString(payload); } catch (JsonSyntaxException ex) { log.debug("Payload is in incorrect format: {}", payload); throw new AdaptorException(ex); @@ -175,7 +175,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); result.setRequestId(getRequestId(topicName, topicBase)); String payload = inbound.payload().toString(UTF8); - JsonElement requestBody = new JsonParser().parse(payload); + JsonElement requestBody = JsonParser.parseString(payload); Set clientKeys = toStringSet(requestBody, "clientKeys"); Set sharedKeys = toStringSet(requestBody, "sharedKeys"); if (clientKeys != null) { @@ -208,7 +208,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { int requestId = getRequestId(topicName, topicBase); - return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); + return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), requestId); } catch (IllegalStateException | JsonSyntaxException ex) { log.debug("Failed to decode to server rpc request", ex); throw new AdaptorException(ex); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index ad86096a29..25113dedca 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -50,7 +50,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { byte[] bytes = toBytes(inbound.payload()); Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); } catch (Exception e) { log.debug("Failed to decode post telemetry request", e); throw new AdaptorException(e); @@ -63,7 +63,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { byte[] bytes = toBytes(inbound.payload()); Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); } catch (Exception e) { log.debug("Failed to decode post attributes request", e); throw new AdaptorException(e); @@ -102,7 +102,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); try { int requestId = getRequestId(topicName, topicBase); - JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); + JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); } catch (Exception e) { log.debug("Failed to decode rpc response", e); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 2d58785cdd..05658fe97a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -94,7 +94,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler tsKv) { JsonObject json = new JsonObject(); for (KeyValueProto kv : tsKv) { @@ -43,7 +41,7 @@ public class JsonUtils { json.addProperty(kv.getKey(), kv.getStringV()); break; case JSON_V: - json.add(kv.getKey(), jsonParser.parse(kv.getJsonV())); + json.add(kv.getKey(), JsonParser.parseString(kv.getJsonV())); break; } } @@ -51,7 +49,7 @@ public class JsonUtils { } public static JsonElement parse(String params) { - return jsonParser.parse(params); + return JsonParser.parseString(params); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index 4b957db9cd..1aad815dfa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -72,7 +72,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String TABLE_PREFIX = "cs_tb_"; - private static final JsonParser parser = new JsonParser(); private static final String ENTITY_ID = "$entityId"; private TbSaveToCustomCassandraTableNodeConfiguration config; @@ -168,7 +167,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { } private ListenableFuture save(TbMsg msg, TbContext ctx) { - JsonElement data = parser.parse(msg.getData()); + JsonElement data = JsonParser.parseString(msg.getData()); if (!data.isJsonObject()) { throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data); } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java index 32404cdf81..b2d16434d5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java @@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -75,7 +74,6 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode entityStates = new HashMap<>(); private final Gson gson = new Gson(); - private final JsonParser parser = new JsonParser(); @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { @@ -88,7 +86,7 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode Date: Mon, 22 Apr 2024 10:58:58 +0300 Subject: [PATCH 082/147] UI: Improved component and clear code --- ui-ngx/package.json | 1 - ui-ngx/src/app/core/core.module.ts | 4 +- .../app/core/services/resources.service.ts | 7 +- .../src/app/core/settings/settings.effects.ts | 6 -- .../src/app/core/settings/settings.utils.ts | 10 +- .../translate/translate-default-loader.ts | 14 +++ ui-ngx/src/app/core/utils.ts | 2 + .../login/pages/login/login.component.html | 2 +- .../components/json-content.component.html | 2 +- .../components/json-content.component.ts | 101 ++++++++++++------ .../assets/locale/locale.constant-en_US.json | 1 + ui-ngx/yarn.lock | 7 -- 12 files changed, 97 insertions(+), 60 deletions(-) create mode 100644 ui-ngx/src/app/core/translate/translate-default-loader.ts diff --git a/ui-ngx/package.json b/ui-ngx/package.json index a5304207f5..b8a8e4c9a9 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -43,7 +43,6 @@ "@ngrx/store": "^15.4.0", "@ngrx/store-devtools": "^15.4.0", "@ngx-translate/core": "^14.0.0", - "@ngx-translate/http-loader": "^7.0.0", "@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.js": "^3.2.0", "@tinymce/tinymce-angular": "^7.0.0", diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts index 2f713e405f..a72eb1ada7 100644 --- a/ui-ngx/src/app/core/core.module.ts +++ b/ui-ngx/src/app/core/core.module.ts @@ -31,7 +31,6 @@ import { TranslateModule, TranslateParser } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog'; @@ -41,9 +40,10 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp import { WINDOW_PROVIDERS } from '@core/services/window.service'; import { HotkeyModule } from 'angular2-hotkeys'; import { TranslateDefaultParser } from '@core/translate/translate-default-parser'; +import { TranslateDefaultLoader } from '@core/translate/translate-default-loader'; export function HttpLoaderFactory(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); + return new TranslateDefaultLoader(http); } @NgModule({ diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index bd2402b44c..449e950a65 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -34,6 +34,7 @@ import { select, Store } from '@ngrx/store'; import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { map, tap } from 'rxjs/operators'; +import { RequestConfig } from '@core/http/http-utils'; declare const System; @@ -106,11 +107,11 @@ export class ResourcesService { return this.loadResourceByType(fileType, url); } - public downloadResource(downloadUrl: string): Observable { - return this.http.get(downloadUrl, { + public downloadResource(downloadUrl: string, config?: RequestConfig): Observable { + return this.http.get(downloadUrl, {...config, ...{ responseType: 'arraybuffer', observe: 'response' - }).pipe( + }}).pipe( map((response) => { const headers = response.headers; const filename = headers.get('x-filename'); diff --git a/ui-ngx/src/app/core/settings/settings.effects.ts b/ui-ngx/src/app/core/settings/settings.effects.ts index 56615bf62c..e78641a649 100644 --- a/ui-ngx/src/app/core/settings/settings.effects.ts +++ b/ui-ngx/src/app/core/settings/settings.effects.ts @@ -28,7 +28,6 @@ import { AppState } from '@app/core/core.state'; import { LocalStorageService } from '@app/core/local-storage/local-storage.service'; import { TitleService } from '@app/core/services/title.service'; import { updateUserLang } from '@app/core/settings/settings.utils'; -import { AuthService } from '@core/auth/auth.service'; import { UtilsService } from '@core/services/utils.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions'; @@ -40,7 +39,6 @@ export class SettingsEffects { constructor( private actions$: Actions, private store: Store, - private authService: AuthService, private utils: UtilsService, private router: Router, private localStorageService: LocalStorageService, @@ -49,7 +47,6 @@ export class SettingsEffects { ) { } - persistSettings = createEffect(() => this.actions$.pipe( ofType( SettingsActionTypes.CHANGE_LANGUAGE, @@ -60,7 +57,6 @@ export class SettingsEffects { ) ), {dispatch: false}); - setTranslateServiceLanguage = createEffect(() => this.store.pipe( select(selectSettingsState), map(settings => settings.userLang), @@ -68,7 +64,6 @@ export class SettingsEffects { tap(userLang => updateUserLang(this.translate, userLang)) ), {dispatch: false}); - setTitle = createEffect(() => merge( this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)), this.router.events.pipe(filter(event => event instanceof ActivationEnd)) @@ -81,7 +76,6 @@ export class SettingsEffects { }) ), {dispatch: false}); - setPublicId = createEffect(() => merge( this.router.events.pipe(filter(event => event instanceof ActivationEnd)) ).pipe( diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts index dfe8b37519..e816bbffb0 100644 --- a/ui-ngx/src/app/core/settings/settings.utils.ts +++ b/ui-ngx/src/app/core/settings/settings.utils.ts @@ -18,7 +18,7 @@ import { environment as env } from '@env/environment'; import { TranslateService } from '@ngx-translate/core'; import * as _moment from 'moment'; -export function updateUserLang(translate: TranslateService, userLang: string) { +export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs) { let targetLang = userLang; if (!env.production) { console.log(`User lang: ${targetLang}`); @@ -29,7 +29,7 @@ export function updateUserLang(translate: TranslateService, userLang: string) { console.log(`Fallback to browser lang: ${targetLang}`); } } - const detectedSupportedLang = detectSupportedLang(targetLang); + const detectedSupportedLang = detectSupportedLang(targetLang, translations); if (!env.production) { console.log(`Detected supported lang: ${detectedSupportedLang}`); } @@ -37,10 +37,10 @@ export function updateUserLang(translate: TranslateService, userLang: string) { _moment.locale([detectedSupportedLang]); } -function detectSupportedLang(targetLang: string): string { +function detectSupportedLang(targetLang: string, translations: string[]): string { const langTag = (targetLang || '').split('-').join('_'); if (langTag.length) { - if (env.supportedLangs.indexOf(langTag) > -1) { + if (translations.indexOf(langTag) > -1) { return langTag; } else { const parts = langTag.split('_'); @@ -50,7 +50,7 @@ function detectSupportedLang(targetLang: string): string { } else { lang = langTag; } - const foundLangs = env.supportedLangs.filter( + const foundLangs = translations.filter( (supportedLang: string) => { const supportedLangParts = supportedLang.split('_'); return supportedLangParts[0] === lang; diff --git a/ui-ngx/src/app/core/translate/translate-default-loader.ts b/ui-ngx/src/app/core/translate/translate-default-loader.ts new file mode 100644 index 0000000000..a24b6a1d6d --- /dev/null +++ b/ui-ngx/src/app/core/translate/translate-default-loader.ts @@ -0,0 +1,14 @@ +import { TranslateLoader } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +export class TranslateDefaultLoader implements TranslateLoader { + + constructor(private http: HttpClient) { + + } + + getTranslation(lang: string): Observable { + return this.http.get(`/assets/locale/locale.constant-${lang}.json`); + } +} diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 999d05b3a8..dc9ed5004b 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -338,6 +338,8 @@ export const isEmpty = (a: any): boolean => _.isEmpty(a); export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path); +export const setByPath = (object: T, path: string | number | symbol, value: any): T => _.set(object, path, value); + export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { if (a === b) { return true; diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index c623d61b9a..8a9b9959c3 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -46,7 +46,7 @@ email - {{ 'user.invalid-email-format' | translate }} + {{ 'login.invalid-email-format' | translate }} diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 33b1936213..1a09d539c8 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -18,7 +18,7 @@
-
+
- - +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html index 9d6e9a4bd5..8ba255052d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html @@ -59,7 +59,7 @@
- {{ 'widgets.doughnut.auto-scale' | translate }} + {{ 'widgets.latest-chart.auto-scale' | translate }}
@@ -99,12 +99,12 @@
- {{ 'widgets.doughnut.clockwise-layout' | translate }} + {{ 'widgets.latest-chart.clockwise-layout' | translate }}
- {{ 'widgets.doughnut.sort-series' | translate }} + {{ 'widgets.latest-chart.sort-series' | translate }}
@@ -194,8 +194,8 @@
- - {{ doughnutTooltipValueTypeTranslationMap.get(type) | translate }} + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} @@ -229,6 +229,9 @@
+ +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts index 99e45d49a7..d2bff75354 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts @@ -31,7 +31,7 @@ import { } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils'; +import { formatValue, isDefinedAndNotNull, isUndefined, mergeDeep } from '@core/utils'; import { getTimewindowConfig, setTimewindowConfig @@ -42,12 +42,14 @@ import { doughnutLayoutImages, doughnutLayouts, doughnutLayoutTranslations, - DoughnutTooltipValueType, - doughnutTooltipValueTypes, - doughnutTooltipValueTypeTranslations, DoughnutWidgetSettings, horizontalDoughnutLayoutImages } from '@home/components/widget/lib/chart/doughnut-widget.models'; +import { + LatestChartTooltipValueType, + latestChartTooltipValueTypes, + latestChartTooltipValueTypeTranslations +} from '@home/components/widget/lib/chart/latest-chart.models'; @Component({ selector: 'tb-doughnut-basic-config', @@ -87,9 +89,9 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent { legendPositionTranslationMap = legendPositionTranslationMap; - doughnutTooltipValueTypes = doughnutTooltipValueTypes; + latestChartTooltipValueTypes = latestChartTooltipValueTypes; - doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations; + latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations; doughnutWidgetConfigForm: UntypedFormGroup; @@ -125,7 +127,8 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent { } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: DoughnutWidgetSettings = {...doughnutDefaultSettings(this.horizontal), ...(configData.config.settings || {})}; + const settings: DoughnutWidgetSettings = mergeDeep({} as DoughnutWidgetSettings, + doughnutDefaultSettings(this.horizontal), configData.config.settings as DoughnutWidgetSettings); this.doughnutWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], datasources: [configData.config.datasources, []], @@ -151,6 +154,8 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent { units: [configData.config.units, []], decimals: [configData.config.decimals, []], + animation: [settings.animation, []], + showLegend: [settings.showLegend, []], legendPosition: [settings.legendPosition, []], legendLabelFont: [settings.legendLabelFont, []], @@ -202,6 +207,8 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent { this.widgetConfig.config.units = config.units; this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.animation = config.animation; + this.widgetConfig.config.settings.showLegend = config.showLegend; this.widgetConfig.config.settings.legendPosition = config.legendPosition; this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; @@ -329,9 +336,9 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent { } private _tooltipValuePreviewFn(): string { - const tooltipValueType: DoughnutTooltipValueType = this.doughnutWidgetConfigForm.get('tooltipValueType').value; + const tooltipValueType: LatestChartTooltipValueType = this.doughnutWidgetConfigForm.get('tooltipValueType').value; const decimals: number = this.doughnutWidgetConfigForm.get('tooltipValueDecimals').value; - if (tooltipValueType === DoughnutTooltipValueType.percentage) { + if (tooltipValueType === LatestChartTooltipValueType.percentage) { return formatValue(35, decimals, '%', false); } else { const units: string = this.doughnutWidgetConfigForm.get('units').value; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.html new file mode 100644 index 0000000000..b0a5027944 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.html @@ -0,0 +1,276 @@ + + + + + + + + +
+
widget-config.card-appearance
+
+ + {{ 'widgets.pie-chart.label' | translate }} + +
+ + + + {{ pieChartLabelPositionTranslationMap.get(position) | translate }} + + + + + + + +
+
+
+
{{ 'widgets.pie-chart.border' | translate }}
+
+ + +
px
+
+ + +
+
+
+
{{ 'widgets.pie-chart.radius' | translate }}
+
+ + +
%
+
+
+
+
+ + {{ 'widget-config.card-title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.latest-chart.clockwise-layout' | translate }} + +
+
+ + {{ 'widgets.latest-chart.sort-series' | translate }} + +
+
+
widget-config.units-short
+ + +
+
+
widget-config.decimals-short
+ + + +
+
+
+ + + + + {{ 'widget-config.legend' | translate }} + + + + +
+
{{ 'legend.position' | translate }}
+ + + + {{ legendPositionTranslationMap.get(pos) | translate }} + + + +
+
+
{{ 'legend.label' | translate }}
+
+ + + + +
+
+
+
{{ 'legend.value' | translate }}
+
+ + + + +
+
+
+
+
+
+ + + + + {{ 'widget-config.tooltip' | translate }} + + + + +
+
{{ 'tooltip.value' | translate }}
+
+ + + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} + + + + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+
{{ 'tooltip.background-color' | translate }}
+ + +
+
+
{{ 'tooltip.background-blur' | translate }}
+ + +
px
+
+
+
+
+
+ + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts new file mode 100644 index 0000000000..7e0dc8d993 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts @@ -0,0 +1,343 @@ +/// +/// 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + DataKey, + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + legendPositions, + legendPositionTranslationMap, + WidgetConfig +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { formatValue, isUndefined, mergeDeep } from '@core/utils'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { + LatestChartTooltipValueType, + latestChartTooltipValueTypes, + latestChartTooltipValueTypeTranslations +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { + pieChartWidgetDefaultSettings, + PieChartWidgetSettings +} from '@home/components/widget/lib/chart/pie-chart-widget.models'; +import { + pieChartLabelPositions, + pieChartLabelPositionTranslations +} from '@home/components/widget/lib/chart/pie-chart.models'; + +@Component({ + selector: 'tb-pie-chart-basic-config', + templateUrl: './pie-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class PieChartBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.pieChartWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + public get displayTimewindowConfig(): boolean { + const datasources = this.pieChartWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.pieChartWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + pieChartLabelPositions = pieChartLabelPositions; + + pieChartLabelPositionTranslationMap = pieChartLabelPositionTranslations; + + legendPositions = legendPositions; + + legendPositionTranslationMap = legendPositionTranslationMap; + + latestChartTooltipValueTypes = latestChartTooltipValueTypes; + + latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations; + + pieChartWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.pieChartWidgetConfigForm; + } + + protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { + return [{ name: 'windPower', label: 'Wind', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#08872B' }, + { name: 'solarPower', label: 'Solar', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FF4D5A' }, + { name: 'hydroelectricPower', label: 'Hydroelectric', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FFDE30' }]; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: PieChartWidgetSettings = mergeDeep({} as PieChartWidgetSettings, + pieChartWidgetDefaultSettings, configData.config.settings as PieChartWidgetSettings); + this.pieChartWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + + series: [this.getSeries(configData.config.datasources), []], + + showLabel: [settings.showLabel, []], + labelPosition: [settings.labelPosition, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + borderWidth: [settings.borderWidth, [Validators.min(0)]], + borderColor: [settings.borderColor, []], + + radius: [settings.radius, [Validators.min(0), Validators.max(100)]], + + clockwise: [settings.clockwise, []], + sortSeries: [settings.sortSeries, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + showTitleIcon: [configData.config.showTitleIcon, []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], + + animation: [settings.animation, []], + + showLegend: [settings.showLegend, []], + legendPosition: [settings.legendPosition, []], + legendLabelFont: [settings.legendLabelFont, []], + legendLabelColor: [settings.legendLabelColor, []], + legendValueFont: [settings.legendValueFont, []], + legendValueColor: [settings.legendValueColor, []], + + showTooltip: [settings.showTooltip, []], + tooltipValueType: [settings.tooltipValueType, []], + tooltipValueDecimals: [settings.tooltipValueDecimals, []], + tooltipValueFont: [settings.tooltipValueFont, []], + tooltipValueColor: [settings.tooltipValueColor, []], + tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], + tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + this.setSeries(config.series, this.widgetConfig.config.datasources); + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.showLabel = config.showLabel; + this.widgetConfig.config.settings.labelPosition = config.labelPosition; + this.widgetConfig.config.settings.labelFont = config.labelFont; + this.widgetConfig.config.settings.labelColor = config.labelColor; + + this.widgetConfig.config.settings.borderWidth = config.borderWidth; + this.widgetConfig.config.settings.borderColor = config.borderColor; + + this.widgetConfig.config.settings.radius = config.radius; + + this.widgetConfig.config.settings.clockwise = config.clockwise; + this.widgetConfig.config.settings.sortSeries = config.sortSeries; + + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + + this.widgetConfig.config.settings.animation = config.animation; + + this.widgetConfig.config.settings.showLegend = config.showLegend; + this.widgetConfig.config.settings.legendPosition = config.legendPosition; + this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; + this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor; + this.widgetConfig.config.settings.legendValueFont = config.legendValueFont; + this.widgetConfig.config.settings.legendValueColor = config.legendValueColor; + + this.widgetConfig.config.settings.showTooltip = config.showTooltip; + this.widgetConfig.config.settings.tooltipValueType = config.tooltipValueType; + this.widgetConfig.config.settings.tooltipValueDecimals = config.tooltipValueDecimals; + this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont; + this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor; + this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; + this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showLabel', 'showTitle', 'showTitleIcon', 'showLegend', 'showTooltip']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showLabel: boolean = this.pieChartWidgetConfigForm.get('showLabel').value; + const showTitle: boolean = this.pieChartWidgetConfigForm.get('showTitle').value; + const showTitleIcon: boolean = this.pieChartWidgetConfigForm.get('showTitleIcon').value; + const showLegend: boolean = this.pieChartWidgetConfigForm.get('showLegend').value; + const showTooltip: boolean = this.pieChartWidgetConfigForm.get('showTooltip').value; + + if (showLabel) { + this.pieChartWidgetConfigForm.get('labelPosition').enable(); + this.pieChartWidgetConfigForm.get('labelFont').enable(); + this.pieChartWidgetConfigForm.get('labelColor').enable(); + } else { + this.pieChartWidgetConfigForm.get('labelPosition').disable(); + this.pieChartWidgetConfigForm.get('labelFont').disable(); + this.pieChartWidgetConfigForm.get('labelColor').disable(); + } + + if (showTitle) { + this.pieChartWidgetConfigForm.get('title').enable(); + this.pieChartWidgetConfigForm.get('titleFont').enable(); + this.pieChartWidgetConfigForm.get('titleColor').enable(); + this.pieChartWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.pieChartWidgetConfigForm.get('titleIcon').enable(); + this.pieChartWidgetConfigForm.get('iconColor').enable(); + } else { + this.pieChartWidgetConfigForm.get('titleIcon').disable(); + this.pieChartWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.pieChartWidgetConfigForm.get('title').disable(); + this.pieChartWidgetConfigForm.get('titleFont').disable(); + this.pieChartWidgetConfigForm.get('titleColor').disable(); + this.pieChartWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.pieChartWidgetConfigForm.get('titleIcon').disable(); + this.pieChartWidgetConfigForm.get('iconColor').disable(); + } + if (showLegend) { + this.pieChartWidgetConfigForm.get('legendPosition').enable(); + this.pieChartWidgetConfigForm.get('legendLabelFont').enable(); + this.pieChartWidgetConfigForm.get('legendLabelColor').enable(); + this.pieChartWidgetConfigForm.get('legendValueFont').enable(); + this.pieChartWidgetConfigForm.get('legendValueColor').enable(); + } else { + this.pieChartWidgetConfigForm.get('legendPosition').disable(); + this.pieChartWidgetConfigForm.get('legendLabelFont').disable(); + this.pieChartWidgetConfigForm.get('legendLabelColor').disable(); + this.pieChartWidgetConfigForm.get('legendValueFont').disable(); + this.pieChartWidgetConfigForm.get('legendValueColor').disable(); + } + if (showTooltip) { + this.pieChartWidgetConfigForm.get('tooltipValueType').enable(); + this.pieChartWidgetConfigForm.get('tooltipValueDecimals').enable(); + this.pieChartWidgetConfigForm.get('tooltipValueFont').enable(); + this.pieChartWidgetConfigForm.get('tooltipValueColor').enable(); + this.pieChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); + this.pieChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); + } else { + this.pieChartWidgetConfigForm.get('tooltipValueType').disable(); + this.pieChartWidgetConfigForm.get('tooltipValueDecimals').disable(); + this.pieChartWidgetConfigForm.get('tooltipValueFont').disable(); + this.pieChartWidgetConfigForm.get('tooltipValueColor').disable(); + this.pieChartWidgetConfigForm.get('tooltipBackgroundColor').disable(); + this.pieChartWidgetConfigForm.get('tooltipBackgroundBlur').disable(); + } + } + + private getSeries(datasources?: Datasource[]): DataKey[] { + if (datasources && datasources.length) { + return datasources[0].dataKeys || []; + } + return []; + } + + private setSeries(series: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].dataKeys = series; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _valuePreviewFn(): string { + const units: string = this.pieChartWidgetConfigForm.get('units').value; + const decimals: number = this.pieChartWidgetConfigForm.get('decimals').value; + return formatValue(110, decimals, units, false); + } + + private _tooltipValuePreviewFn(): string { + const tooltipValueType: LatestChartTooltipValueType = this.pieChartWidgetConfigForm.get('tooltipValueType').value; + const decimals: number = this.pieChartWidgetConfigForm.get('tooltipValueDecimals').value; + if (tooltipValueType === LatestChartTooltipValueType.percentage) { + return formatValue(35, decimals, '%', false); + } else { + const units: string = this.pieChartWidgetConfigForm.get('units').value; + return formatValue(110, decimals, units, false); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index e087ce7d91..913974c313 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -367,9 +367,9 @@
- - +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index fcee2a9dba..f8e72b5676 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -328,9 +328,9 @@
- - +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts index 3b6333bbc3..5351b219a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts @@ -23,15 +23,17 @@ import { textStyle } from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; -import { EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { + echartsAnimationDefaultSettings, + EChartsAnimationSettings, + EChartsTooltipWidgetSettings +} from '@home/components/widget/lib/chart/echarts-widget.models'; import { DeepPartial } from '@shared/models/common'; import { defaultTimeSeriesChartXAxisSettings, defaultTimeSeriesChartYAxisSettings, SeriesFillSettings, SeriesFillType, - timeSeriesChartAnimationDefaultSettings, - TimeSeriesChartAnimationSettings, timeSeriesChartGridDefaultSettings, TimeSeriesChartGridSettings, TimeSeriesChartKeySettings, @@ -63,7 +65,7 @@ export interface BarChartWithLabelsWidgetSettings extends EChartsTooltipWidgetSe grid: TimeSeriesChartGridSettings; yAxis: TimeSeriesChartYAxisSettings; xAxis: TimeSeriesChartXAxisSettings; - animation: TimeSeriesChartAnimationSettings; + animation: EChartsAnimationSettings; thresholds: TimeSeriesChartThreshold[]; showLegend: boolean; legendPosition: LegendPosition; @@ -116,8 +118,8 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, defaultTimeSeriesChartXAxisSettings, {showTicks: false, showSplitLines: false} as TimeSeriesChartXAxisSettings), - animation: mergeDeep({} as TimeSeriesChartAnimationSettings, - timeSeriesChartAnimationDefaultSettings), + animation: mergeDeep({} as EChartsAnimationSettings, + echartsAnimationDefaultSettings), thresholds: [], showLegend: true, legendPosition: LegendPosition.top, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html deleted file mode 100644 index 3465e80c15..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html +++ /dev/null @@ -1,37 +0,0 @@ - -
-
- -
-
-
-
-
-
-
-
{{ legendItem.label }}
-
-
{{ legendItem.value }}
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts index d215f37a32..4b2eca3728 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts @@ -14,85 +14,32 @@ /// limitations under the License. /// -import { - AfterViewInit, - ChangeDetectorRef, - Component, - ElementRef, - Input, - OnDestroy, - OnInit, - Renderer2, - TemplateRef, - ViewChild, - ViewEncapsulation -} from '@angular/core'; +import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; import { doughnutDefaultSettings, - DoughnutLayout, - DoughnutTooltipValueType, + doughnutPieChartSettings, DoughnutWidgetSettings } from '@home/components/widget/lib/chart/doughnut-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { - backgroundStyle, - ColorProcessor, - ComponentStyle, - overlayStyle, - textStyle -} from '@shared/models/widget-settings.models'; -import { ResizeObserver } from '@juggle/resize-observer'; import { WidgetComponent } from '@home/components/widget/widget.component'; -import * as echarts from 'echarts/core'; import { TranslateService } from '@ngx-translate/core'; -import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries'; -import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils'; -import { SVG, Svg, Text } from '@svgdotjs/svg.js'; -import { DataKey, LegendPosition } from '@shared/models/widget.models'; -import { Observable } from 'rxjs'; -import { ImagePipe } from '@shared/pipe/image.pipe'; -import { DomSanitizer } from '@angular/platform-browser'; -import { ECharts, echartsModule, EChartsOption } from '@home/components/widget/lib/chart/echarts-widget.models'; - -const shapeSize = 134; -const shapeSegmentWidth = 13.4; - -interface DoughnutDataItem { - id: number; - dataKey: DataKey; - value: number; - hasValue: boolean; - enabled: boolean; -} - -interface DoughnutLegendItem { - id: number; - color: string; - label: string; - value: string; - hasValue: boolean; - enabled: boolean; - total?: boolean; -} +import { isDefinedAndNotNull } from '@core/utils'; +import { TbPieChart } from '@home/components/widget/lib/chart/pie-chart'; +import { + LatestChartComponent, + LatestChartComponentCallbacks +} from '@home/components/widget/lib/chart/latest-chart.component'; @Component({ selector: 'tb-doughnut-widget', - templateUrl: './doughnut-widget.component.html', - styleUrls: ['./doughnut-widget.component.scss'], + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], encapsulation: ViewEncapsulation.None }) -export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit { +export class DoughnutWidgetComponent implements OnInit { - @ViewChild('doughnutContent', {static: false}) - doughnutContent: ElementRef; - - @ViewChild('doughnutShape', {static: false}) - doughnutShape: ElementRef; - - @ViewChild('doughnutLegend', {static: false}) - doughnutLegend: ElementRef; - - settings: DoughnutWidgetSettings; + @ViewChild('latestChart') + latestChart: LatestChartComponent; @Input() ctx: WidgetContext; @@ -100,45 +47,12 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit @Input() widgetTitlePanel: TemplateRef; - showLegend: boolean; - legendClass: string; - - totalValueColor: ColorProcessor; - - backgroundStyle$: Observable; - overlayStyle: ComponentStyle = {}; - - legendItems: DoughnutLegendItem[]; - legendLabelStyle: ComponentStyle; - legendValueStyle: ComponentStyle; - disabledLegendLabelStyle: ComponentStyle; - disabledLegendValueStyle: ComponentStyle; - - private shapeResize$: ResizeObserver; - private legendHorizontal: boolean; - - private decimals = 0; - private units = ''; - - private total = 0; - private totalText = 'N/A'; - private scale = 1; - - private dataItems: DoughnutDataItem[] = []; + settings: DoughnutWidgetSettings; - private drawDoughnutPending = false; - private showTotal = false; - private doughnutChart: ECharts; - private doughnutOptions: EChartsOption; - private svgShape: Svg; - private totalTextNode: Text; + callbacks: LatestChartComponentCallbacks; - constructor(private imagePipe: ImagePipe, - private sanitizer: DomSanitizer, - private widgetComponent: WidgetComponent, - private renderer: Renderer2, - private translate: TranslateService, - private cd: ChangeDetectorRef) { + constructor(private widgetComponent: WidgetComponent, + private translate: TranslateService) { } ngOnInit(): void { @@ -146,390 +60,19 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit const horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; this.ctx.$scope.doughnutWidget = this; this.settings = {...doughnutDefaultSettings(horizontal), ...this.ctx.settings}; - - this.decimals = this.ctx.decimals; - this.units = this.ctx.units; - - this.showLegend = this.settings.showLegend; - this.showTotal = this.settings.layout === DoughnutLayout.with_total; - - if (this.showTotal) { - this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor); - } - - this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); - this.overlayStyle = overlayStyle(this.settings.background.overlay); - - if (this.showLegend) { - this.legendItems = []; - this.legendClass = `legend-${this.settings.legendPosition}`; - this.legendHorizontal = [LegendPosition.left, LegendPosition.right].includes(this.settings.legendPosition); - this.legendLabelStyle = textStyle(this.settings.legendLabelFont); - this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); - this.legendLabelStyle.color = this.settings.legendLabelColor; - this.legendValueStyle = textStyle(this.settings.legendValueFont); - this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont); - this.legendValueStyle.color = this.settings.legendValueColor; - } - let counter = 0; - if (this.ctx.datasources.length) { - for (const datasource of this.ctx.datasources) { - const dataKeys = datasource.dataKeys; - for (const dataKey of dataKeys) { - const id = counter++; - this.dataItems.push({ - id, - dataKey, - value: 0, - hasValue: false, - enabled: true - }); - if (this.showLegend) { - this.legendItems.push( - { - id, - value: '--', - label: dataKey.label, - color: dataKey.color, - enabled: true, - hasValue: false - } - ); - } - } + this.callbacks = { + createChart: (chartShape, renderer) => { + const settings = doughnutPieChartSettings(this.settings); + return new TbPieChart(this.ctx, settings, chartShape.nativeElement, renderer, this.translate, true); } - } - if (this.settings.sortSeries) { - this.dataItems.sort((a, b) => a.dataKey.label.localeCompare(b.dataKey.label)); - if (this.showLegend) { - this.legendItems.sort((a, b) => a.label.localeCompare(b.label)); - } - } - if (this.showLegend && !this.showTotal) { - this.legendItems.push( - { - id: null, - value: '--', - label: this.translate.instant('widgets.doughnut.total'), - color: 'rgba(0, 0, 0, 0.06)', - enabled: true, - hasValue: false, - total: true - } - ); - } - } - - ngAfterViewInit() { - if (this.drawDoughnutPending) { - this.drawDoughnut(); - } - } - - ngOnDestroy() { - if (this.shapeResize$) { - this.shapeResize$.disconnect(); - } - if (this.doughnutChart) { - this.doughnutChart.dispose(); - } + }; } public onInit() { - const borderRadius = this.ctx.$widgetElement.css('borderRadius'); - this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; - if (this.doughnutShape) { - this.drawDoughnut(); - } else { - this.drawDoughnutPending = true; - } - this.cd.detectChanges(); + this.latestChart?.onInit(); } public onDataUpdated() { - for (const dsData of this.ctx.data) { - let value = 0; - const tsValue = dsData.data[0]; - const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey); - if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) { - value = tsValue[1]; - dataItem.hasValue = true; - dataItem.value = Number(value); - } else { - dataItem.hasValue = false; - dataItem.value = 0; - } - } - this.updateSeriesData(); - if (this.showLegend) { - this.cd.detectChanges(); - if (this.legendHorizontal) { - setTimeout(() => { - this.onResize(); - }); - } - } - } - - private updateSeriesData(renderTotal = true) { - this.total = 0; - this.totalText = 'N/A'; - let hasValue = false; - const seriesData: PieDataItemOption[] = []; - const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue); - for (const dataItem of this.dataItems) { - if (dataItem.enabled && dataItem.hasValue) { - hasValue = true; - this.total += dataItem.value; - seriesData.push( - {id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}} - ); - if (enabledDataItems.length > 1) { - seriesData.push({ - value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true} - }); - } - } - if (this.showLegend) { - const legendItem = this.legendItems.find(item => item.id === dataItem.id); - if (dataItem.hasValue) { - legendItem.hasValue = true; - legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false); - } else { - legendItem.hasValue = false; - legendItem.value = '--'; - } - } - } - for (let i= 1; i < seriesData.length; i+=2) { - seriesData[i].value = this.total / 100; - } - if (this.showTotal || this.showLegend) { - if (hasValue) { - this.totalText = formatValue(this.total, this.decimals, this.units, false); - if (this.showLegend && !this.showTotal) { - this.legendItems[this.legendItems.length - 1].hasValue = true; - this.legendItems[this.legendItems.length - 1].value = this.totalText; - } - } else if (this.showLegend && !this.showTotal) { - this.legendItems[this.legendItems.length - 1].hasValue = false; - this.legendItems[this.legendItems.length - 1].value = '--'; - } - } - this.doughnutOptions.series[0].data = seriesData; - this.doughnutChart.setOption(this.doughnutOptions); - if (this.showTotal) { - this.totalValueColor.update(this.total); - if (renderTotal) { - this.renderTotal(); - } - } - } - - public onLegendItemEnter(item: DoughnutLegendItem) { - if (!item.total && item.enabled && item.hasValue) { - const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id); - if (dataIndex > -1) { - this.doughnutChart.dispatchAction({ - type: 'highlight', - dataIndex - }); - } - } - } - - public onLegendItemLeave(item: DoughnutLegendItem) { - if (!item.total && item.enabled && item.hasValue) { - const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id); - if (dataIndex > -1) { - this.doughnutChart.dispatchAction({ - type: 'downplay', - dataIndex - }); - } - } + this.latestChart?.onDataUpdated(); } - - public toggleLegendItem(item: DoughnutLegendItem) { - if (!item.total && item.hasValue) { - const enable = !item.enabled; - const dataItem = this.dataItems.find(d => d.id === item.id); - if (dataItem) { - dataItem.enabled = enable; - this.updateSeriesData(); - item.enabled = enable; - if (enable) { - const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id); - if (dataIndex > -1) { - this.doughnutChart.dispatchAction({ - type: 'highlight', - dataIndex - }); - } - } - } - } - } - - private drawDoughnut() { - echartsModule.init(); - const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width; - const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height; - const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight); - const innerRadius = size / 2 - shapeSegmentWidth; - const outerRadius = size / 2; - this.doughnutChart = echarts.init(this.doughnutShape.nativeElement, null, { - renderer: 'svg', - width: this.settings.autoScale ? shapeSize : undefined, - height: this.settings.autoScale ? shapeSize : undefined, - }); - this.doughnutOptions = { - tooltip: { - trigger: this.settings.showTooltip ? 'item' : 'none', - confine: false, - appendTo: 'body', - }, - series: [ - { - type: 'pie', - clockwise: this.settings.clockwise, - radius: [innerRadius, outerRadius], - avoidLabelOverlap: false, - itemStyle: { - borderRadius: '50%', - borderWidth: 0, - borderColor: '#fff' - }, - label: { - show: false - }, - emphasis: { - scale: false, - itemStyle: { - borderColor: '#fff', - borderWidth: 2, - shadowColor: 'rgba(0, 0, 0, 0.24)', - shadowBlur: 8 - }, - label: { - show: false - } - } - } - ] - }; - if (this.settings.showTooltip) { - this.doughnutOptions.series[0].tooltip = { - formatter: (params) => { - if (!params.name) { - return null; - } - let value: string; - if (this.settings.tooltipValueType === DoughnutTooltipValueType.percentage) { - const percents = params.value / this.total * 100; - value = formatValue(percents, this.settings.tooltipValueDecimals, '%', false); - } else { - value = formatValue(params.value, this.settings.tooltipValueDecimals, this.units, false); - } - const textElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setStyle(textElement, 'display', 'inline-flex'); - this.renderer.setStyle(textElement, 'align-items', 'center'); - this.renderer.setStyle(textElement, 'gap', '8px'); - const labelElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.appendChild(labelElement, this.renderer.createText(params.name)); - this.renderer.setStyle(labelElement, 'font-family', 'Roboto'); - this.renderer.setStyle(labelElement, 'font-size', '11px'); - this.renderer.setStyle(labelElement, 'font-style', 'normal'); - this.renderer.setStyle(labelElement, 'font-weight', '400'); - this.renderer.setStyle(labelElement, 'line-height', '16px'); - this.renderer.setStyle(labelElement, 'letter-spacing', '0.25px'); - this.renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)'); - const valueElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.appendChild(valueElement, this.renderer.createText(value)); - this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); - this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); - this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); - this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight); - this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); - this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); - this.renderer.appendChild(textElement, labelElement); - this.renderer.appendChild(textElement, valueElement); - return textElement; - }, - padding: [4, 8], - backgroundColor: this.settings.tooltipBackgroundColor, - extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` - }; - this.doughnutOptions.series[0].tooltip.position = (pos) => [pos[0] + 10, pos[1] + 10]; - } - this.updateSeriesData(false); - - this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'overflow', 'visible'); - if (this.settings.autoScale) { - this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'position', 'absolute'); - } - this.renderer.setStyle(this.doughnutChart.getDom().firstChild.firstChild, 'overflow', 'visible'); - - this.svgShape = SVG(this.doughnutChart.getDom().firstChild.firstChild).toRoot(); - - if (this.showTotal) { - this.totalTextNode = this.svgShape.text('').font({ - family: 'Roboto', - leading: 1 - }).attr({'text-anchor': 'middle'}); - this.renderTotal(); - } - - this.shapeResize$ = new ResizeObserver(() => { - this.onResize(); - }); - this.shapeResize$.observe(this.doughnutContent.nativeElement); - this.onResize(); - } - - private renderTotal() { - this.totalTextNode.text(add => { - add.tspan(this.translate.instant('widgets.doughnut.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)'); - add.tspan('').newLine().font({size: '4px'}); - add.tspan(this.totalText).newLine().font( - {family: this.settings.totalValueFont.family, - size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit, - weight: this.settings.totalValueFont.weight, - style: this.settings.totalValueFont.style} - ).fill(this.totalValueColor.color); - }).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2); - } - - private onResize() { - if (this.legendHorizontal) { - this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', null); - this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', null); - this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', null); - } - const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width; - const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height; - const size = Math.min(shapeWidth, shapeHeight); - if (this.legendHorizontal) { - this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', `${size}px`); - this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', `${size}px`); - this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', '1'); - } - if (!this.settings.autoScale) { - const innerRadius = size / 2 - shapeSegmentWidth; - const outerRadius = size / 2; - this.doughnutOptions.series[0].radius = [innerRadius, outerRadius]; - this.doughnutChart.setOption(this.doughnutOptions); - } else { - this.scale = size / shapeSize; - this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'transform', `scale(${this.scale})`); - } - if (!this.settings.autoScale) { - this.doughnutChart.resize(); - } - if (this.showTotal) { - this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2, - (this.settings.autoScale ? shapeSize : shapeHeight) / 2); - } - } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts index 9110cfcd4d..d059e0186c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts @@ -14,14 +14,16 @@ /// limitations under the License. /// -import { - BackgroundSettings, - BackgroundType, - ColorSettings, - constantColor, - Font -} from '@shared/models/widget-settings.models'; +import { BackgroundType, ColorSettings, constantColor, Font } from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; +import { pieChartAnimationDefaultSettings, PieChartSettings } from '@home/components/widget/lib/chart/pie-chart.models'; +import { DeepPartial } from '@shared/models/common'; +import { + LatestChartTooltipValueType, + LatestChartWidgetSettings +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { mergeDeep } from '@core/utils'; +import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; export enum DoughnutLayout { default = 'default', @@ -51,41 +53,11 @@ export const horizontalDoughnutLayoutImages = new Map( ] ); -export enum DoughnutTooltipValueType { - absolute = 'absolute', - percentage = 'percentage' -} - -export const doughnutTooltipValueTypes = Object.keys(DoughnutTooltipValueType) as DoughnutTooltipValueType[]; - -export const doughnutTooltipValueTypeTranslations = new Map( - [ - [DoughnutTooltipValueType.absolute, 'widgets.doughnut.tooltip-value-type-absolute'], - [DoughnutTooltipValueType.percentage, 'widgets.doughnut.tooltip-value-type-percentage'] - ] -); - -export interface DoughnutWidgetSettings { +export interface DoughnutWidgetSettings extends LatestChartWidgetSettings { layout: DoughnutLayout; - autoScale: boolean; clockwise: boolean; - sortSeries: boolean; totalValueFont: Font; totalValueColor: ColorSettings; - showLegend: boolean; - legendPosition: LegendPosition; - legendLabelFont: Font; - legendLabelColor: string; - legendValueFont: Font; - legendValueColor: string; - showTooltip: boolean; - tooltipValueType: DoughnutTooltipValueType; - tooltipValueDecimals: number; - tooltipValueFont: Font; - tooltipValueColor: string; - tooltipBackgroundColor: string; - tooltipBackgroundBlur: number; - background: BackgroundSettings; } export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSettings => ({ @@ -102,6 +74,8 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett lineHeight: '1' }, totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'), + animation: mergeDeep({} as EChartsAnimationSettings, + pieChartAnimationDefaultSettings), showLegend: true, legendPosition: horizontal ? LegendPosition.right : LegendPosition.bottom, legendLabelFont: { @@ -123,7 +97,7 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett }, legendValueColor: 'rgba(0, 0, 0, 0.87)', showTooltip: true, - tooltipValueType: DoughnutTooltipValueType.percentage, + tooltipValueType: LatestChartTooltipValueType.percentage, tooltipValueDecimals: 0, tooltipValueFont: { family: 'Roboto', @@ -146,3 +120,31 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett } } }); + +export const doughnutPieChartSettings = (settings: DoughnutWidgetSettings): DeepPartial => ({ + autoScale: settings.autoScale, + doughnut: true, + clockwise: settings.clockwise, + sortSeries: settings.sortSeries, + showTotal: settings.layout === DoughnutLayout.with_total, + animation: settings.animation, + showLegend: settings.showLegend, + totalValueFont: settings.totalValueFont, + totalValueColor: settings.totalValueColor, + showLabel: false, + borderWidth: 0, + borderColor: '#fff', + borderRadius: '50%', + emphasisScale: false, + emphasisBorderWidth: 2, + emphasisBorderColor: '#fff', + emphasisShadowColor: 'rgba(0, 0, 0, 0.24)', + emphasisShadowBlur: 8, + showTooltip: settings.showTooltip, + tooltipValueType: settings.tooltipValueType, + tooltipValueDecimals: settings.tooltipValueDecimals, + tooltipValueFont: settings.tooltipValueFont, + tooltipValueColor: settings.tooltipValueColor, + tooltipBackgroundColor: settings.tooltipBackgroundColor, + tooltipBackgroundBlur: settings.tooltipBackgroundBlur +}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 4d188e0a74..a225973b82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -162,6 +162,63 @@ export const timeSeriesChartShapeOffsetFunctions = new Map { let interval: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html new file mode 100644 index 0000000000..436687b23a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html @@ -0,0 +1,24 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html new file mode 100644 index 0000000000..bd85f19d81 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html @@ -0,0 +1,37 @@ + +
+
+ +
+
+
+
+
+
+
+
{{ legendItem.label }}
+
+
{{ legendItem.value }}
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss similarity index 82% rename from ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss index 3ed90e1c2c..77b4f0fc08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.tb-doughnut-panel { +.tb-latest-chart-panel { width: 100%; height: 100%; position: relative; @@ -21,10 +21,10 @@ flex-direction: column; gap: 16px; padding: 20px 24px 24px 24px; - > div:not(.tb-doughnut-overlay) { + > div:not(.tb-latest-chart-overlay) { z-index: 1; } - .tb-doughnut-overlay { + .tb-latest-chart-overlay { position: absolute; top: 12px; left: 12px; @@ -34,7 +34,7 @@ div.tb-widget-title { padding: 0; } - .tb-doughnut-content { + .tb-latest-chart-content { flex: 1; min-width: 0; min-height: 0; @@ -50,22 +50,22 @@ &.legend-left { flex-direction: row-reverse; } - .tb-doughnut-shape { + .tb-latest-chart-shape { flex: 1; - min-width: 0; - min-height: 0; + min-width: 10px; + min-height: 10px; display: flex; align-items: center; justify-content: center; } - .tb-doughnut-legend { + .tb-latest-chart-legend { display: flex; justify-content: space-around; align-items: center; align-self: stretch; flex-wrap: wrap; gap: 8px; - .tb-doughnut-legend-item { + .tb-latest-chart-legend-item { display: flex; flex-direction: column; justify-content: center; @@ -74,19 +74,19 @@ &.pointer { cursor: pointer; } - .tb-doughnut-legend-item-label { + .tb-latest-chart-legend-item-label { display: flex; align-items: center; gap: 4px; color: #ccc; - .tb-doughnut-legend-item-label-circle { + .tb-latest-chart-legend-item-label-circle { width: 8px; height: 8px; border-radius: 50%; background-color: #ccc; } } - .tb-doughnut-legend-item-value { + .tb-latest-chart-legend-item-value { padding-left: 12px; color: #ccc; } @@ -94,11 +94,11 @@ } &.legend-right, &.legend-left { gap: 24px; - .tb-doughnut-legend { + .tb-latest-chart-legend { flex-direction: column; justify-content: center; align-items: stretch; - .tb-doughnut-legend-item { + .tb-latest-chart-legend-item { flex-direction: row; justify-content: space-between; align-items: center; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts new file mode 100644 index 0000000000..21599dc94b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts @@ -0,0 +1,200 @@ +/// +/// 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, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + LatestChartDataItem, + LatestChartLegendItem, + LatestChartSettings, + LatestChartWidgetSettings +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; +import { TbLatestChart } from '@home/components/widget/lib/chart/latest-chart'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { TranslateService } from '@ngx-translate/core'; +import { LegendPosition } from '@shared/models/widget.models'; + +export interface LatestChartComponentCallbacks { + createChart: (chartShape: ElementRef, renderer: Renderer2) => TbLatestChart; + onItemClick?: ($event: Event, item: LatestChartDataItem) => void; +} + +@Component({ + selector: 'tb-latest-chart', + templateUrl: './latest-chart.component.html', + styleUrls: ['./latest-chart.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('chartContent', {static: false}) + chartContent: ElementRef; + + @ViewChild('chartShape', {static: false}) + chartShape: ElementRef; + + @ViewChild('chartLegend', {static: false}) + chartLegend: ElementRef; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + @Input() + callbacks: LatestChartComponentCallbacks; + + @Input() + settings: LatestChartWidgetSettings; + + showLegend: boolean; + legendClass: string; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + get legendItems(): LatestChartLegendItem[] { + return this.latestChart ? this.latestChart.getLegendItems() : []; + } + + legendLabelStyle: ComponentStyle; + legendValueStyle: ComponentStyle; + disabledLegendLabelStyle: ComponentStyle; + disabledLegendValueStyle: ComponentStyle; + + private shapeResize$: ResizeObserver; + private legendHorizontal: boolean; + + private latestChart: TbLatestChart; + + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private widgetComponent: WidgetComponent, + private renderer: Renderer2, + private translate: TranslateService, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.showLegend = this.settings.showLegend; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + if (this.showLegend) { + this.legendClass = `legend-${this.settings.legendPosition}`; + this.legendHorizontal = [LegendPosition.left, LegendPosition.right].includes(this.settings.legendPosition); + this.legendLabelStyle = textStyle(this.settings.legendLabelFont); + this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); + this.legendLabelStyle.color = this.settings.legendLabelColor; + this.legendValueStyle = textStyle(this.settings.legendValueFont); + this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont); + this.legendValueStyle.color = this.settings.legendValueColor; + } + } + + ngAfterViewInit() { + this.latestChart = this.callbacks.createChart(this.chartShape, this.renderer); + this.latestChart.onItemClick(this.callbacks.onItemClick); + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.chartContent.nativeElement); + this.onResize(); + } + + ngOnDestroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + if (this.latestChart) { + this.latestChart.destroy(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onDataUpdated() { + if (this.latestChart) { + this.latestChart.update(); + } + if (this.showLegend) { + this.cd.detectChanges(); + if (this.legendHorizontal) { + setTimeout(() => { + this.onResize(); + }); + } + } + } + + public onLegendItemEnter(item: LatestChartLegendItem) { + if (!item.total && item.hasValue) { + this.latestChart.keyEnter(item.dataKey); + } + } + + public onLegendItemLeave(item: LatestChartLegendItem) { + if (!item.total && item.hasValue) { + this.latestChart.keyLeave(item.dataKey); + } + } + + public toggleLegendItem(item: LatestChartLegendItem) { + if (!item.total && item.hasValue) { + this.latestChart.toggleKey(item.dataKey); + } + } + + private onResize() { + if (this.legendHorizontal) { + this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', null); + this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null); + this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', null); + } + const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width; + const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height; + const size = Math.min(shapeWidth, shapeHeight); + if (this.legendHorizontal) { + this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', `${size}px`); + this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`); + this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', '1'); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts new file mode 100644 index 0000000000..c99cd5e773 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts @@ -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 { DataKey, Datasource, LegendPosition } from '@shared/models/widget.models'; +import { BackgroundSettings, Font } from '@shared/models/widget-settings.models'; +import { Renderer2 } from '@angular/core'; +import { CallbackDataParams } from 'echarts/types/dist/shared'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; +import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; + +export interface LatestChartDataItem { + id: number; + datasource: Datasource; + dataKey: DataKey; + value: number; + hasValue: boolean; + enabled: boolean; +} + +export interface LatestChartLegendItem { + dataKey?: DataKey; + color: string; + label: string; + value: string; + hasValue: boolean; + total?: boolean; +} + +export enum LatestChartTooltipValueType { + absolute = 'absolute', + percentage = 'percentage' +} + +export const latestChartTooltipValueTypes = Object.keys(LatestChartTooltipValueType) as LatestChartTooltipValueType[]; + +export const latestChartTooltipValueTypeTranslations = new Map( + [ + [LatestChartTooltipValueType.absolute, 'widgets.latest-chart.tooltip-value-type-absolute'], + [LatestChartTooltipValueType.percentage, 'widgets.latest-chart.tooltip-value-type-percentage'] + ] +); + +export interface LatestChartTooltipSettings { + showTooltip: boolean; + tooltipValueType: LatestChartTooltipValueType; + tooltipValueDecimals: number; + tooltipValueFont: Font; + tooltipValueColor: string; + tooltipBackgroundColor: string; + tooltipBackgroundBlur: number; +} + +export interface LatestChartSettings extends LatestChartTooltipSettings { + autoScale?: boolean; + sortSeries: boolean; + showTotal?: boolean; + showLegend: boolean; + animation: EChartsAnimationSettings; +} + +export interface LatestChartWidgetSettings extends LatestChartSettings { + legendPosition: LegendPosition; + legendLabelFont: Font; + legendLabelColor: string; + legendValueFont: Font; + legendValueColor: string; + background: BackgroundSettings; +} + +export const latestChartTooltipFormatter = (renderer: Renderer2, + settings: LatestChartTooltipSettings, + params: CallbackDataParams, + units: string, + total: number): null | HTMLElement => { + if (!params.name) { + return null; + } + let value: string; + if (settings.tooltipValueType === LatestChartTooltipValueType.percentage) { + const percents = isDefinedAndNotNull(params.percent) ? params.percent : (params.value as number) / total * 100; + value = formatValue(percents, settings.tooltipValueDecimals, '%', false); + } else { + value = formatValue(params.value, settings.tooltipValueDecimals, units, false); + } + const textElement: HTMLElement = renderer.createElement('div'); + renderer.setStyle(textElement, 'display', 'inline-flex'); + renderer.setStyle(textElement, 'align-items', 'center'); + renderer.setStyle(textElement, 'gap', '8px'); + const labelElement: HTMLElement = renderer.createElement('div'); + renderer.appendChild(labelElement, renderer.createText(params.name)); + renderer.setStyle(labelElement, 'font-family', 'Roboto'); + renderer.setStyle(labelElement, 'font-size', '11px'); + renderer.setStyle(labelElement, 'font-style', 'normal'); + renderer.setStyle(labelElement, 'font-weight', '400'); + renderer.setStyle(labelElement, 'line-height', '16px'); + renderer.setStyle(labelElement, 'letter-spacing', '0.25px'); + renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)'); + const valueElement: HTMLElement = renderer.createElement('div'); + renderer.appendChild(valueElement, renderer.createText(value)); + renderer.setStyle(valueElement, 'font-family', settings.tooltipValueFont.family); + renderer.setStyle(valueElement, 'font-size', settings.tooltipValueFont.size + settings.tooltipValueFont.sizeUnit); + renderer.setStyle(valueElement, 'font-style', settings.tooltipValueFont.style); + renderer.setStyle(valueElement, 'font-weight', settings.tooltipValueFont.weight); + renderer.setStyle(valueElement, 'line-height', settings.tooltipValueFont.lineHeight); + renderer.setStyle(valueElement, 'color', settings.tooltipValueColor); + renderer.appendChild(textElement, labelElement); + renderer.appendChild(textElement, valueElement); + return textElement; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts new file mode 100644 index 0000000000..ddb6e47560 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -0,0 +1,374 @@ +/// +/// 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 { ResizeObserver } from '@juggle/resize-observer'; +import { ECharts, echartsModule, EChartsOption } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { + LatestChartDataItem, + LatestChartLegendItem, + LatestChartSettings, + latestChartTooltipFormatter +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DeepPartial } from '@shared/models/common'; +import { Renderer2 } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { formatValue, isDefinedAndNotNull, isNumeric, mergeDeep } from '@core/utils'; +import { DataKey } from '@shared/models/widget.models'; +import * as echarts from 'echarts/core'; +import { CallbackDataParams } from 'echarts/types/dist/shared'; +import { SVG, Svg } from '@svgdotjs/svg.js'; + +export abstract class TbLatestChart { + + private readonly shapeResize$: ResizeObserver; + + protected readonly settings: S; + + protected readonly decimals: number; + protected readonly units: string; + + protected total = 0; + protected totalText = 'N/A'; + + protected latestChart: ECharts; + protected latestChartOption: EChartsOption; + + protected svgShape: Svg; + + protected dataItems: LatestChartDataItem[] = []; + private legendItems: LatestChartLegendItem[] = []; + + private itemClick: ($event: Event, item: LatestChartDataItem) => void; + + protected constructor(private ctx: WidgetContext, + private readonly inputSettings: DeepPartial, + protected chartElement: HTMLElement, + private renderer: Renderer2, + protected translate: TranslateService, + private autoResize = true) { + this.settings = mergeDeep({} as S, + this.defaultSettings(), + this.inputSettings as S); + + this.decimals = this.ctx.decimals; + this.units = this.ctx.units; + + this.initSettings(); + + this.setupData(); + + this.onResize(); + if (this.autoResize) { + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.chartElement); + } + } + + private setupData(): void { + let counter = 0; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + const dataKeys = datasource.dataKeys; + for (const dataKey of dataKeys) { + const id = counter++; + this.dataItems.push({ + id, + datasource, + dataKey, + value: 0, + hasValue: false, + enabled: true + }); + if (this.settings.showLegend) { + this.legendItems.push( + { + dataKey, + value: '--', + label: dataKey.label, + color: dataKey.color, + hasValue: false + } + ); + } + } + } + } + if (this.settings.sortSeries) { + this.dataItems.sort((a, b) => a.dataKey.label.localeCompare(b.dataKey.label)); + if (this.settings.showLegend) { + this.legendItems.sort((a, b) => a.label.localeCompare(b.label)); + } + } + if (this.settings.showLegend && !this.settings.showTotal) { + this.legendItems.push( + { + value: '--', + label: this.translate.instant('widgets.latest-chart.total'), + color: 'rgba(0, 0, 0, 0.06)', + hasValue: false, + total: true + } + ); + } + } + + public getLegendItems(): LatestChartLegendItem[] { + return this.legendItems; + } + + public update(): void { + for (const dsData of this.ctx.data) { + let value = 0; + const tsValue = dsData.data[0]; + const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey); + if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) { + value = tsValue[1]; + dataItem.hasValue = true; + dataItem.value = Number(value); + } else { + dataItem.hasValue = false; + dataItem.value = 0; + } + } + this.updateSeriesData(); + } + + public keyEnter(dataKey: DataKey): void { + const item = this.dataItems.find(d => d.dataKey === dataKey); + if (item) { + const dataIndex = this.latestChartOption.series[0].data.findIndex( + (d: any) => d.id === item.id); + if (dataIndex > -1) { + this.latestChart.dispatchAction({ + type: 'highlight', + dataIndex + }); + } + } + } + + public keyLeave(dataKey: DataKey): void { + const item = this.dataItems.find(d => d.dataKey === dataKey); + if (item) { + const dataIndex = this.latestChartOption.series[0].data.findIndex( + (d: any) => d.id === item.id); + if (dataIndex > -1) { + this.latestChart.dispatchAction({ + type: 'downplay', + dataIndex + }); + } + } + } + + public toggleKey(dataKey: DataKey): void { + const enable = dataKey.hidden; + const dataItem = this.dataItems.find(d => d.dataKey === dataKey); + if (dataItem) { + let dataIndex = this.latestChartOption.series[0].data.findIndex( + (d: any) => d.id === dataItem.id); + dataItem.enabled = enable; + if (!enable && dataIndex > -1) { + this.latestChart.dispatchAction({ + type: 'downplay', + dataIndex + }); + } + this.updateSeriesData(); + dataKey.hidden = !enable; + if (enable) { + dataIndex = this.latestChartOption.series[0].data.findIndex( + (d: any) => d.id === dataItem.id); + if (dataIndex > -1) { + this.latestChart.dispatchAction({ + type: 'highlight', + dataIndex + }); + } + } + } + } + + public destroy(): void { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + if (this.latestChart) { + this.latestChart.dispose(); + } + } + + public resize(): void { + this.onResize(); + } + + public onItemClick(itemClick: ($event: Event, item: LatestChartDataItem) => void) { + this.itemClick = itemClick; + } + + private updateSeriesData(initial = false) { + this.total = 0; + this.totalText = 'N/A'; + let hasValue = false; + for (const dataItem of this.dataItems) { + if (dataItem.enabled && dataItem.hasValue) { + hasValue = true; + this.total += dataItem.value; + } + if (this.settings.showLegend) { + const legendItem = this.legendItems.find(item => item.dataKey === dataItem.dataKey); + if (dataItem.hasValue) { + legendItem.hasValue = true; + legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false); + } else { + legendItem.hasValue = false; + legendItem.value = '--'; + } + } + } + if (this.settings.showTotal || this.settings.showLegend) { + if (hasValue) { + this.totalText = formatValue(this.total, this.decimals, this.units, false); + if (this.settings.showLegend && !this.settings.showTotal) { + this.legendItems[this.legendItems.length - 1].hasValue = true; + this.legendItems[this.legendItems.length - 1].value = this.totalText; + } + } else if (this.settings.showLegend && !this.settings.showTotal) { + this.legendItems[this.legendItems.length - 1].hasValue = false; + this.legendItems[this.legendItems.length - 1].value = '--'; + } + } + this.doUpdateSeriesData(); + this.latestChart.setOption(this.latestChartOption); + this.afterUpdateSeriesData(initial); + } + + private drawChart() { + echartsModule.init(); + this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); + this.latestChart = echarts.init(this.chartElement, null, { + renderer: 'svg', + width: this.settings.autoScale ? this.initialShapeWidth() : undefined, + height: this.settings.autoScale ? this.initialShapeHeight() : undefined, + }); + this.latestChartOption = { + tooltip: { + trigger: this.settings.showTooltip ? 'item' : 'none', + confine: false, + appendTo: 'body', + formatter: (params: CallbackDataParams) => + this.settings.showTooltip + ? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total) + : undefined, + padding: [4, 8], + backgroundColor: this.settings.tooltipBackgroundColor, + extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);`, + position: (pos) => [pos[0] + 10, pos[1] + 10] + }, + animation: this.settings.animation.animation, + animationThreshold: this.settings.animation.animationThreshold, + animationDuration: this.settings.animation.animationDuration, + animationEasing: this.settings.animation.animationEasing, + animationDelay: this.settings.animation.animationDelay, + animationDurationUpdate: this.settings.animation.animationDurationUpdate, + animationEasingUpdate: this.settings.animation.animationEasingUpdate, + animationDelayUpdate: this.settings.animation.animationDelayUpdate + }; + this.prepareLatestChartOption(); + this.updateSeriesData(true); + this.renderer.setStyle(this.latestChart.getDom().firstChild, 'overflow', 'visible'); + if (this.settings.autoScale) { + this.renderer.setStyle(this.latestChart.getDom().firstChild, 'position', 'absolute'); + } + this.renderer.setStyle(this.latestChart.getDom().firstChild.firstChild, 'overflow', 'visible'); + this.svgShape = SVG(this.latestChart.getDom().firstChild.firstChild).toRoot(); + this.afterDrawChart(); + this.latestChart.on('click', (params) => { + this.onChartClick(params); + }); + } + + protected onChartClick(params: echarts.ECElementEvent) { + if (params.componentType === 'series') { + if (params.data) { + const data = params.data as any; + if (isDefinedAndNotNull(data?.id)) { + const item = this.dataItems.find(d => d.id === data.id); + if (item && this.itemClick) { + this.itemClick(params.event.event, item); + } + } + } + } + }; + + private onResize() { + const shapeWidth = this.chartElement.offsetWidth; + const shapeHeight = this.chartElement.offsetHeight; + if (shapeWidth && shapeHeight) { + if (!this.latestChart) { + this.drawChart(); + } else { + const width = this.latestChart.getWidth(); + const height = this.latestChart.getHeight(); + if (width !== shapeWidth || height !== shapeHeight) { + this.beforeResize(shapeWidth, shapeHeight); + if (!this.settings.autoScale) { + this.latestChart.resize(); + } else { + let scale: number; + if (shapeWidth < shapeHeight) { + scale = shapeWidth / this.initialShapeWidth(); + } else { + scale = shapeHeight / this.initialShapeHeight(); + } + this.renderer.setStyle(this.latestChart.getDom().firstChild, 'transform', `scale(${scale})`); + } + this.afterResize(shapeWidth, shapeHeight); + } + } + } + } + + protected initSettings() { + } + + protected initialShapeWidth(): number { + return 100; + } + + protected initialShapeHeight(): number { + return 100; + } + + protected beforeResize(_shapeWidth: number, _shapeHeight: number) {}; + + protected afterResize(_shapeWidth: number, _shapeHeight: number) {}; + + protected afterDrawChart() {}; + + protected afterUpdateSeriesData(_initial: boolean) {}; + + protected abstract defaultSettings(): S; + + protected abstract prepareLatestChartOption(): void; + + protected abstract doUpdateSeriesData(): void; + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts new file mode 100644 index 0000000000..41f36a43a5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts @@ -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. +/// + +import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { TranslateService } from '@ngx-translate/core'; +import { TbPieChart } from '@home/components/widget/lib/chart/pie-chart'; +import { + LatestChartComponent, + LatestChartComponentCallbacks +} from '@home/components/widget/lib/chart/latest-chart.component'; +import { + pieChartWidgetDefaultSettings, + pieChartWidgetPieChartSettings, + PieChartWidgetSettings +} from '@home/components/widget/lib/chart/pie-chart-widget.models'; + +@Component({ + selector: 'tb-pie-chart-widget', + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], + encapsulation: ViewEncapsulation.None +}) +export class PieChartWidgetComponent implements OnInit { + + @ViewChild('latestChart') + latestChart: LatestChartComponent; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + settings: PieChartWidgetSettings; + + callbacks: LatestChartComponentCallbacks; + + constructor(private widgetComponent: WidgetComponent, + private translate: TranslateService) { + } + + ngOnInit(): void { + this.ctx.$scope.pieChartWidget = this; + this.settings = {...pieChartWidgetDefaultSettings, ...this.ctx.settings}; + this.callbacks = { + createChart: (chartShape, renderer) => { + const settings = pieChartWidgetPieChartSettings(this.settings); + return new TbPieChart(this.ctx, settings, chartShape.nativeElement, renderer, this.translate, true); + }, + onItemClick: ($event: Event, item) => { + const descriptors = this.ctx.actionsApi.getActionDescriptors('sliceClick'); + if ($event && descriptors.length) { + $event.stopPropagation(); + const datasource = item.datasource; + const entityId = datasource ? datasource.entity?.id : null; + const entityName = datasource ? datasource.entityName : null; + const entityLabel = datasource ? datasource.entityLabel : null; + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, item, entityLabel); + } + } + }; + } + + public onInit() { + this.latestChart?.onInit(); + } + + public onDataUpdated() { + this.latestChart?.onDataUpdated(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts new file mode 100644 index 0000000000..05dbef0959 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts @@ -0,0 +1,131 @@ +/// +/// 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 { + LatestChartTooltipValueType, + LatestChartWidgetSettings +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { BackgroundType, Font } from '@shared/models/widget-settings.models'; +import { LegendPosition } from '@shared/models/widget.models'; +import { DeepPartial } from '@shared/models/common'; +import { + pieChartAnimationDefaultSettings, + PieChartLabelPosition, + PieChartSettings +} from '@home/components/widget/lib/chart/pie-chart.models'; +import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; + +export interface PieChartWidgetSettings extends LatestChartWidgetSettings { + showLabel: boolean; + labelPosition: PieChartLabelPosition; + labelFont: Font; + labelColor: string; + borderWidth: number; + borderColor: string; + radius: number; + clockwise: boolean; +} + +export const pieChartWidgetDefaultSettings: PieChartWidgetSettings = { + showLabel: true, + labelPosition: PieChartLabelPosition.outside, + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: 'normal', + lineHeight: '1.2' + }, + labelColor: '#000', + borderWidth: 0, + borderColor: '#000', + radius: 80, + clockwise: false, + sortSeries: false, + animation: mergeDeep({} as EChartsAnimationSettings, + pieChartAnimationDefaultSettings), + showLegend: true, + legendPosition: LegendPosition.bottom, + legendLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + legendLabelColor: 'rgba(0, 0, 0, 0.38)', + legendValueFont: { + family: 'Roboto', + size: 14, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '20px' + }, + legendValueColor: 'rgba(0, 0, 0, 0.87)', + showTooltip: true, + tooltipValueType: LatestChartTooltipValueType.percentage, + tooltipValueDecimals: 0, + tooltipValueFont: { + family: 'Roboto', + size: 13, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + tooltipValueColor: 'rgba(0, 0, 0, 0.76)', + tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', + tooltipBackgroundBlur: 4, + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; + +export const pieChartWidgetPieChartSettings = (settings: PieChartWidgetSettings): DeepPartial => ({ + autoScale: false, + doughnut: false, + clockwise: settings.clockwise, + sortSeries: settings.sortSeries, + showTotal: false, + animation: settings.animation, + showLegend: settings.showLegend, + showLabel: settings.showLabel, + labelPosition: settings.labelPosition, + labelFont: settings.labelFont, + labelColor: settings.labelColor, + borderWidth: settings.borderWidth, + borderColor: settings.borderColor, + radius: isDefinedAndNotNull(settings.radius) ? settings.radius + '%' : undefined, + emphasisBorderWidth: settings.borderWidth, + emphasisBorderColor: settings.borderColor, + showTooltip: settings.showTooltip, + tooltipValueType: settings.tooltipValueType, + tooltipValueDecimals: settings.tooltipValueDecimals, + tooltipValueFont: settings.tooltipValueFont, + tooltipValueColor: settings.tooltipValueColor, + tooltipBackgroundColor: settings.tooltipBackgroundColor, + tooltipBackgroundBlur: settings.tooltipBackgroundBlur +}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts new file mode 100644 index 0000000000..419c4b61bb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts @@ -0,0 +1,126 @@ +/// +/// 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 { ColorSettings, constantColor, Font } from '@shared/models/widget-settings.models'; +import { + LatestChartSettings, + LatestChartTooltipValueType +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { mergeDeep } from '@core/utils'; +import { + EChartsAnimationEasing, + EChartsAnimationSettings +} from '@home/components/widget/lib/chart/echarts-widget.models'; + +export enum PieChartLabelPosition { + outside = 'outside', + inside = 'inside' +} + +export const pieChartLabelPositions = Object.keys(PieChartLabelPosition) as PieChartLabelPosition[]; + +export const pieChartLabelPositionTranslations = new Map( + [ + [PieChartLabelPosition.outside, 'widgets.pie-chart.label-position-outside'], + [PieChartLabelPosition.inside, 'widgets.pie-chart.label-position-inside'] + ] +); + +export interface PieChartSettings extends LatestChartSettings { + doughnut: boolean; + radius: string; + clockwise: boolean; + totalValueFont: Font; + totalValueColor: ColorSettings; + showLabel: boolean; + labelPosition: PieChartLabelPosition; + labelFont: Font; + labelColor: string; + borderWidth: number; + borderColor: string; + borderRadius: string; + emphasisScale: boolean; + emphasisBorderWidth: number; + emphasisBorderColor: string; + emphasisShadowBlur: number; + emphasisShadowColor: string; +} + +export const pieChartAnimationDefaultSettings: EChartsAnimationSettings = { + animation: true, + animationThreshold: 2000, + animationDuration: 1000, + animationEasing: EChartsAnimationEasing.cubicOut, + animationDelay: 0, + animationDurationUpdate: 500, + animationEasingUpdate: EChartsAnimationEasing.cubicOut, + animationDelayUpdate: 0 +}; + +export const pieChartDefaultSettings: PieChartSettings = { + autoScale: false, + doughnut: false, + radius: '80%', + clockwise: false, + sortSeries: false, + showTotal: false, + animation: mergeDeep({} as EChartsAnimationSettings, + pieChartAnimationDefaultSettings), + showLegend: true, + totalValueFont: { + family: 'Roboto', + size: 24, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '1' + }, + totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'), + showLabel: false, + labelPosition: PieChartLabelPosition.outside, + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: 'normal', + lineHeight: '1' + }, + labelColor: '#000', + borderWidth: 0, + borderColor: '#000', + borderRadius: '0%', + emphasisScale: true, + emphasisBorderWidth: 0, + emphasisBorderColor: '#000', + emphasisShadowBlur: 10, + emphasisShadowColor: 'rgba(0, 0, 0, 0.5)', + showTooltip: true, + tooltipValueType: LatestChartTooltipValueType.percentage, + tooltipValueDecimals: 0, + tooltipValueFont: { + family: 'Roboto', + size: 13, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + tooltipValueColor: 'rgba(0, 0, 0, 0.76)', + tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', + tooltipBackgroundBlur: 4 +}; + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts new file mode 100644 index 0000000000..cfe003732e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts @@ -0,0 +1,194 @@ +/// +/// 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 { pieChartDefaultSettings, PieChartSettings } from '@home/components/widget/lib/chart/pie-chart.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DeepPartial } from '@shared/models/common'; +import { Renderer2 } from '@angular/core'; +import { ColorProcessor, textStyle } from '@shared/models/widget-settings.models'; +import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries'; +import { Text } from '@svgdotjs/svg.js'; +import { TranslateService } from '@ngx-translate/core'; +import { TbLatestChart } from '@home/components/widget/lib/chart/latest-chart'; +import { formatValue } from '@core/utils'; + +const shapeSize = 134; +const shapeSegmentWidth = 13.4; + +export class TbPieChart extends TbLatestChart { + + private totalValueColor: ColorProcessor; + private totalTextNode: Text; + + constructor(ctx: WidgetContext, + inputSettings: DeepPartial, + chartElement: HTMLElement, + renderer: Renderer2, + translate: TranslateService, + autoResize = true) { + + super(ctx, inputSettings, chartElement, renderer, translate, autoResize); + } + + protected defaultSettings(): PieChartSettings { + return pieChartDefaultSettings; + } + + protected initSettings() { + if (this.settings.showTotal) { + this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor); + } + } + + protected prepareLatestChartOption() { + const shapeWidth = this.chartElement.offsetWidth; + const shapeHeight = this.chartElement.offsetHeight; + const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight); + const innerRadius = size / 2 - shapeSegmentWidth; + const outerRadius = size / 2; + const labelStyle = textStyle(this.settings.labelFont); + labelStyle.fontSize = this.settings.labelFont.size; + labelStyle.lineHeight = labelStyle.fontSize * 1.2; + labelStyle.color = this.settings.labelColor; + this.latestChartOption.series = [ + { + type: 'pie', + clockwise: this.settings.clockwise, + radius: this.settings.doughnut ? [innerRadius, outerRadius] : this.settings.radius, + avoidLabelOverlap: false, + itemStyle: { + borderRadius: this.settings.borderRadius, + borderWidth: this.settings.borderWidth, + borderColor: this.settings.borderColor + }, + label: { + show: this.settings.showLabel, + position: this.settings.labelPosition, + formatter: (params) => { + const percents = params.percent; + const value = formatValue(percents, 0, '%', false); + return `{label|${params.name}\n${value}}`; + }, + rich: { + label: labelStyle + } + }, + emphasis: { + scale: this.settings.emphasisScale, + itemStyle: { + borderColor: this.settings.emphasisBorderColor, + borderWidth: this.settings.emphasisBorderWidth, + shadowColor: this.settings.emphasisShadowColor, + shadowBlur: this.settings.emphasisShadowBlur + }, + label: { + show: this.settings.showLabel + } + }, + animation: this.settings.animation.animation, + animationThreshold: this.settings.animation.animationThreshold, + animationDuration: this.settings.animation.animationDuration, + animationEasing: this.settings.animation.animationEasing, + animationDelay: this.settings.animation.animationDelay, + animationDurationUpdate: this.settings.animation.animationDurationUpdate, + animationEasingUpdate: this.settings.animation.animationEasingUpdate, + animationDelayUpdate: this.settings.animation.animationDelayUpdate + } + ]; + } + + protected afterDrawChart() { + if (this.settings.showTotal) { + this.totalTextNode = this.svgShape.text('').font({ + family: 'Roboto', + leading: 1 + }).attr({'text-anchor': 'middle'}); + this.renderTotal(); + } + }; + + protected doUpdateSeriesData() { + const seriesData: PieDataItemOption[] = []; + const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue); + for (const dataItem of this.dataItems) { + if (dataItem.enabled && dataItem.hasValue) { + seriesData.push( + {id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}} + ); + if (this.settings.doughnut && enabledDataItems.length > 1) { + seriesData.push({ + value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true} + }); + } + } + } + if (this.settings.doughnut) { + for (let i = 1; i < seriesData.length; i += 2) { + seriesData[i].value = this.total / 100; + } + } + this.latestChartOption.series[0].data = seriesData; + } + + protected afterUpdateSeriesData(initial: boolean) { + if (this.settings.showTotal) { + this.totalValueColor.update(this.total); + if (!initial) { + this.renderTotal(); + } + } + }; + + protected initialShapeWidth(): number { + return shapeSize; + } + + protected initialShapeHeight(): number { + return shapeSize; + } + + protected beforeResize(shapeWidth: number, shapeHeight: number) { + if (!this.settings.autoScale) { + if (this.settings.doughnut) { + const size = Math.min(shapeWidth, shapeHeight); + const innerRadius = size / 2 - shapeSegmentWidth; + const outerRadius = size / 2; + this.latestChartOption.series[0].radius = [innerRadius, outerRadius]; + this.latestChart.setOption(this.latestChartOption); + } + } + }; + + protected afterResize(shapeWidth: number, shapeHeight: number) { + if (this.settings.showTotal) { + this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2, + (this.settings.autoScale ? shapeSize : shapeHeight) / 2); + } + }; + + private renderTotal() { + this.totalTextNode.text(add => { + add.tspan(this.translate.instant('widgets.latest-chart.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)'); + add.tspan('').newLine().font({size: '4px'}); + add.tspan(this.totalText).newLine().font( + {family: this.settings.totalValueFont.family, + size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit, + weight: this.settings.totalValueFont.weight, + style: this.settings.totalValueFont.style} + ).fill(this.totalValueColor.color); + }).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index c1221536ee..c411737761 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -24,15 +24,19 @@ import { sortedColorRange } from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; -import { EChartsShape, EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { + echartsAnimationDefaultSettings, + EChartsAnimationSettings, + EChartsShape, + EChartsTooltipWidgetSettings +} from '@home/components/widget/lib/chart/echarts-widget.models'; import { createTimeSeriesChartVisualMapPiece, defaultTimeSeriesChartXAxisSettings, defaultTimeSeriesChartYAxisSettings, LineSeriesStepType, SeriesFillType, - SeriesLabelPosition, ThresholdLabelPosition, timeSeriesChartAnimationDefaultSettings, - TimeSeriesChartAnimationSettings, + SeriesLabelPosition, ThresholdLabelPosition, timeSeriesChartColorScheme, timeSeriesChartGridDefaultSettings, TimeSeriesChartGridSettings, TimeSeriesChartKeySettings, TimeSeriesChartLineType, @@ -84,7 +88,7 @@ export interface RangeChartWidgetSettings extends EChartsTooltipWidgetSettings { grid: TimeSeriesChartGridSettings; yAxis: TimeSeriesChartYAxisSettings; xAxis: TimeSeriesChartXAxisSettings; - animation: TimeSeriesChartAnimationSettings; + animation: EChartsAnimationSettings; thresholds: TimeSeriesChartThreshold[]; showLegend: boolean; legendPosition: LegendPosition; @@ -150,8 +154,8 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, defaultTimeSeriesChartXAxisSettings, {showSplitLines: false} as TimeSeriesChartXAxisSettings), - animation: mergeDeep({} as TimeSeriesChartAnimationSettings, - timeSeriesChartAnimationDefaultSettings), + animation: mergeDeep({} as EChartsAnimationSettings, + echartsAnimationDefaultSettings), thresholds: [], showLegend: true, legendPosition: LegendPosition.top, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index e03c2a7158..0fd702a95f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -16,6 +16,8 @@ import { ECharts, + echartsAnimationDefaultSettings, + EChartsAnimationSettings, EChartsOption, EChartsSeriesItem, EChartsShape, @@ -571,64 +573,6 @@ export interface TimeSeriesChartBarWidthSettings { intervalGap: number; } -export enum TimeSeriesChartAnimationEasing { - linear = 'linear', - quadraticIn = 'quadraticIn', - quadraticOut = 'quadraticOut', - quadraticInOut = 'quadraticInOut', - cubicIn = 'cubicIn', - cubicOut = 'cubicOut', - cubicInOut = 'cubicInOut', - quarticIn = 'quarticIn', - quarticOut = 'quarticOut', - quarticInOut = 'quarticInOut', - quinticIn = 'quinticIn', - quinticOut = 'quinticOut', - quinticInOut = 'quinticInOut', - sinusoidalIn = 'sinusoidalIn', - sinusoidalOut = 'sinusoidalOut', - sinusoidalInOut = 'sinusoidalInOut', - exponentialIn = 'exponentialIn', - exponentialOut = 'exponentialOut', - exponentialInOut = 'exponentialInOut', - circularIn = 'circularIn', - circularOut = 'circularOut', - circularInOut = 'circularInOut', - elasticIn = 'elasticIn', - elasticOut = 'elasticOut', - elasticInOut = 'elasticInOut', - backIn = 'backIn', - backOut = 'backOut', - backInOut = 'backInOut', - bounceIn = 'bounceIn', - bounceOut = 'bounceOut', - bounceInOut = 'bounceInOut' -} - -export const timeSeriesChartAnimationEasings = Object.keys(TimeSeriesChartAnimationEasing) as TimeSeriesChartAnimationEasing[]; - -export interface TimeSeriesChartAnimationSettings { - animation: boolean; - animationThreshold: number; - animationDuration: number; - animationEasing: TimeSeriesChartAnimationEasing; - animationDelay: number; - animationDurationUpdate: number; - animationEasingUpdate: TimeSeriesChartAnimationEasing; - animationDelayUpdate: number; -} - -export const timeSeriesChartAnimationDefaultSettings: TimeSeriesChartAnimationSettings = { - animation: true, - animationThreshold: 2000, - animationDuration: 500, - animationEasing: TimeSeriesChartAnimationEasing.cubicOut, - animationDelay: 0, - animationDurationUpdate: 300, - animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut, - animationDelayUpdate: 0 -}; - export interface TimeSeriesChartVisualMapPiece { lt?: number; gt?: number; @@ -721,7 +665,7 @@ export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, T grid: TimeSeriesChartGridSettings; yAxes: TimeSeriesChartYAxes; xAxis: TimeSeriesChartXAxisSettings; - animation: TimeSeriesChartAnimationSettings; + animation: EChartsAnimationSettings; barWidthSettings: TimeSeriesChartBarWidthSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; visualMapSettings?: TimeSeriesChartVisualMapSettings; @@ -742,8 +686,8 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { }, xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, defaultTimeSeriesChartXAxisSettings), - animation: mergeDeep({} as TimeSeriesChartAnimationSettings, - timeSeriesChartAnimationDefaultSettings), + animation: mergeDeep({} as EChartsAnimationSettings, + echartsAnimationDefaultSettings), barWidthSettings: { barGap: 0.3, intervalGap: 0.6 diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 04b2bff29c..920374ec00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -227,9 +227,9 @@
- - +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html index ac99c1c42e..863663db08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html @@ -32,17 +32,17 @@
- {{ 'widgets.doughnut.auto-scale' | translate }} + {{ 'widgets.latest-chart.auto-scale' | translate }}
- {{ 'widgets.doughnut.clockwise-layout' | translate }} + {{ 'widgets.latest-chart.clockwise-layout' | translate }}
- {{ 'widgets.doughnut.sort-series' | translate }} + {{ 'widgets.latest-chart.sort-series' | translate }}
@@ -119,8 +119,8 @@
- - {{ doughnutTooltipValueTypeTranslationMap.get(type) | translate }} + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} @@ -154,6 +154,9 @@
+ +
{{ 'widgets.background.background' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts index 20910e36af..c71f4b8a8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts @@ -24,19 +24,22 @@ import { import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { formatValue, isDefinedAndNotNull } from '@core/utils'; +import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { doughnutDefaultSettings, DoughnutLayout, doughnutLayoutImages, doughnutLayouts, doughnutLayoutTranslations, - DoughnutTooltipValueType, - doughnutTooltipValueTypes, - doughnutTooltipValueTypeTranslations, + DoughnutWidgetSettings, horizontalDoughnutLayoutImages } from '@home/components/widget/lib/chart/doughnut-widget.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + LatestChartTooltipValueType, + latestChartTooltipValueTypes, + latestChartTooltipValueTypeTranslations +} from '@home/components/widget/lib/chart/latest-chart.models'; @Component({ selector: 'tb-doughnut-widget-settings', @@ -62,9 +65,9 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent { legendPositionTranslationMap = legendPositionTranslationMap; - doughnutTooltipValueTypes = doughnutTooltipValueTypes; + latestChartTooltipValueTypes = latestChartTooltipValueTypes; - doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations; + latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations; doughnutWidgetSettingsForm: UntypedFormGroup; @@ -88,7 +91,7 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent { } protected defaultSettings(): WidgetSettings { - return doughnutDefaultSettings(this.horizontal); + return mergeDeep({} as DoughnutWidgetSettings, doughnutDefaultSettings(this.horizontal)); } protected onSettingsSet(settings: WidgetSettings) { @@ -101,6 +104,8 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent { totalValueFont: [settings.totalValueFont, []], totalValueColor: [settings.totalValueColor, []], + animation: [settings.animation, []], + showLegend: [settings.showLegend, []], legendPosition: [settings.legendPosition, []], legendLabelFont: [settings.legendLabelFont, []], @@ -175,9 +180,9 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent { } private _tooltipValuePreviewFn(): string { - const tooltipValueType: DoughnutTooltipValueType = this.doughnutWidgetSettingsForm.get('tooltipValueType').value; + const tooltipValueType: LatestChartTooltipValueType = this.doughnutWidgetSettingsForm.get('tooltipValueType').value; const decimals: number = this.doughnutWidgetSettingsForm.get('tooltipValueDecimals').value; - if (tooltipValueType === DoughnutTooltipValueType.percentage) { + if (tooltipValueType === LatestChartTooltipValueType.percentage) { return formatValue(35, decimals, '%', false); } else { const units: string = this.widgetConfig.config.units; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.html new file mode 100644 index 0000000000..3f15f8af34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.html @@ -0,0 +1,184 @@ + + +
+
widgets.pie-chart.pie-chart-card-style
+
+ + {{ 'widgets.pie-chart.label' | translate }} + +
+ + + + {{ pieChartLabelPositionTranslationMap.get(position) | translate }} + + + + + + + +
+
+
+
{{ 'widgets.pie-chart.border' | translate }}
+
+ + +
px
+
+ + +
+
+
+
{{ 'widgets.pie-chart.radius' | translate }}
+
+ + +
%
+
+
+
+
+ + {{ 'widgets.latest-chart.clockwise-layout' | translate }} + +
+
+ + {{ 'widgets.latest-chart.sort-series' | translate }} + +
+
+ + + + + {{ 'widget-config.legend' | translate }} + + + + +
+
{{ 'legend.position' | translate }}
+ + + + {{ legendPositionTranslationMap.get(pos) | translate }} + + + +
+
+
{{ 'legend.label' | translate }}
+
+ + + + +
+
+
+
{{ 'legend.value' | translate }}
+
+ + + + +
+
+
+
+
+
+ + + + + {{ 'widget-config.tooltip' | translate }} + + + + +
+
{{ 'tooltip.value' | translate }}
+
+ + + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} + + + + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+
{{ 'tooltip.background-color' | translate }}
+ + +
+
+
{{ 'tooltip.background-blur' | translate }}
+ + +
px
+
+
+
+
+
+ + +
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts new file mode 100644 index 0000000000..ae96753264 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts @@ -0,0 +1,182 @@ +/// +/// 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 } from '@angular/core'; +import { + legendPositions, + legendPositionTranslationMap, + WidgetSettings, + WidgetSettingsComponent +} from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { formatValue, mergeDeep } from '@core/utils'; +import { + LatestChartTooltipValueType, + latestChartTooltipValueTypes, + latestChartTooltipValueTypeTranslations +} from '@home/components/widget/lib/chart/latest-chart.models'; +import { + pieChartLabelPositions, + pieChartLabelPositionTranslations +} from '@home/components/widget/lib/chart/pie-chart.models'; +import { + pieChartWidgetDefaultSettings, + PieChartWidgetSettings +} from '@home/components/widget/lib/chart/pie-chart-widget.models'; + +@Component({ + selector: 'tb-pie-chart-widget-settings', + templateUrl: './pie-chart-widget-settings.component.html', + styleUrls: [] +}) +export class PieChartWidgetSettingsComponent extends WidgetSettingsComponent { + + pieChartLabelPositions = pieChartLabelPositions; + + pieChartLabelPositionTranslationMap = pieChartLabelPositionTranslations; + + legendPositions = legendPositions; + + legendPositionTranslationMap = legendPositionTranslationMap; + + latestChartTooltipValueTypes = latestChartTooltipValueTypes; + + latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations; + + pieChartWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.pieChartWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return mergeDeep({} as PieChartWidgetSettings, pieChartWidgetDefaultSettings); + } + + protected onSettingsSet(settings: WidgetSettings) { + this.pieChartWidgetSettingsForm = this.fb.group({ + showLabel: [settings.showLabel, []], + labelPosition: [settings.labelPosition, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + borderWidth: [settings.borderWidth, [Validators.min(0)]], + borderColor: [settings.borderColor, []], + + radius: [settings.radius, [Validators.min(0), Validators.max(100)]], + + clockwise: [settings.clockwise, []], + sortSeries: [settings.sortSeries, []], + + animation: [settings.animation, []], + + showLegend: [settings.showLegend, []], + legendPosition: [settings.legendPosition, []], + legendLabelFont: [settings.legendLabelFont, []], + legendLabelColor: [settings.legendLabelColor, []], + legendValueFont: [settings.legendValueFont, []], + legendValueColor: [settings.legendValueColor, []], + + showTooltip: [settings.showTooltip, []], + tooltipValueType: [settings.tooltipValueType, []], + tooltipValueDecimals: [settings.tooltipValueDecimals, []], + tooltipValueFont: [settings.tooltipValueFont, []], + tooltipValueColor: [settings.tooltipValueColor, []], + tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], + tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showLabel', 'showLegend', 'showTooltip']; + } + + protected updateValidators(emitEvent: boolean) { + const showLabel: boolean = this.pieChartWidgetSettingsForm.get('showLabel').value; + const showLegend: boolean = this.pieChartWidgetSettingsForm.get('showLegend').value; + const showTooltip: boolean = this.pieChartWidgetSettingsForm.get('showTooltip').value; + + if (showLabel) { + this.pieChartWidgetSettingsForm.get('labelPosition').enable(); + this.pieChartWidgetSettingsForm.get('labelFont').enable(); + this.pieChartWidgetSettingsForm.get('labelColor').enable(); + } else { + this.pieChartWidgetSettingsForm.get('labelPosition').disable(); + this.pieChartWidgetSettingsForm.get('labelFont').disable(); + this.pieChartWidgetSettingsForm.get('labelColor').disable(); + } + + if (showLegend) { + this.pieChartWidgetSettingsForm.get('legendPosition').enable(); + this.pieChartWidgetSettingsForm.get('legendLabelFont').enable(); + this.pieChartWidgetSettingsForm.get('legendLabelColor').enable(); + this.pieChartWidgetSettingsForm.get('legendValueFont').enable(); + this.pieChartWidgetSettingsForm.get('legendValueColor').enable(); + } else { + this.pieChartWidgetSettingsForm.get('legendPosition').disable(); + this.pieChartWidgetSettingsForm.get('legendLabelFont').disable(); + this.pieChartWidgetSettingsForm.get('legendLabelColor').disable(); + this.pieChartWidgetSettingsForm.get('legendValueFont').disable(); + this.pieChartWidgetSettingsForm.get('legendValueColor').disable(); + } + if (showTooltip) { + this.pieChartWidgetSettingsForm.get('tooltipValueType').enable(); + this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').enable(); + this.pieChartWidgetSettingsForm.get('tooltipValueFont').enable(); + this.pieChartWidgetSettingsForm.get('tooltipValueColor').enable(); + this.pieChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); + this.pieChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); + } else { + this.pieChartWidgetSettingsForm.get('tooltipValueType').disable(); + this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').disable(); + this.pieChartWidgetSettingsForm.get('tooltipValueFont').disable(); + this.pieChartWidgetSettingsForm.get('tooltipValueColor').disable(); + this.pieChartWidgetSettingsForm.get('tooltipBackgroundColor').disable(); + this.pieChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable(); + } + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(110, decimals, units, false); + } + + private _tooltipValuePreviewFn(): string { + const tooltipValueType: LatestChartTooltipValueType = this.pieChartWidgetSettingsForm.get('tooltipValueType').value; + const decimals: number = this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').value; + if (tooltipValueType === LatestChartTooltipValueType.percentage) { + return formatValue(35, decimals, '%', false); + } else { + const units: string = this.widgetConfig.config.units; + return formatValue(110, decimals, units, false); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html index 63b514c68b..30c5eea4c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -303,9 +303,9 @@
- - +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 737e2e2c21..6bf8abdb70 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -254,9 +254,9 @@
- - +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.html similarity index 80% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.html index 5e0ad8e2ec..53246d2cfc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.html @@ -23,61 +23,61 @@ - {{ 'widgets.time-series-chart.animation.animation' | translate }} + {{ 'widgets.echarts.animation.animation' | translate }}
-
{{ 'widgets.time-series-chart.animation.animation-threshold' | translate }}
+
{{ 'widgets.echarts.animation.animation-threshold' | translate }}
ms
-
{{ 'widgets.time-series-chart.animation.animation-duration' | translate }}
+
{{ 'widgets.echarts.animation.animation-duration' | translate }}
ms
-
widgets.time-series-chart.animation.animation-easing
+
widgets.echarts.animation.animation-easing
- + {{ easing }}
-
{{ 'widgets.time-series-chart.animation.animation-delay' | translate }}
+
{{ 'widgets.echarts.animation.animation-delay' | translate }}
ms
-
{{ 'widgets.time-series-chart.animation.update-animation-duration' | translate }}
+
{{ 'widgets.echarts.animation.update-animation-duration' | translate }}
ms
-
widgets.time-series-chart.animation.update-animation-easing
+
widgets.echarts.animation.update-animation-easing
- + {{ easing }}
-
{{ 'widgets.time-series-chart.animation.update-animation-delay' | translate }}
+
{{ 'widgets.echarts.animation.update-animation-delay' | translate }}
ms
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.ts similarity index 83% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.ts index 5da5aa2fe9..b3a10c6298 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.ts @@ -22,34 +22,34 @@ import { UntypedFormGroup, Validators } from '@angular/forms'; -import { - timeSeriesChartAnimationEasings, - TimeSeriesChartAnimationSettings -} from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetService } from '@core/http/widget.service'; +import { + echartsAnimationEasings, + EChartsAnimationSettings +} from '@home/components/widget/lib/chart/echarts-widget.models'; @Component({ - selector: 'tb-time-series-chart-animation-settings', - templateUrl: './time-series-chart-animation-settings.component.html', + selector: 'tb-echarts-animation-settings', + templateUrl: './echarts-animation-settings.component.html', styleUrls: ['./../../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartAnimationSettingsComponent), + useExisting: forwardRef(() => EchartsAnimationSettingsComponent), multi: true } ] }) -export class TimeSeriesChartAnimationSettingsComponent implements OnInit, ControlValueAccessor { +export class EchartsAnimationSettingsComponent implements OnInit, ControlValueAccessor { settingsExpanded = false; - timeSeriesChartAnimationEasings = timeSeriesChartAnimationEasings; + echartsAnimationEasings = echartsAnimationEasings; @Input() disabled: boolean; - private modelValue: TimeSeriesChartAnimationSettings; + private modelValue: EChartsAnimationSettings; private propagateChange = null; @@ -96,7 +96,7 @@ export class TimeSeriesChartAnimationSettingsComponent implements OnInit, Contro } } - writeValue(value: TimeSeriesChartAnimationSettings): void { + writeValue(value: EChartsAnimationSettings): void { this.modelValue = value; this.animationSettingsFormGroup.patchValue( value, {emitEvent: false} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html index d171833001..e897a9de8b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html @@ -83,7 +83,7 @@
widgets.widget-font.preview
-
{{ previewText }}
+