From 8f49f2a0214de38c8f7714e315e3a01d23c26271 Mon Sep 17 00:00:00 2001 From: devaskim Date: Tue, 1 Nov 2022 16:14:39 +0500 Subject: [PATCH 001/452] Introduce NOT option for relation query. --- .../data/query/RelationsQueryFilter.java | 1 + .../relation/RelationEntityTypeFilter.java | 15 ++++++- .../query/DefaultEntityQueryRepository.java | 21 +++++++--- .../entity/entity-filter.component.html | 3 ++ .../entity/entity-filter.component.ts | 3 +- .../relation/relation-filters.component.html | 41 +++++++++++-------- .../relation/relation-filters.component.ts | 6 ++- ui-ngx/src/app/shared/models/alias.models.ts | 1 + .../src/app/shared/models/relation.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 4 +- 10 files changed, 66 insertions(+), 30 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 2bed46a63b..ea9f031de5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -40,5 +40,6 @@ public class RelationsQueryFilter implements EntityFilter { private List filters; private int maxLevel; private boolean fetchLastLevelOnly; + private boolean negate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index 6bac4c9a0f..18786250a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.relation; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.EntityType; @@ -27,13 +26,25 @@ import java.util.List; * Created by ashvayka on 02.05.17. */ @Data -@AllArgsConstructor @ApiModel public class RelationEntityTypeFilter { + public RelationEntityTypeFilter(String relationType, List entityTypes) { + this(relationType, entityTypes, false); + } + + public RelationEntityTypeFilter(String relationType, List entityTypes, boolean negate) { + this.relationType = relationType; + this.entityTypes = entityTypes; + this.negate = negate; + } + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains") private String relationType; @ApiModelProperty(position = 2, value = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').") private List entityTypes; + + @ApiModelProperty(position = 3, value = "Negate relation type between root entity and other entity.") + private boolean negate; } 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 1963190aa5..be91d78704 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 @@ -704,10 +704,16 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { .append(entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from") .append("_type in (:where_entity_types").append(")"); ctx.addStringListParameter("where_entity_types", Arrays.stream(RELATION_QUERY_ENTITY_TYPES).map(EntityType::name).collect(Collectors.toList())); - } - - if (!noConditions && !single) { - whereFilter = new StringBuilder().append("(").append(whereFilter).append(")"); + } else { + if (!single) { + whereFilter = new StringBuilder() + .append(entityFilter.isNegate() ? " NOT (" : "(") + .append(whereFilter).append(")"); + } else if (entityFilter.isNegate()) { + whereFilter = new StringBuilder() + .append(" NOT ") + .append(whereFilter).append(")"); + } } if (entityFilter.isFetchLastLevelOnly()) { @@ -743,8 +749,11 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { boolean hasRelationType = !StringUtils.isEmpty(relationType); if (hasRelationType) { ctx.addStringParameter("where_relation_type" + entityTypeFilterIdx, relationType); - whereFilter - .append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx); + if (etf.isNegate()) { + whereFilter.append("re.relation_type != :where_relation_type").append(entityTypeFilterIdx); + } else { + whereFilter.append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx); + } } if (!whereEntityTypes.isEmpty()) { if (hasRelationType) { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 6f588f34e5..85ac9febf5 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -185,6 +185,9 @@ formControlName="maxLevel"> + + {{ 'relation.negate-all-relations' | translate }} +
relation.relation-filters
{ this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]); diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html index 1f5d6273ec..095cfe6eba 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html @@ -28,24 +28,29 @@
-
- - - - - +
+
+ + + + + +
+ + {{ 'relation.negate-relation' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index 911aa4fe30..71d4350b4f 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -104,7 +104,8 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; const filter: RelationEntityTypeFilter = { relationType: null, - entityTypes: [] + entityTypes: [], + negate: false }; relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); } @@ -112,7 +113,8 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl { return this.fb.group({ relationType: [filter ? filter.relationType : null], - entityTypes: [filter ? filter.entityTypes : []] + entityTypes: [filter ? filter.entityTypes : []], + negate: [filter ? filter.negate : false], }); } diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index f67c0d50a7..c3322cda16 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -114,6 +114,7 @@ export interface RelationsQueryFilter { filters?: Array; maxLevel?: number; fetchLastLevelOnly?: boolean; + negate?: boolean; } export interface EntitySearchQueryFilter { diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index e42b915e9e..27920f275a 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -55,6 +55,7 @@ export const directionTypeTranslations = new Map( export interface RelationEntityTypeFilter { relationType: string; entityTypes: Array; + negate?: boolean; } export interface RelationsSearchParameters { 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 e8c322c650..b053cd83a6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2840,7 +2840,9 @@ "relation-filters": "Relation filters", "additional-info": "Additional info (JSON)", "invalid-additional-info": "Unable to parse additional info json.", - "no-relations-text": "No relations found" + "no-relations-text": "No relations found", + "negate-all-relations": "Negate all relations (NOT condition)", + "negate-relation": "Negate this relation (NOT condition)" }, "resource": { "add": "Add Resource", From 11eca7684aa82227ecb655495b6d372d4b725522 Mon Sep 17 00:00:00 2001 From: devaskim Date: Tue, 1 Nov 2022 20:49:49 +0500 Subject: [PATCH 002/452] Add default constructor to fix deserialization issue. --- .../server/common/data/relation/RelationEntityTypeFilter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index 18786250a5..e8922c4c94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -29,6 +29,8 @@ import java.util.List; @ApiModel public class RelationEntityTypeFilter { + public RelationEntityTypeFilter() {} + public RelationEntityTypeFilter(String relationType, List entityTypes) { this(relationType, entityTypes, false); } From c40fbf7dd9f46267aec504a66334c7ccf6779282 Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 7 Nov 2023 18:44:46 +0200 Subject: [PATCH 003/452] UI: alarm table widget clear selection on entering edit mode --- .../main/data/json/system/widget_types/alarms_table.json | 2 +- .../widget/lib/alarm/alarms-table-widget.component.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 5a360bcc32..12716482c9 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 20f123972a..133f484c28 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -321,6 +321,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.ctx.detectChanges(); } + public onEditModeChanged() { + if (this.alarmsDatasource.selection.hasValue()) { + this.alarmsDatasource.clearSelection(); + } + } + public pageLinkSortDirection(): SortDirection { return entityDataPageLinkSortDirection(this.pageLink); } From b75704e6feb3c44fcbd08f2c494398f731ff1682 Mon Sep 17 00:00:00 2001 From: rusikv Date: Wed, 8 Nov 2023 15:50:24 +0200 Subject: [PATCH 004/452] UI: alarm table widget on edit mode if selection has value do not hide title panel with edit action --- .../widget/lib/alarm/alarms-table-widget.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 133f484c28..20526591ef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -322,8 +322,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } public onEditModeChanged() { - if (this.alarmsDatasource.selection.hasValue()) { - this.alarmsDatasource.clearSelection(); + if (this.enableSelection && this.alarmsDatasource.selection.hasValue()) { + this.ctx.hideTitlePanel = !this.ctx.isEdit; + this.ctx.detectChanges(true); } } From acc2b068f514cbe99f16f82cb6e453d20c408f6d Mon Sep 17 00:00:00 2001 From: rusikv Date: Mon, 13 Nov 2023 17:50:13 +0200 Subject: [PATCH 005/452] Added ability to display right layout first in mobile view --- .../dashboard-page.component.html | 4 +- .../dashboard-page.component.ts | 7 ++++ .../dashboard-settings-dialog.component.html | 39 +++++++++++-------- .../dashboard-settings-dialog.component.ts | 7 ++++ ...nage-dashboard-layouts-dialog.component.ts | 3 +- .../src/app/shared/models/dashboard.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 42 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index a388f69252..1cbdd2fcf8 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -296,7 +296,7 @@ [(opened)]="rightLayoutOpened"> -
+
dashboard.mobile-layout - - {{ 'dashboard.autofill-height' | translate }} + + {{ 'dashboard.display-first-in-mobile-view' | translate }} - - dashboard.mobile-row-height - - - {{ 'dashboard.mobile-row-height-required' | translate }} - - - {{ 'dashboard.min-mobile-row-height-message' | translate }} - - - {{ 'dashboard.max-mobile-row-height-message' | translate }} - - +
+ + {{ 'dashboard.autofill-height' | translate }} + + + dashboard.mobile-row-height + + + {{ 'dashboard.mobile-row-height-required' | translate }} + + + {{ 'dashboard.min-mobile-row-height-message' | translate }} + + + {{ 'dashboard.max-mobile-row-height-message' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index 4844211355..303964e099 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -32,6 +32,7 @@ import { StatesControllerService } from './states/states-controller.service'; export interface DashboardSettingsDialogData { settings?: DashboardSettings; gridSettings?: GridSettings; + isRightLayout?: boolean; } @Component({ @@ -45,6 +46,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent { if (mobileAutoFillHeightValue) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts index 1c04d05bae..b93a362f92 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts @@ -217,7 +217,8 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent { if (data && data.gridSettings) { diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index ffe475d19d..8b92f9eb39 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -58,6 +58,7 @@ export interface GridSettings { autoFillHeight?: boolean; mobileAutoFillHeight?: boolean; mobileRowHeight?: number; + mobileDisplayLayoutFirst?: boolean; layoutDimension?: LayoutDimension; [key: string]: any; } 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 ef660731bf..194eee430a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1113,6 +1113,7 @@ "mobile-row-height-required": "Mobile row height value is required.", "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.", "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.", + "display-first-in-mobile-view": "Display first in mobile view", "title-settings": "Title settings", "display-title": "Display dashboard title", "title-color": "Title color", From e817542db74fe832e6b0c479a17bf3302d4bf979 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 12 Dec 2023 17:37:54 +0100 Subject: [PATCH 006/452] added tests and fix --- .../BaseEntityQueryControllerTest.java | 63 +++++++++++++++++++ .../query/DefaultEntityQueryRepository.java | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index 8c692ef8a2..f1a342b767 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -46,7 +46,11 @@ 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.RelationsQueryFilter; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; import java.util.ArrayList; @@ -224,6 +228,65 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe } + @Test + public void testFindEntityDataByQueryWithNegateParam() throws Exception { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + Device mainDevice = new Device(); + mainDevice.setName("Main device"); + mainDevice = doPost("/api/device", mainDevice, Device.class); + + for (int i = 0; i < 10; i++) { + EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "CONTAINS"); + doPost("/api/relation", relation).andExpect(status().isOk()); + } + + for (int i = 10; i < 97; i++) { + EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "NOT_CONTAINS"); + doPost("/api/relation", relation).andExpect(status().isOk()); + } + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setRootEntity(mainDevice.getId()); + filter.setDirection(EntitySearchDirection.FROM); + filter.setNegate(true); + filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false))); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + + PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + + Assert.assertEquals(87, data.getTotalElements()); + + filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false))); + query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + Assert.assertEquals(10, data.getTotalElements()); + + filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true))); + query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + Assert.assertEquals(87, data.getTotalElements()); + } + + private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) { + return new EntityRelation(mainDevice.getId(), device.getId(), relationType); + } + @Test public void testFindEntityDataByQueryWithAttributes() throws Exception { List devices = new ArrayList<>(); 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 be91d78704..cdae7eff0f 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 @@ -711,7 +711,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { .append(whereFilter).append(")"); } else if (entityFilter.isNegate()) { whereFilter = new StringBuilder() - .append(" NOT ") + .append(" NOT (") .append(whereFilter).append(")"); } } From e5311984bba0d6449e6db65c9fdfac22cf2e98b7 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 7 Feb 2024 15:24:40 +0200 Subject: [PATCH 007/452] 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 008/452] 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 009/452] 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 010/452] 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 011/452] 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 012/452] 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 013/452] 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 8e16a1562a62c87f0e321c3729c7cdd7d434c2a1 Mon Sep 17 00:00:00 2001 From: devaskim Date: Sat, 17 Feb 2024 18:58:11 +0500 Subject: [PATCH 014/452] Implemented cell click action for table widgets. --- .../widget_types/asset_admin_table.json | 2 +- .../widget_types/device_admin_table.json | 2 +- .../system/widget_types/entities_table.json | 2 +- .../entities-table-widget.component.html | 38 ++++++++++++++----- .../entity/entities-table-widget.component.ts | 30 +++++++++++++++ .../assets/locale/locale.constant-en_US.json | 2 + 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json index 0a2eb2c37a..6e1dc45660 100644 --- a/application/src/main/data/json/system/widget_types/asset_admin_table.json +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n },\n 'cellDoubleClick': {\n name: 'widget-action.cell-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json index af460a286c..61aecb377d 100644 --- a/application/src/main/data/json/system/widget_types/device_admin_table.json +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n },\n 'cellDoubleClick': {\n name: 'widget-action.cell-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 1ebf8d4fde..5b8ccc1d50 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n },\n 'cellDoubleClick': {\n name: 'widget-action.cell-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html index 22e79fd526..46ea65141c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html @@ -42,10 +42,19 @@ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> {{ column.title }} - - + + + + + + + + - + + + + + + ): boolean { + let configured = false; + actionSourceIds.forEach(id => configured = configured || this.ctx.actionsApi.getActionDescriptors(id).length > 0 ); + return configured; + } + ngOnDestroy(): void { if (this.widgetResize$) { this.widgetResize$.disconnect(); @@ -310,6 +317,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni cssParser.cssPreviewNamespace = namespace; cssParser.createStyleElement(namespace, cssString); $(this.elementRef.nativeElement).addClass(namespace); + + this.useCellClickAction = this.isActionsConfigured(['cellClick', 'cellDoubleClick']) && + !this.isActionsConfigured(['rowClick', 'rowDoubleClick']); } private updateDatasources() { @@ -687,6 +697,26 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } } + public onCellClick($event: Event, entity: EntityData, key: EntityColumn, isDouble?: boolean) { + if ($event) { + $event.stopPropagation(); + } + this.entityDatasource.toggleCurrentEntity(entity); + const actionSourceId = isDouble ? 'cellDoubleClick' : 'cellClick'; + const descriptors = this.ctx.actionsApi.getActionDescriptors(actionSourceId); + if (descriptors.length) { + let entityId; + let entityName; + let entityLabel; + if (entity) { + entityId = entity.id; + entityName = entity.entityName; + entityLabel = entity.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {entity, key}, entityLabel); + } + } + public onRowClick($event: Event, entity: EntityData, isDouble?: boolean) { if ($event) { $event.stopPropagation(); 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..aca909ad88 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6911,6 +6911,7 @@ "widget-action": { "action-cell-button": "Action cell button", "row-click": "On row click", + "cell-click": "On cell click", "polygon-click": "On polygon click", "marker-click": "On marker click", "circle-click": "On circle click", @@ -6919,6 +6920,7 @@ "element-click": "On HTML element click", "pie-slice-click": "On slice click", "row-double-click": "On row double click", + "cell-double-click": "On cell double click", "card-click": "On card click", "click": "On click" } From 721854c8793e5f89c71cfdf05fb2c2320ed6a62b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 20 Feb 2024 18:50:29 +0200 Subject: [PATCH 015/452] 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 016/452] 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 017/452] 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 018/452] 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 019/452] 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 020/452] 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 021/452] 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 e1f8f519006d313b4124d92de65a207f620fc26f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 1 Mar 2024 14:45:51 +0200 Subject: [PATCH 022/452] UI: Color picker design improvement --- .../components/color-input.component.ts | 8 +- .../color-picker-panel.component.html | 26 ++++--- .../color-picker-panel.component.scss | 17 ++++- .../color-picker-panel.component.ts | 16 +++- .../color-picker/color-picker.component.html | 57 +++++++------- .../color-picker/color-picker.component.scss | 49 +++++++++++- .../color-picker/color-picker.component.ts | 4 +- .../color-picker/hex-input.component.html | 36 +++++++++ .../color-picker/hex-input.component.scss | 48 ++++++++++++ .../color-picker/hex-input.component.ts | 75 +++++++++++++++++++ .../dialog/color-picker-dialog.component.html | 8 +- .../dialog/color-picker-dialog.component.scss | 28 +++++++ .../shared/components/popover.component.ts | 3 +- .../app/shared/components/popover.service.ts | 9 ++- ui-ngx/src/app/shared/shared.module.ts | 16 ++-- 15 files changed, 333 insertions(+), 67 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/color-picker/hex-input.component.html create mode 100644 ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss create mode 100644 ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index ad772ba861..706db50677 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -189,13 +189,13 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro this.popoverService.hidePopover(trigger); } else { const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, + this.viewContainerRef, ColorPickerPanelComponent, ['left'], true, null, { color: this.colorFormGroup.get('color').value, - colorClearButton: this.colorClearButton + colorClearButton: this.colorClearButton, + colorCancelButton: true }, - {}, - {}, {}, true); + {}, {}, {}, false, () => {}, {padding: '12px 4px 12px 12px'}); colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { colorPickerPopover.hide(); diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html index 40e6653867..6ddae212e1 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html @@ -16,8 +16,7 @@ -->
-
color.color
- +
- + +
+ + +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss index d25f6b9d85..fc71d69822 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss @@ -13,11 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../scss/constants"; + .tb-color-picker-panel { - width: 328px; + width: 342px; display: flex; flex-direction: column; - gap: 16px; + max-height: calc(100vh - 24px); + min-height: 100%; + @media #{$mat-sm} { + width: 578px; + } .tb-color-picker-title { font-size: 16px; font-weight: 500; @@ -25,12 +31,17 @@ letter-spacing: 0.25px; color: rgba(0, 0, 0, 0.87); } + .tb-color-picker { + padding-right: 4px; + overflow-y: scroll; + } .tb-color-picker-panel-buttons { height: 60px; + padding: 8px 8px 0 0; display: flex; flex-direction: row; gap: 16px; - justify-content: flex-end; + justify-content: space-between; align-items: flex-end; } } diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts index 8ed2d304ae..443dd6967f 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts @@ -38,12 +38,19 @@ export class ColorPickerPanelComponent extends PageComponent implements OnInit { @coerceBoolean() colorClearButton = false; + @Input() + @coerceBoolean() + colorCancelButton = false; + @Input() popover: TbPopoverComponent; @Output() colorSelected = new EventEmitter(); + @Output() + colorCancelDialog = new EventEmitter(); + colorPickerControl: UntypedFormControl; constructor(protected store: Store) { @@ -61,4 +68,11 @@ export class ColorPickerPanelComponent extends PageComponent implements OnInit { clearColor() { this.colorSelected.emit(null); } -} + + cancelColor() { + if (this.popover) { + this.popover.hide(); + } else { + this.colorCancelDialog.emit(); + } + }} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index 7940f1ec73..888c5b0b0b 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -15,35 +15,38 @@ limitations under the License. --> - - -
- - -
- - -
-
- -
- - HEX - RGBA - HSLA - -
- - - +
+ + +
+
+ + +
+ + +
+
+ +
+ + HEX + RGBA + HSLA + +
+ + + +
+
- +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index 9aca580994..ef22ac203f 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../scss/constants"; + :host { width: 100%; display: flex; @@ -20,10 +22,34 @@ gap: 32px; overflow: auto; + .color-input-container { + display: flex; + flex-direction: column; + gap: 32px; + @media #{$mat-sm} { + flex-direction: row; + gap: 12px; + } + } + + .control-input-container { + display: flex; + flex-direction: column; + gap: 32px; + @media #{$mat-sm} { + width: 100%; + justify-content: center; + } + } + .saturation-component { + width: 100%; height: 238px; - min-height: 80px; + min-height: 160px; border-radius: 8px; + @media #{$mat-sm} { + max-height: 160px; + } } .control-component { @@ -54,7 +80,9 @@ } .color-input-block { + height: 56px; display: flex; + align-items: center; gap: 20px; .presentation-select { @@ -74,8 +102,13 @@ .color-presets-block { .color-presets-component { display: flex; - flex-direction: column; - gap: 12px; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + gap: 8px; + @media #{$mat-xs} { + flex-direction: column; + } } } } @@ -111,13 +144,21 @@ .color-presets-component { .presets-row { - gap: 10px; + gap: 8px; justify-content: space-between; } color-preset { height: 20px; width: 20px; border-radius: 4px; + @media #{$mat-xs} { + height: 40px; + width: 48px; + } + @media #{$mat-sm} { + height: 40px; + width: 40px; + } } } } diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts index 5cca12a968..100d1177cf 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts @@ -31,8 +31,8 @@ export enum ColorType { } const colorPresetsHex = - ['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63', - '#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1']; + ['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63', '#6F113A', + '#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1', '#BC91A4']; @Component({ selector: `tb-color-picker`, diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html new file mode 100644 index 0000000000..c3faa497ce --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html @@ -0,0 +1,36 @@ + +
+ + {{prefixValue}} + + + + + + + % + +
diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss new file mode 100644 index 0000000000..0f5125874c --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss @@ -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. + */ + +:host { + .hex-input-container { + display: flex; + gap: 8px; + } + .hex-input { + max-width: 190px; + } + .alpha-input { + min-width: 60px; + max-width: 60px; + } + + ::ng-deep { + .mdc-text-field--filled, .mat-mdc-form-field-focus-overlay { + &:before { + background-color: transparent !important; + } + } + .mat-mdc-form-field-icon-prefix, .mdc-line-ripple, .copy-button { + opacity: 0.4; + } + .alpha-input { + .mat-mdc-text-field-wrapper { + padding-left: 0; + } + } + + } +} + + diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts new file mode 100644 index 0000000000..fcd91a2397 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { Color } from '@iplab/ngx-color-picker'; + +@Component({ + selector: `tb-hex-input`, + templateUrl: `./hex-input.component.html`, + styleUrls: ['./hex-input.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HexInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + public labelVisible = false; + + @Input() + public prefixValue = '#'; + + public get value() { + return this.color ? this.color.toHexString(this.color.getRgba().alpha < 1).replace('#', '') : ''; + } + + public get copyColor() { + return this.prefixValue + this.value; + } + + public get hueValue(): string { + return this.color ? Math.round(this.color.getRgba().alpha * 100).toString() : ''; + } + + public onHueInputChange(event: KeyboardEvent, inputValue: string): void { + const color = this.color.getRgba(); + const alpha = +inputValue / 100; + if (color.getAlpha() !== alpha) { + const newColor = new Color().setRgba(color.red, color.green, color.blue, alpha).toHexString(true); + this.colorChange.emit(new Color(newColor)); + } + } + + public onInputChange(event: KeyboardEvent, inputValue: string): void { + const value = inputValue.toLowerCase(); + if ( + ((event.keyCode === 13 || event.key.toLowerCase() === 'enter') && value.length === 3) + || value.length === 6 || value.length === 8 + ) { + const hex = parseInt(value, 16); + const hexStr = hex.toString(16); + if (hexStr.padStart(value.length, '0') === value && this.value !== value) { + const newColor = new Color(`#${value}`); + this.colorChange.emit(newColor); + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index 3d37d83c15..35b560bb3a 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -16,14 +16,10 @@ -->
-
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss index 2a4ec55f7d..e5b24d78e1 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss @@ -13,10 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../scss/constants"; + :host { .tb-close-button { position: absolute; top: 6px; right: 6px; } + ::ng-deep { + .mat-mdc-dialog-content { + max-height: 100%; + overflow: hidden; + padding: 12px 4px 12px 12px !important; + } + .tb-color-picker-panel { + @media #{$mat-sm} { + width: 342px; + .color-input-container { + flex-direction: column !important; + } + .color-presets-component color-preset { + width: 48px; + } + } + + @media #{$mat-xs} { + width: 100%; + .hex-input { + flex: 1; + max-width: 100% !important; + } + } + } + } } diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index de2c328204..f0aa4981af 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -340,7 +340,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {