diff --git a/application/pom.xml b/application/pom.xml index f81ae293d2..c2ae7eccc2 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard application @@ -46,10 +46,6 @@ - - de.ruedigermoeller - fst - io.netty netty-transport-native-epoll diff --git a/application/src/main/data/json/demo/rule_chains/root_rule_chain.json b/application/src/main/data/json/demo/rule_chains/root_rule_chain.json index 9805c6f996..97ad46c756 100644 --- a/application/src/main/data/json/demo/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/demo/rule_chains/root_rule_chain.json @@ -43,7 +43,8 @@ "name": "Save Client Attributes", "debugMode": false, "configuration": { - "scope": "CLIENT_SCOPE" + "scope": "CLIENT_SCOPE", + "notifyDevice": "false" } }, { diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 26292c60b3..22c9d20a23 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -166,7 +166,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" } } ] diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json new file mode 100644 index 0000000000..3d076ff812 --- /dev/null +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -0,0 +1,135 @@ +{ + "ruleChain": { + "additionalInfo": { + "description": "" + }, + "name": "Device Profile Rule Chain Template", + "firstRuleNodeId": null, + "root": false, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 6, + "nodes": [ + { + "additionalInfo": { + "layoutX": 822, + "layoutY": 294 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 221 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 494, + "layoutY": 309 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 383 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 444 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 822, + "layoutY": 507 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "description": "", + "layoutX": 209, + "layoutY": 307 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false + } + } + ], + "connections": [ + { + "fromIndex": 2, + "toIndex": 4, + "type": "Other" + }, + { + "fromIndex": 2, + "toIndex": 1, + "type": "Post attributes" + }, + { + "fromIndex": 2, + "toIndex": 0, + "type": "Post telemetry" + }, + { + "fromIndex": 2, + "toIndex": 3, + "type": "RPC Request from Device" + }, + { + "fromIndex": 2, + "toIndex": 5, + "type": "RPC Request to Device" + }, + { + "fromIndex": 6, + "toIndex": 2, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index 59b7021aa7..a37b1fc865 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -8,7 +8,7 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 2, + "firstNodeIndex": 6, "nodes": [ { "additionalInfo": { @@ -31,7 +31,8 @@ "name": "Save Client Attributes", "debugMode": false, "configuration": { - "scope": "CLIENT_SCOPE" + "scope": "CLIENT_SCOPE", + "notifyDevice": "false" } }, { @@ -81,9 +82,28 @@ "configuration": { "timeoutInSeconds": 60 } + }, + { + "additionalInfo": { + "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", + "layoutX": 204, + "layoutY": 240 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } } ], "connections": [ + { + "fromIndex": 6, + "toIndex": 2, + "type": "Success" + }, { "fromIndex": 2, "toIndex": 4, diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql index 0916c241a1..41e1cfbb7a 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql @@ -64,6 +64,7 @@ BEGIN AND tablename like 'ts_kv_' || '%' AND tablename != 'ts_kv_latest' AND tablename != 'ts_kv_dictionary' + AND tablename != 'ts_kv_indefinite' LOOP IF partition != partition_by_max_ttl_date THEN IF partition_year IS NOT NULL THEN diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql index 5e20e1c664..7de74032ce 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql @@ -59,8 +59,8 @@ $$ DECLARE tenant_cursor CURSOR FOR select tenant.id as tenant_id from tenant; - tenant_id_record varchar; - customer_id_record varchar; + tenant_id_record uuid; + customer_id_record uuid; tenant_ttl bigint; customer_ttl bigint; deleted_for_entities bigint; diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_after.sql b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql new file mode 100644 index 0000000000..c8f9d2970e --- /dev/null +++ b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql @@ -0,0 +1,28 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +DROP PROCEDURE IF EXISTS update_tenant_profiles; +DROP PROCEDURE IF EXISTS update_device_profiles; + +ALTER TABLE tenant ALTER COLUMN tenant_profile_id SET NOT NULL; +ALTER TABLE tenant DROP CONSTRAINT IF EXISTS fk_tenant_profile; +ALTER TABLE tenant ADD CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id); +ALTER TABLE tenant DROP COLUMN IF EXISTS isolated_tb_core; +ALTER TABLE tenant DROP COLUMN IF EXISTS isolated_tb_rule_engine; + +ALTER TABLE device ALTER COLUMN device_profile_id SET NOT NULL; +ALTER TABLE device DROP CONSTRAINT IF EXISTS fk_device_profile; +ALTER TABLE device ADD CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id); diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql new file mode 100644 index 0000000000..c1591e7831 --- /dev/null +++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql @@ -0,0 +1,81 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS device_profile ( + id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + type varchar(255), + transport_type varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) +); + +CREATE TABLE IF NOT EXISTS tenant_profile ( + id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + isolated_tb_core boolean, + isolated_tb_rule_engine boolean, + CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) +); + +CREATE OR REPLACE PROCEDURE update_tenant_profiles() + LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = false AND isolated_tb_rule_engine = false) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = false AND t.isolated_tb_rule_engine = false; + + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = true AND isolated_tb_rule_engine = false) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = true AND t.isolated_tb_rule_engine = false; + + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = false AND isolated_tb_rule_engine = true) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = false AND t.isolated_tb_rule_engine = true; + + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = true AND isolated_tb_rule_engine = true) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = true AND t.isolated_tb_rule_engine = true; +END; +$$; + +CREATE OR REPLACE PROCEDURE update_device_profiles() + LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE device as d SET device_profile_id = p.id, device_data = '{"configuration":{"type":"DEFAULT"}, "transportConfiguration":{"type":"DEFAULT"}}' + FROM + (SELECT id, tenant_id, name from device_profile) as p + WHERE d.device_profile_id IS NULL AND p.tenant_id = d.tenant_id AND d.type = p.name; +END; +$$; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b8425ccfed..8f6a290af4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -32,6 +32,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.common.data.DataConstants; @@ -44,7 +45,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -58,17 +58,20 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.rule.RuleNodeStateService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.SharedEventLoopGroupService; import org.thingsboard.server.service.mail.MailExecutorService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; @@ -89,7 +92,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Component @@ -125,6 +127,10 @@ public class ActorSystemContext { @Getter private DeviceService deviceService; + @Autowired + @Getter + private TbDeviceProfileCache deviceProfileCache; + @Autowired @Getter private AssetService assetService; @@ -137,6 +143,10 @@ public class ActorSystemContext { @Getter private TenantService tenantService; + @Autowired + @Getter + private TenantProfileService tenantProfileService; + @Autowired @Getter private CustomerService customerService; @@ -149,6 +159,10 @@ public class ActorSystemContext { @Getter private RuleChainService ruleChainService; + @Autowired + @Getter + private RuleNodeStateService ruleNodeStateService; + @Autowired private PartitionService partitionService; @@ -527,4 +541,5 @@ public class ActorSystemContext { log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs); getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 953188f3f7..16beb3a045 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -27,6 +27,7 @@ import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.tenant.TenantActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; @@ -116,7 +117,9 @@ public class AppActor extends ContextAwareActor { boolean isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); boolean isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); for (Tenant tenant : tenantIterator) { - if (isCore || (isRuleEngine && !tenant.isIsolatedTbRuleEngine())) { + // TODO: Tenant Profile from cache + TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId()); + if (isCore || (isRuleEngine && !tenantProfile.isIsolatedTbRuleEngine())) { log.debug("[{}] Creating tenant actor", tenant.getId()); getOrCreateTenantActor(tenant.getId()); log.debug("[{}] Tenant actor created.", tenant.getId()); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index d20554357b..a9ddaea2e4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -23,6 +23,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -39,13 +40,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; @@ -67,7 +70,6 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import java.util.Collections; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -105,6 +107,7 @@ class DefaultTbContext implements TbContext { if (nodeCtx.getSelf().isDebugMode()) { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th)); } + msg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId()); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationTypes, msg, th != null ? th.getMessage() : null)); } @@ -122,7 +125,7 @@ class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, String queueName, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); enqueue(tpi, tbMsg, onFailure, onSuccess); } @@ -139,46 +142,54 @@ class DefaultTbContext implements TbContext { @Override public void enqueueForTellFailure(TbMsg tbMsg, String failureMessage) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg); enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null); } @Override public void enqueueForTellNext(TbMsg tbMsg, String relationType) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg); enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, null, null); } @Override public void enqueueForTellNext(TbMsg tbMsg, Set relationTypes) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg); enqueueForTellNext(tpi, tbMsg, relationTypes, null, null, null); } @Override public void enqueueForTellNext(TbMsg tbMsg, String relationType, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg); enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure); } @Override public void enqueueForTellNext(TbMsg tbMsg, Set relationTypes, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg); enqueueForTellNext(tpi, tbMsg, relationTypes, null, onSuccess, onFailure); } @Override public void enqueueForTellNext(TbMsg tbMsg, String queueName, String relationType, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure); } @Override public void enqueueForTellNext(TbMsg tbMsg, String queueName, Set relationTypes, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); enqueueForTellNext(tpi, tbMsg, relationTypes, null, onSuccess, onFailure); } + private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) { + return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator()); + } + + private TopicPartitionInfo resolvePartition(TbMsg tbMsg) { + return resolvePartition(tbMsg, tbMsg.getQueueName()); + } + private void enqueueForTellNext(TopicPartitionInfo tpi, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId(); RuleNodeId ruleNodeId = nodeCtx.getSelf().getId(); @@ -203,6 +214,7 @@ class DefaultTbContext implements TbContext { if (nodeCtx.getSelf().isDebugMode()) { mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "ACK", null); } + tbMsg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId()); tbMsg.getCallback().onSuccess(); } @@ -388,6 +400,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getEntityViewService(); } + @Override + public RuleEngineDeviceProfileCache getDeviceProfileCache() { + return mainCtx.getDeviceProfileCache(); + } + @Override public EventLoopGroup getSharedEventLoop() { return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup(); @@ -422,6 +439,30 @@ class DefaultTbContext implements TbContext { return mainCtx.getRedisTemplate(); } + @Override + public PageData findRuleNodeStates(PageLink pageLink) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}] Fetch Rule Node States.", getTenantId(), getSelfId()); + } + return mainCtx.getRuleNodeStateService().findByRuleNodeId(getTenantId(), getSelfId(), pageLink); + } + + @Override + public RuleNodeState findRuleNodeStateForEntity(EntityId entityId) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}][{}] Fetch Rule Node State for entity.", getTenantId(), getSelfId(), entityId); + } + return mainCtx.getRuleNodeStateService().findByRuleNodeIdAndEntityId(getTenantId(), getSelfId(), entityId); + } + + @Override + public RuleNodeState saveRuleNodeState(RuleNodeState state) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}][{}] Persist Rule Node State for entity: {}", getTenantId(), getSelfId(), state.getEntityId(), state.getStateData()); + } + state.setRuleNodeId(getSelfId()); + return mainCtx.getRuleNodeStateService().save(getTenantId(), state); + } private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { TbMsgMetaData metaData = new TbMsgMetaData(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java index 7583ea553e..db55ff8edf 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java @@ -103,7 +103,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant(); + // TODO: Tenant Profile from cache + + TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(tenantId, tenant.getTenantProfileId()); + isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); if (isRuleEngineForCurrentTenant) { try { - if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenant.isIsolatedTbRuleEngine())) { + if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) { log.info("[{}] Going to init rule chains", tenantId); initRuleChains(); } else { @@ -111,6 +117,9 @@ public class TenantActor extends RuleChainManagerActor { if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) { QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg; queueMsg.getTbMsg().getCallback().onSuccess(); + } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)){ + TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg; + transportMsg.getCallback().onSuccess(); } return true; } diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index f692d8dd60..4d063d07f1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -90,7 +90,7 @@ public class AlarmController extends BaseController { checkEntity(alarm.getId(), alarm, Resource.ALARM); Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm)); - logEntityAction(savedAlarm.getId(), savedAlarm, + logEntityAction(savedAlarm.getOriginator(), savedAlarm, getCurrentUser().getCustomerId(), alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); return savedAlarm; @@ -126,7 +126,7 @@ public class AlarmController extends BaseController { long ackTs = System.currentTimeMillis(); alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get(); alarm.setAckTs(ackTs); - logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); + logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); } catch (Exception e) { throw handleException(e); } @@ -143,7 +143,7 @@ public class AlarmController extends BaseController { long clearTs = System.currentTimeMillis(); alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get(); alarm.setClearTs(clearTs); - logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); + logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 2c202e71b2..8434f67d1d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -33,12 +33,15 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -52,12 +55,14 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; @@ -82,6 +87,7 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -89,6 +95,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; @@ -98,6 +105,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; @@ -134,6 +142,9 @@ public abstract class BaseController { @Autowired protected TenantService tenantService; + @Autowired + protected TenantProfileService tenantProfileService; + @Autowired protected CustomerService customerService; @@ -143,6 +154,9 @@ public abstract class BaseController { @Autowired protected DeviceService deviceService; + @Autowired + protected DeviceProfileService deviceProfileService; + @Autowired protected AssetService assetService; @@ -197,6 +211,9 @@ public abstract class BaseController { @Autowired protected TbQueueProducerProvider producerProvider; + @Autowired + protected TbDeviceProfileCache deviceProfileCache; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -312,6 +329,30 @@ public abstract class BaseController { } } + TenantInfo checkTenantInfoId(TenantId tenantId, Operation operation) throws ThingsboardException { + try { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + TenantInfo tenant = tenantService.findTenantInfoById(tenantId); + checkNotNull(tenant); + accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant); + return tenant; + } catch (Exception e) { + throw handleException(e, false); + } + } + + TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException { + try { + validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId); + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(getTenantId(), tenantProfileId); + checkNotNull(tenantProfile); + accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, operation); + return tenantProfile; + } catch (Exception e) { + throw handleException(e, false); + } + } + protected TenantId getTenantId() throws ThingsboardException { return getCurrentUser().getTenantId(); } @@ -360,12 +401,18 @@ public abstract class BaseController { case DEVICE: checkDeviceId(new DeviceId(entityId.getId()), operation); return; + case DEVICE_PROFILE: + checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation); + return; case CUSTOMER: checkCustomerId(new CustomerId(entityId.getId()), operation); return; case TENANT: checkTenantId(new TenantId(entityId.getId()), operation); return; + case TENANT_PROFILE: + checkTenantProfileId(new TenantProfileId(entityId.getId()), operation); + return; case RULE_CHAIN: checkRuleChain(new RuleChainId(entityId.getId()), operation); return; @@ -422,6 +469,18 @@ public abstract class BaseController { } } + DeviceProfile checkDeviceProfileId(DeviceProfileId deviceProfileId, Operation operation) throws ThingsboardException { + try { + validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId); + checkNotNull(deviceProfile); + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile); + return deviceProfile; + } catch (Exception e) { + throw handleException(e, false); + } + } + protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException { try { validateId(entityViewId, "Incorrect entityViewId " + entityViewId); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index d0a662d2ec..c8fde63c4d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -315,6 +316,7 @@ public class DeviceController extends BaseController { @RequestParam int pageSize, @RequestParam int page, @RequestParam(required = false) String type, + @RequestParam(required = false) String deviceProfileId, @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortOrder) throws ThingsboardException { @@ -323,6 +325,9 @@ public class DeviceController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink)); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink)); } else { return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink)); } @@ -379,6 +384,7 @@ public class DeviceController extends BaseController { @RequestParam int pageSize, @RequestParam int page, @RequestParam(required = false) String type, + @RequestParam(required = false) String deviceProfileId, @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortOrder) throws ThingsboardException { @@ -390,6 +396,9 @@ public class DeviceController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink)); } else { return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java new file mode 100644 index 0000000000..b474a0c0f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -0,0 +1,203 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class DeviceProfileController extends BaseController { + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET) + @ResponseBody + public DeviceProfile getDeviceProfileById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + return checkDeviceProfileId(deviceProfileId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET) + @ResponseBody + public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfileInfo/default", method = RequestMethod.GET) + @ResponseBody + public DeviceProfileInfo getDefaultDeviceProfileInfo() throws ThingsboardException { + try { + return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST) + @ResponseBody + public DeviceProfile saveDeviceProfile(@RequestBody DeviceProfile deviceProfile) throws ThingsboardException { + try { + boolean created = deviceProfile.getId() == null; + deviceProfile.setTenantId(getTenantId()); + + checkEntity(deviceProfile.getId(), deviceProfile, Resource.DEVICE_PROFILE); + + DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile)); + + deviceProfileCache.put(savedDeviceProfile); + tbClusterService.onDeviceProfileChange(savedDeviceProfile, null); + tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(), + created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + + logEntityAction(savedDeviceProfile.getId(), savedDeviceProfile, + null, + savedDeviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + return savedDeviceProfile; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile, + null, deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE); + deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId); + deviceProfileCache.evict(deviceProfileId); + + tbClusterService.onDeviceProfileDelete(deviceProfile, null); + tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED); + + logEntityAction(deviceProfileId, deviceProfile, + null, + ActionType.DELETED, null, strDeviceProfileId); + + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE_PROFILE), + null, + null, + ActionType.DELETED, e, strDeviceProfileId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST) + @ResponseBody + public DeviceProfile setDefaultDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE); + DeviceProfile previousDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(getTenantId()); + if (deviceProfileService.setDefaultDeviceProfile(getTenantId(), deviceProfileId)) { + if (previousDefaultDeviceProfile != null) { + previousDefaultDeviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), previousDefaultDeviceProfile.getId()); + + logEntityAction(previousDefaultDeviceProfile.getId(), previousDefaultDeviceProfile, + null, ActionType.UPDATED, null); + } + deviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), deviceProfileId); + + logEntityAction(deviceProfile.getId(), deviceProfile, + null, ActionType.UPDATED, null); + } + return deviceProfile; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE_PROFILE), + null, + null, + ActionType.UPDATED, e, strDeviceProfileId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getDeviceProfiles(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getDeviceProfileInfos(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 7e8291356d..5c54c0232e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -47,7 +48,10 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; @@ -55,6 +59,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.script.JsInvokeService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.security.permission.Operation; @@ -77,6 +82,9 @@ public class RuleChainController extends BaseController { private static final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private InstallScripts installScripts; + @Autowired private EventService eventService; @@ -146,6 +154,27 @@ public class RuleChainController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) + @ResponseBody + public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException { + try { + checkNotNull(request); + checkNotNull(request.getName()); + + RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); + + logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); + + return savedRuleChain; + } catch (Exception e) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(request.getName()); + logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain, null, ActionType.ADDED, e); + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) @ResponseBody @@ -360,6 +389,36 @@ public class RuleChainController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public RuleChainData exportRuleChains(@RequestParam("limit") int limit) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = new PageLink(limit); + return checkNotNull(ruleChainService.exportTenantRuleChains(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) + @ResponseBody + public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); + if (!CollectionUtils.isEmpty(importResults)) { + for (RuleChainImportResult importResult : importResults) { + tbClusterService.onEntityStateChange(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent()); + } + } + } catch (Exception e) { + throw handleException(e); + } + } + private String msgToOutput(TbMsg msg) throws Exception { ObjectNode msgData = objectMapper.createObjectNode(); if (!StringUtils.isEmpty(msg.getData())) { diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index f61565d320..c8c41ea479 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -197,19 +197,21 @@ public class TelemetryController extends BaseController { @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"}) @ResponseBody public DeferredResult getTimeseries( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, + @PathVariable("entityType") String entityType, + @PathVariable("entityId") String entityIdStr, @RequestParam(name = "keys") String keys, @RequestParam(name = "startTs") Long startTs, @RequestParam(name = "endTs") Long endTs, @RequestParam(name = "interval", defaultValue = "0") Long interval, @RequestParam(name = "limit", defaultValue = "100") Integer limit, @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, + @RequestParam(name= "orderBy", defaultValue = "DESC") String orderBy, @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, (result, tenantId, entityId) -> { // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); - List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg)) + List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy)) .collect(Collectors.toList()); Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 59eea87f51..ebb46778c9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -28,6 +28,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -58,8 +59,20 @@ public class TenantController extends BaseController { checkParameter("tenantId", strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); - checkTenantId(tenantId, Operation.READ); - return checkNotNull(tenantService.findTenantById(tenantId)); + return checkTenantId(tenantId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET) + @ResponseBody + public TenantInfo getTenantInfoById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException { + checkParameter("tenantId", strTenantId); + try { + TenantId tenantId = new TenantId(toUUID(strTenantId)); + return checkTenantInfoId(tenantId, Operation.READ); } catch (Exception e) { throw handleException(e); } @@ -115,4 +128,20 @@ public class TenantController extends BaseController { } } + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantInfos(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantService.findTenantInfos(pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java new file mode 100644 index 0000000000..dfe0b19a42 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.exception.ThingsboardException; +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.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class TenantProfileController extends BaseController { + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET) + @ResponseBody + public TenantProfile getTenantProfileById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + return checkTenantProfileId(tenantProfileId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET) + @ResponseBody + public EntityInfo getTenantProfileInfoById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + return checkNotNull(tenantProfileService.findTenantProfileInfoById(getTenantId(), tenantProfileId)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET) + @ResponseBody + public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException { + try { + return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId())); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) + @ResponseBody + public TenantProfile saveTenantProfile(@RequestBody TenantProfile tenantProfile) throws ThingsboardException { + try { + boolean newTenantProfile = tenantProfile.getId() == null; + if (newTenantProfile) { + accessControlService + .checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); + } else { + checkEntityId(tenantProfile.getId(), Operation.WRITE); + } + + tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile)); + return tenantProfile; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + checkTenantProfileId(tenantProfileId, Operation.DELETE); + tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST) + @ResponseBody + public TenantProfile setDefaultTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE); + tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId); + return tenantProfile; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantProfiles(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantProfileInfos(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } +} 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 de7b2bdce3..7bcf38080e 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -175,6 +175,13 @@ public class ThingsboardInstallService { case "3.1.0": log.info("Upgrading ThingsBoard from version 3.1.0 to 3.1.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.1.0"); + case "3.1.1": + log.info("Upgrading ThingsBoard from version 3.1.1 to 3.2.0 ..."); + if (databaseTsUpgradeService != null) { + databaseTsUpgradeService.upgradeDatabase("3.1.1"); + } + databaseEntitiesUpgradeService.upgradeDatabase("3.1.1"); + dataUpdateService.updateData("3.1.1"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; @@ -206,6 +213,7 @@ public class ThingsboardInstallService { componentDiscoveryService.discoverComponents(); systemDataLoaderService.createSysAdmin(); + systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createAdminSettings(); systemDataLoaderService.loadSystemWidgets(); // systemDataLoaderService.loadSystemPlugins(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java index 58180583ef..17857e2807 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java @@ -49,6 +49,7 @@ public class CassandraTsDatabaseUpgradeService extends AbstractCassandraDatabase log.info("Schema updated."); break; case "2.5.0": + case "3.1.1": break; default: throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); 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 6aa3e0cd2d..bb875ec5c4 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 @@ -27,11 +27,15 @@ import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.TenantProfileData; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -46,9 +50,12 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -82,6 +89,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private TenantService tenantService; + @Autowired + private TenantProfileService tenantProfileService; + @Autowired private CustomerService customerService; @@ -94,6 +104,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private DeviceService deviceService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired private AttributesService attributesService; @@ -110,6 +123,50 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { createUser(Authority.SYS_ADMIN, null, null, "sysadmin@thingsboard.org", "sysadmin"); } + @Override + public void createDefaultTenantProfiles() throws Exception { + tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID); + + TenantProfile isolatedTbCoreProfile = new TenantProfile(); + isolatedTbCoreProfile.setDefault(false); + isolatedTbCoreProfile.setName("Isolated TB Core"); + isolatedTbCoreProfile.setProfileData(new TenantProfileData()); + isolatedTbCoreProfile.setDescription("Isolated TB Core tenant profile"); + isolatedTbCoreProfile.setIsolatedTbCore(true); + isolatedTbCoreProfile.setIsolatedTbRuleEngine(false); + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreProfile); + } catch (DataValidationException e) { + log.warn(e.getMessage()); + } + + TenantProfile isolatedTbRuleEngineProfile = new TenantProfile(); + isolatedTbRuleEngineProfile.setDefault(false); + isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine"); + isolatedTbRuleEngineProfile.setProfileData(new TenantProfileData()); + isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile"); + isolatedTbRuleEngineProfile.setIsolatedTbCore(false); + isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true); + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbRuleEngineProfile); + } catch (DataValidationException e) { + log.warn(e.getMessage()); + } + + TenantProfile isolatedTbCoreAndTbRuleEngineProfile = new TenantProfile(); + isolatedTbCoreAndTbRuleEngineProfile.setDefault(false); + isolatedTbCoreAndTbRuleEngineProfile.setName("Isolated TB Core and TB Rule Engine"); + isolatedTbCoreAndTbRuleEngineProfile.setProfileData(new TenantProfileData()); + isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile"); + isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true); + isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true); + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreAndTbRuleEngineProfile); + } catch (DataValidationException e) { + log.warn(e.getMessage()); + } + } + @Override public void createAdminSettings() throws Exception { AdminSettings generalSettings = new AdminSettings(); @@ -162,16 +219,18 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { createUser(Authority.CUSTOMER_USER, demoTenant.getId(), customerB.getId(), "customerB@thingsboard.org", CUSTOMER_CRED); createUser(Authority.CUSTOMER_USER, demoTenant.getId(), customerC.getId(), "customerC@thingsboard.org", CUSTOMER_CRED); - createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A1", "A1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A2", "A2_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A3", "A3_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerB.getId(), DEFAULT_DEVICE_TYPE, "Test Device B1", "B1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerC.getId(), DEFAULT_DEVICE_TYPE, "Test Device C1", "C1_TEST_TOKEN", null); + DeviceProfile defaultDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), DEFAULT_DEVICE_TYPE); + + createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A1", "A1_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A2", "A2_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A3", "A3_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerB.getId(), defaultDeviceProfile.getId(), "Test Device B1", "B1_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerC.getId(), defaultDeviceProfile.getId(), "Test Device C1", "C1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " + + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " + "applications that upload data from DHT11 temperature and humidity sensor"); - createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + "Raspberry Pi GPIO control sample application"); Asset thermostatAlarms = new Asset(); @@ -180,8 +239,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { thermostatAlarms.setType("AlarmPropagationAsset"); thermostatAlarms = assetService.saveAsset(thermostatAlarms); - DeviceId t1Id = createDevice(demoTenant.getId(), null, "thermostat", "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); - DeviceId t2Id = createDevice(demoTenant.getId(), null, "thermostat", "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); + DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat"); + + DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); + DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset")); relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t2Id, "ToAlarmPropagationAsset")); @@ -257,14 +318,14 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private Device createDevice(TenantId tenantId, CustomerId customerId, - String type, + DeviceProfileId deviceProfileId, String name, String accessToken, String description) { Device device = new Device(); device.setTenantId(tenantId); device.setCustomerId(customerId); - device.setType(type); + device.setDeviceProfileId(deviceProfileId); device.setName(name); if (description != null) { ObjectNode additionalInfo = objectMapper.createObjectNode(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 5181834b80..cd74753f31 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -57,6 +57,7 @@ public class InstallScripts { public static final String JSON_DIR = "json"; public static final String SYSTEM_DIR = "system"; public static final String TENANT_DIR = "tenant"; + public static final String DEVICE_PROFILE_DIR = "device_profile"; public static final String DEMO_DIR = "demo"; public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; @@ -83,6 +84,10 @@ public class InstallScripts { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } + public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { + return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -110,15 +115,7 @@ public class InstallScripts { dirStream.forEach( path -> { try { - JsonNode ruleChainJson = objectMapper.readTree(path.toFile()); - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); - - ruleChain.setTenantId(tenantId); - ruleChain = ruleChainService.saveRuleChain(ruleChain); - - ruleChainMetaData.setRuleChainId(ruleChain.getId()); - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + createRuleChainFromFile(tenantId, path, null); } catch (Exception e) { log.error("Unable to load rule chain from json: [{}]", path.toString()); throw new RuntimeException("Unable to load rule chain from json", e); @@ -128,6 +125,28 @@ public class InstallScripts { } } + public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException { + return createRuleChainFromFile(tenantId, getDeviceProfileDefaultRuleChainTemplateFilePath(), ruleChainName); + } + + public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePath, String newRuleChainName) throws IOException { + JsonNode ruleChainJson = objectMapper.readTree(templateFilePath.toFile()); + RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); + + ruleChain.setTenantId(tenantId); + if (!StringUtils.isEmpty(newRuleChainName)) { + ruleChain.setName(newRuleChainName); + } + ruleChain = ruleChainService.saveRuleChain(ruleChain); + + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + + return ruleChain; + } + + public void loadSystemWidgets() throws Exception { Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java index 7a8174af16..18433c6f53 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java @@ -195,6 +195,14 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005001"); } break; + case "3.1.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Load TTL functions ..."); + loadSql(conn, LOAD_TTL_FUNCTIONS_SQL); + log.info("Load Drop Partitions functions ..."); + loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } @@ -239,4 +247,4 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 8cf471ab38..d6068b9616 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -20,7 +20,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; 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.tenant.TenantService; import org.thingsboard.server.service.install.sql.SqlDbHelper; import java.nio.charset.Charset; @@ -34,6 +41,7 @@ import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.List; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; @@ -76,6 +84,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @Autowired private InstallScripts installScripts; + @Autowired + private SystemDataLoaderService systemDataLoaderService; + + @Autowired + private TenantService tenantService; + + @Autowired + private DeviceService deviceService; + + @Autowired + private DeviceProfileService deviceProfileService; + + @Override public void upgradeDatabase(String fromVersion) throws Exception { switch (fromVersion) { @@ -303,6 +324,77 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Schema updated."); } break; + case "3.1.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3001000)) { + + try { + conn.createStatement().execute("ALTER TABLE device ADD COLUMN device_profile_id uuid, ADD COLUMN device_data jsonb"); + } catch (Exception e) { + } + + try { + conn.createStatement().execute("ALTER TABLE tenant ADD COLUMN tenant_profile_id uuid"); + } catch (Exception e) { + } + + try { + conn.createStatement().execute("CREATE TABLE IF NOT EXISTS rule_node_state (" + + " id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY," + + " created_time bigint NOT NULL," + + " rule_node_id uuid NOT NULL," + + " entity_type varchar(32) NOT NULL," + + " entity_id uuid NOT NULL," + + " state_data varchar(16384) NOT NULL," + + " CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id)," + + " CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE)"); + } catch (Exception e) { + } + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_before.sql"); + loadSql(schemaUpdateFile, conn); + + log.info("Creating default tenant profiles..."); + systemDataLoaderService.createDefaultTenantProfiles(); + + log.info("Updating tenant profiles..."); + conn.createStatement().execute("call update_tenant_profiles()"); + + log.info("Creating default device profiles..."); + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = tenantService.findTenants(pageLink); + for (Tenant tenant : pageData.getData()) { + List deviceTypes = deviceService.findDeviceTypesByTenantId(tenant.getId()).get(); + try { + deviceProfileService.createDefaultDeviceProfile(tenant.getId()); + } catch (Exception e) { + } + for (EntitySubtype deviceType : deviceTypes) { + try { + deviceProfileService.findOrCreateDeviceProfile(tenant.getId(), deviceType.getType()); + } catch (Exception e) { + } + } + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + + log.info("Updating device profiles..."); + conn.createStatement().execute("call update_device_profiles()"); + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_after.sql"); + loadSql(schemaUpdateFile, conn); + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002000;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } 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 76e65deaa4..b588c2dff2 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 @@ -19,6 +19,8 @@ public interface SystemDataLoaderService { void createSysAdmin() throws Exception; + void createDefaultTenantProfiles() throws Exception; + void createAdminSettings() throws Exception; void loadSystemWidgets() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index d8f7ea61f9..756356581a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -177,6 +177,8 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005001"); } break; + case "3.1.1": + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } @@ -207,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index f0fbfb3448..bc86857c4d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.install.update; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -23,9 +25,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; +import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; @@ -35,10 +41,13 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.service.install.InstallScripts; import javax.annotation.Nullable; @@ -49,6 +58,7 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.apache.commons.lang.StringUtils.isBlank; +import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; @Service @Profile("install") @@ -81,6 +91,10 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Updating data from version 3.0.1 to 3.1.0 ..."); tenantsEntityViewsUpdater.updateEntities(null); break; + case "3.1.1": + log.info("Updating data from version 3.1.1 to 3.2.0 ..."); + tenantsRootRuleChainUpdater.updateEntities(null); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } @@ -107,6 +121,60 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; + private PaginatedUpdater tenantsRootRuleChainUpdater = + new PaginatedUpdater() { + + @Override + protected PageData findEntities(String region, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + try { + RuleChain ruleChain = ruleChainService.getRootTenantRuleChain(tenant.getId()); + if (ruleChain == null) { + installScripts.createDefaultRuleChains(tenant.getId()); + } else { + RuleChainMetaData md = ruleChainService.loadRuleChainMetaData(tenant.getId(), ruleChain.getId()); + int oldIdx = md.getFirstNodeIndex(); + int newIdx = md.getNodes().size(); + + if (md.getNodes().size() < oldIdx) { + // Skip invalid rule chains + return; + } + + RuleNode oldFirstNode = md.getNodes().get(oldIdx); + if (oldFirstNode.getType().equals(TbDeviceProfileNode.class.getName())) { + // No need to update the rule node twice. + return; + } + + RuleNode ruleNode = new RuleNode(); + ruleNode.setRuleChainId(ruleChain.getId()); + ruleNode.setName("Device Profile Node"); + ruleNode.setType(TbDeviceProfileNode.class.getName()); + ruleNode.setDebugMode(false); + TbDeviceProfileNodeConfiguration ruleNodeConfiguration = new TbDeviceProfileNodeConfiguration().defaultConfiguration(); + ruleNode.setConfiguration(JacksonUtil.valueToTree(ruleNodeConfiguration)); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put("description", "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type."); + additionalInfo.put("layoutX", 204); + additionalInfo.put("layoutY", 240); + ruleNode.setAdditionalInfo(additionalInfo); + + md.getNodes().add(ruleNode); + md.setFirstNodeIndex(newIdx); + md.addConnectionInfo(newIdx, oldIdx, "Success"); + ruleChainService.saveRuleChainMetaData(tenant.getId(), md); + } + } catch (Exception e) { + log.error("Unable to update Tenant", e); + } + } + }; + private PaginatedUpdater tenantsEntityViewsUpdater = new PaginatedUpdater() { @@ -121,30 +189,30 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; - private void updateTenantEntityViews(TenantId tenantId) { - PageLink pageLink = new PageLink(100); - PageData pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink); - boolean hasNext = true; - while (hasNext) { - List>> updateFutures = new ArrayList<>(); - for (EntityView entityView : pageData.getData()) { - updateFutures.add(updateEntityViewLatestTelemetry(entityView)); - } - - try { - Futures.allAsList(updateFutures).get(); - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to copy latest telemetry to entity view", e); - } - - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink); - } else { - hasNext = false; - } - } - } + private void updateTenantEntityViews(TenantId tenantId) { + PageLink pageLink = new PageLink(100); + PageData pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink); + boolean hasNext = true; + while (hasNext) { + List>> updateFutures = new ArrayList<>(); + for (EntityView entityView : pageData.getData()) { + updateFutures.add(updateEntityViewLatestTelemetry(entityView)); + } + + try { + Futures.allAsList(updateFutures).get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to copy latest telemetry to entity view", e); + } + + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink); + } else { + hasNext = false; + } + } + } private ListenableFuture> updateEntityViewLatestTelemetry(EntityView entityView) { EntityViewId entityId = entityView.getId(); @@ -160,13 +228,13 @@ public class DefaultDataUpdateService implements DataUpdateService { keysFuture = Futures.immediateFuture(keys); } ListenableFuture> latestFuture = Futures.transformAsync(keysFuture, fetchKeys -> { - List queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList()); - if (!queries.isEmpty()) { - return tsService.findAll(TenantId.SYS_TENANT_ID, entityView.getEntityId(), queries); - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + List queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList()); + if (!queries.isEmpty()) { + return tsService.findAll(TenantId.SYS_TENANT_ID, entityView.getEntityId(), queries); + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); return Futures.transformAsync(latestFuture, latestValues -> { if (latestValues != null && !latestValues.isEmpty()) { ListenableFuture> saveFuture = tsService.saveLatest(TenantId.SYS_TENANT_ID, entityId, latestValues); diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java new file mode 100644 index 0000000000..b0b47fe886 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.profile; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache { + + private final Lock deviceProfileFetchLock = new ReentrantLock(); + private final DeviceProfileService deviceProfileService; + private final DeviceService deviceService; + + private final ConcurrentMap deviceProfilesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap devicesMap = new ConcurrentHashMap<>(); + + public DefaultTbDeviceProfileCache(DeviceProfileService deviceProfileService, DeviceService deviceService) { + this.deviceProfileService = deviceProfileService; + this.deviceService = deviceService; + } + + @Override + public DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId) { + DeviceProfile profile = deviceProfilesMap.get(deviceProfileId); + if (profile == null) { + profile = deviceProfilesMap.get(deviceProfileId); + if (profile == null) { + deviceProfileFetchLock.lock(); + try { + profile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + if (profile != null) { + deviceProfilesMap.put(deviceProfileId, profile); + } + } finally { + deviceProfileFetchLock.unlock(); + } + } + } + return profile; + } + + @Override + public DeviceProfile get(TenantId tenantId, DeviceId deviceId) { + DeviceProfileId profileId = devicesMap.get(deviceId); + if (profileId == null) { + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + profileId = device.getDeviceProfileId(); + devicesMap.put(deviceId, profileId); + } + } + return get(tenantId, profileId); + } + + @Override + public void put(DeviceProfile profile) { + if (profile.getId() != null) { + deviceProfilesMap.put(profile.getId(), profile); + } + } + + @Override + public void evict(DeviceProfileId profileId) { + deviceProfilesMap.remove(profileId); + } + + @Override + public void evict(DeviceId deviceId) { + devicesMap.remove(deviceId); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java new file mode 100644 index 0000000000..ec19eb1da6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.profile; + +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; + +public interface TbDeviceProfileCache extends RuleEngineDeviceProfileCache { + + void put(DeviceProfile profile); + + void evict(DeviceProfileId id); + + void evict(DeviceId id); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 926b08f38b..28cd998e46 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -21,14 +21,20 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; @@ -40,7 +46,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import java.util.HashSet; @@ -64,11 +70,13 @@ public class DefaultTbClusterService implements TbClusterService { private final TbQueueProducerProvider producerProvider; private final PartitionService partitionService; private final DataDecodingEncodingService encodingService; + private final TbDeviceProfileCache deviceProfileCache; - public DefaultTbClusterService(TbQueueProducerProvider producerProvider, PartitionService partitionService, DataDecodingEncodingService encodingService) { + public DefaultTbClusterService(TbQueueProducerProvider producerProvider, PartitionService partitionService, DataDecodingEncodingService encodingService, TbDeviceProfileCache deviceProfileCache) { this.producerProvider = producerProvider; this.partitionService = partitionService; this.encodingService = encodingService; + this.deviceProfileCache = deviceProfileCache; } @Override @@ -124,6 +132,12 @@ public class DefaultTbClusterService implements TbClusterService { log.warn("[{}][{}] Received invalid message: {}", tenantId, entityId, tbMsg); return; } + } else { + if (entityId.getEntityType().equals(EntityType.DEVICE)) { + tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceId(entityId.getId()))); + } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { + tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); + } } TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); @@ -135,6 +149,16 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); } + private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) { + if (deviceProfile != null) { + RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId(); + if (targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId())) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); + } + } + return tbMsg; + } + @Override public void pushNotificationToRuleEngine(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) { TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); @@ -163,6 +187,36 @@ public class DefaultTbClusterService implements TbClusterService { broadcast(new ComponentLifecycleMsg(tenantId, entityId, state)); } + @Override + public void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback) { + log.trace("[{}][{}] Processing device profile [{}] change event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName()); + TransportProtos.DeviceProfileUpdateMsg profileUpdateMsg = TransportProtos.DeviceProfileUpdateMsg.newBuilder() + .setData(ByteString.copyFrom(encodingService.encode(deviceProfile))).build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileUpdateMsg(profileUpdateMsg).build(); + broadcast(transportMsg); + } + + @Override + public void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback) { + log.trace("[{}][{}] Processing device profile [{}] delete event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName()); + TransportProtos.DeviceProfileDeleteMsg profileDeleteMsg = TransportProtos.DeviceProfileDeleteMsg.newBuilder() + .setProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) + .setProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) + .build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileDeleteMsg(profileDeleteMsg).build(); + broadcast(transportMsg); + } + + private void broadcast(ToTransportMsg transportMsg) { + TbQueueProducer> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer(); + Set tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT); + for (String transportServiceId : tbTransportServices) { + TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId); + toTransportNfProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), null); + toTransportNfs.incrementAndGet(); + } + } + private void broadcast(ComponentLifecycleMsg msg) { byte[] msgBytes = encodingService.encode(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 6e1eca4a7d..5d240790f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -21,10 +21,13 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.RpcError; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.util.mapping.JacksonUtil; @@ -47,7 +50,8 @@ import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; @@ -92,8 +96,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreNotification.getComponentLifecycleMsg().toByteArray()); - if (actorMsg.isPresent()) { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); - actorContext.tellWithHighPriority(actorMsg.get()); - } + handleComponentLifecycleMsg(id, toCoreNotification.getComponentLifecycleMsg()); callback.onSuccess(); } if (statsEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 0f3efb6f05..fd356cabcc 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.queue; +import com.google.protobuf.ByteString; import com.google.protobuf.ProtocolStringList; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -37,7 +38,8 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import org.thingsboard.server.queue.util.TbRuleEngineComponent; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.*; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; @@ -80,8 +82,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< TbRuleEngineQueueFactory tbRuleEngineQueueFactory, RuleEngineStatisticsService statisticsService, ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbRuleEngineDeviceRpcService tbDeviceRpcService, - StatsFactory statsFactory) { - super(actorContext, encodingService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer()); + StatsFactory statsFactory, TbDeviceProfileCache deviceProfileCache) { + super(actorContext, encodingService, deviceProfileCache, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer()); this.statisticsService = statisticsService; this.ruleEngineSettings = ruleEngineSettings; this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory; @@ -144,7 +146,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< submitStrategy.init(msgs); while (!stopped) { - TbMsgPackProcessingContext ctx = new TbMsgPackProcessingContext(submitStrategy); + TbMsgPackProcessingContext ctx = new TbMsgPackProcessingContext(configuration.getName(), submitStrategy); submitStrategy.submitAttempt((id, msg) -> submitExecutor.submit(() -> { log.trace("[{}] Creating callback for message: {}", id, msg.getValue()); ToRuleEngineMsg toRuleEngineMsg = msg.getValue(); @@ -175,6 +177,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< if (!ctx.getFailedMap().isEmpty()) { printFirstOrAll(configuration, ctx, ctx.getFailedMap(), "Failed"); } + ctx.printProfilerStats(); + TbRuleEngineProcessingDecision decision = ackStrategy.analyze(result); if (statsEnabled) { stats.log(result, decision.isCommit()); @@ -237,11 +241,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception { ToRuleEngineNotificationMsg nfMsg = msg.getValue(); if (nfMsg.getComponentLifecycleMsg() != null && !nfMsg.getComponentLifecycleMsg().isEmpty()) { - Optional actorMsg = encodingService.decode(nfMsg.getComponentLifecycleMsg().toByteArray()); - if (actorMsg.isPresent()) { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); - actorContext.tellWithHighPriority(actorMsg.get()); - } + handleComponentLifecycleMsg(id, nfMsg.getComponentLifecycleMsg()); callback.onSuccess(); } else if (nfMsg.hasFromDeviceRpcResponse()) { TransportProtos.FromDeviceRPCResponseProto proto = nfMsg.getFromDeviceRpcResponse(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java index aae3cef0ac..f89e05a57b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java @@ -19,7 +19,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @@ -31,15 +33,20 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService private final TenantService tenantService; - public DefaultTenantRoutingInfoService(TenantService tenantService) { + private final TenantProfileService tenantProfileService; + + public DefaultTenantRoutingInfoService(TenantService tenantService, TenantProfileService tenantProfileService) { this.tenantService = tenantService; + this.tenantProfileService = tenantProfileService; } @Override public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { Tenant tenant = tenantService.findTenantById(tenantId); if (tenant != null) { - return new TenantRoutingInfo(tenantId, tenant.isIsolatedTbCore(), tenant.isIsolatedTbRuleEngine()); + // TODO: Tenant Profile from cache + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); + return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbCore(), tenantProfile.isIsolatedTbRuleEngine()); } else { throw new RuntimeException("Tenant not found!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java index cc722720d5..cf212a06f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java @@ -16,6 +16,8 @@ package org.thingsboard.server.service.queue; import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -49,4 +51,7 @@ public interface TbClusterService { void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); + void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback); + + void onDeviceProfileDelete(DeviceProfile deviceProfileId, TbQueueCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java index 4d5be5b883..376c64a17d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java @@ -67,8 +67,14 @@ public class TbMsgPackCallback implements TbMsgCallback { } @Override - public void visit(RuleNodeInfo ruleNodeInfo) { - log.trace("[{}] ON PROCESS: {}", id, ruleNodeInfo); - ctx.visit(id, ruleNodeInfo); + public void onProcessingStart(RuleNodeInfo ruleNodeInfo) { + log.trace("[{}] ON PROCESSING START: {}", id, ruleNodeInfo); + ctx.onProcessingStart(id, ruleNodeInfo); + } + + @Override + public void onProcessingEnd(RuleNodeId ruleNodeId) { + log.trace("[{}] ON PROCESSING END: {}", id, ruleNodeId); + ctx.onProcessingEnd(id, ruleNodeId); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java index 1b7ffd9c30..6d88ed204a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.queue; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.RuleEngineException; @@ -24,6 +25,8 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; +import java.util.Comparator; +import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -31,9 +34,13 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +@Slf4j public class TbMsgPackProcessingContext { + private final String queueName; private final TbRuleEngineSubmitStrategy submitStrategy; + @Getter + private final boolean profilerEnabled; private final AtomicInteger pendingCount; private final CountDownLatch processingTimeoutLatch = new CountDownLatch(1); @Getter @@ -47,14 +54,20 @@ public class TbMsgPackProcessingContext { private final ConcurrentMap lastRuleNodeMap = new ConcurrentHashMap<>(); - public TbMsgPackProcessingContext(TbRuleEngineSubmitStrategy submitStrategy) { + public TbMsgPackProcessingContext(String queueName, TbRuleEngineSubmitStrategy submitStrategy) { + this.queueName = queueName; this.submitStrategy = submitStrategy; + this.profilerEnabled = log.isDebugEnabled(); this.pendingMap = submitStrategy.getPendingMap(); this.pendingCount = new AtomicInteger(pendingMap.size()); } public boolean await(long packProcessingTimeout, TimeUnit milliseconds) throws InterruptedException { - return processingTimeoutLatch.await(packProcessingTimeout, milliseconds); + boolean success = processingTimeoutLatch.await(packProcessingTimeout, milliseconds); + if (!success && profilerEnabled) { + msgProfilerMap.values().forEach(this::onTimeout); + } + return success; } public void onSuccess(UUID id) { @@ -85,12 +98,53 @@ public class TbMsgPackProcessingContext { } } - public void visit(UUID id, RuleNodeInfo ruleNodeInfo) { + private final ConcurrentHashMap msgProfilerMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap ruleNodeProfilerMap = new ConcurrentHashMap<>(); + + public void onProcessingStart(UUID id, RuleNodeInfo ruleNodeInfo) { lastRuleNodeMap.put(id, ruleNodeInfo); + if (profilerEnabled) { + msgProfilerMap.computeIfAbsent(id, TbMsgProfilerInfo::new).onStart(ruleNodeInfo.getRuleNodeId()); + ruleNodeProfilerMap.putIfAbsent(ruleNodeInfo.getRuleNodeId().getId(), new TbRuleNodeProfilerInfo(ruleNodeInfo)); + } + } + + public void onProcessingEnd(UUID id, RuleNodeId ruleNodeId) { + if (profilerEnabled) { + long processingTime = msgProfilerMap.computeIfAbsent(id, TbMsgProfilerInfo::new).onEnd(ruleNodeId); + if (processingTime > 0) { + ruleNodeProfilerMap.computeIfAbsent(ruleNodeId.getId(), TbRuleNodeProfilerInfo::new).record(processingTime); + } + } + } + + public void onTimeout(TbMsgProfilerInfo profilerInfo) { + Map.Entry ruleNodeInfo = profilerInfo.onTimeout(); + if (ruleNodeInfo != null) { + ruleNodeProfilerMap.computeIfAbsent(ruleNodeInfo.getKey(), TbRuleNodeProfilerInfo::new).record(ruleNodeInfo.getValue()); + } } public RuleNodeInfo getLastVisitedRuleNode(UUID id) { return lastRuleNodeMap.get(id); } + public void printProfilerStats() { + if (profilerEnabled) { + log.debug("Top Rule Nodes by max execution time:"); + ruleNodeProfilerMap.values().stream() + .sorted(Comparator.comparingLong(TbRuleNodeProfilerInfo::getMaxExecutionTime).reversed()).limit(5) + .forEach(info -> log.debug("[{}][{}] max execution time: {}. {}", queueName, info.getRuleNodeId(), info.getMaxExecutionTime(), info.getLabel())); + + log.info("Top Rule Nodes by avg execution time:"); + ruleNodeProfilerMap.values().stream() + .sorted(Comparator.comparingDouble(TbRuleNodeProfilerInfo::getAvgExecutionTime).reversed()).limit(5) + .forEach(info -> log.info("[{}][{}] avg execution time: {}. {}", queueName, info.getRuleNodeId(), info.getAvgExecutionTime(), info.getLabel())); + + log.info("Top Rule Nodes by execution count:"); + ruleNodeProfilerMap.values().stream() + .sorted(Comparator.comparingInt(TbRuleNodeProfilerInfo::getExecutionCount).reversed()).limit(5) + .forEach(info -> log.info("[{}][{}] execution count: {}. {}", queueName, info.getRuleNodeId(), info.getExecutionCount(), info.getLabel())); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgProfilerInfo.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgProfilerInfo.java new file mode 100644 index 0000000000..f66cd1a50a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgProfilerInfo.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.msg.queue.RuleNodeInfo; + +import java.util.AbstractMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Slf4j +public class TbMsgProfilerInfo { + private final UUID msgId; + private AtomicLong totalProcessingTime = new AtomicLong(); + private Lock stateLock = new ReentrantLock(); + private RuleNodeId currentRuleNodeId; + private long stateChangeTime; + + public TbMsgProfilerInfo(UUID msgId) { + this.msgId = msgId; + } + + public void onStart(RuleNodeId ruleNodeId) { + long currentTime = System.currentTimeMillis(); + stateLock.lock(); + try { + currentRuleNodeId = ruleNodeId; + stateChangeTime = currentTime; + } finally { + stateLock.unlock(); + } + } + + public long onEnd(RuleNodeId ruleNodeId) { + long currentTime = System.currentTimeMillis(); + stateLock.lock(); + try { + if (ruleNodeId.equals(currentRuleNodeId)) { + long processingTime = currentTime - stateChangeTime; + stateChangeTime = currentTime; + totalProcessingTime.addAndGet(processingTime); + currentRuleNodeId = null; + return processingTime; + } else { + log.trace("[{}] Invalid sequence of rule node processing detected. Expected [{}] but was [{}]", msgId, currentRuleNodeId, ruleNodeId); + return 0; + } + } finally { + stateLock.unlock(); + } + } + + public Map.Entry onTimeout() { + long currentTime = System.currentTimeMillis(); + stateLock.lock(); + try { + if (currentRuleNodeId != null && stateChangeTime > 0) { + long timeoutTime = currentTime - stateChangeTime; + totalProcessingTime.addAndGet(timeoutTime); + return new AbstractMap.SimpleEntry<>(currentRuleNodeId.getId(), timeoutTime); + } + } finally { + stateLock.unlock(); + } + return null; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbRuleNodeProfilerInfo.java b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleNodeProfilerInfo.java new file mode 100644 index 0000000000..c88532fbc3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleNodeProfilerInfo.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue; + +import lombok.Getter; +import org.thingsboard.server.common.msg.queue.RuleNodeInfo; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class TbRuleNodeProfilerInfo { + @Getter + private final UUID ruleNodeId; + @Getter + private final String label; + private AtomicInteger executionCount = new AtomicInteger(0); + private AtomicLong executionTime = new AtomicLong(0); + private AtomicLong maxExecutionTime = new AtomicLong(0); + + public TbRuleNodeProfilerInfo(RuleNodeInfo ruleNodeInfo) { + this.ruleNodeId = ruleNodeInfo.getRuleNodeId().getId(); + this.label = ruleNodeInfo.toString(); + } + + public TbRuleNodeProfilerInfo(UUID ruleNodeId) { + this.ruleNodeId = ruleNodeId; + this.label = ""; + } + + public void record(long processingTime) { + executionCount.incrementAndGet(); + executionTime.addAndGet(processingTime); + while (true) { + long value = maxExecutionTime.get(); + if (value >= processingTime) { + break; + } + if (maxExecutionTime.compareAndSet(value, processingTime)) { + break; + } + } + } + + int getExecutionCount() { + return executionCount.get(); + } + + long getMaxExecutionTime() { + return maxExecutionTime.get(); + } + + double getAvgExecutionTime() { + double executionCnt = (double) executionCount.get(); + if (executionCnt > 0) { + return executionTime.get() / executionCnt; + } else { + return 0.0; + } + } + +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 4007c9e17d..3f6595f81e 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -15,23 +15,31 @@ */ package org.thingsboard.server.service.queue.processing; +import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.EventListener; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbPackCallback; import org.thingsboard.server.service.queue.TbPackProcessingContext; import javax.annotation.PreDestroy; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -51,12 +59,15 @@ public abstract class AbstractConsumerService> nfConsumer; - public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbQueueConsumer> nfConsumer) { + public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, + TbDeviceProfileCache deviceProfileCache, TbQueueConsumer> nfConsumer) { this.actorContext = actorContext; this.encodingService = encodingService; + this.deviceProfileCache = deviceProfileCache; this.nfConsumer = nfConsumer; } @@ -126,18 +137,32 @@ public abstract class AbstractConsumerService actorMsgOpt = encodingService.decode(nfMsg.toByteArray()); + if (actorMsgOpt.isPresent()) { + TbActorMsg actorMsg = actorMsgOpt.get(); + if (actorMsg instanceof ComponentLifecycleMsg) { + ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg; + if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + deviceProfileCache.evict(new DeviceProfileId(componentLifecycleMsg.getEntityId().getId())); + } else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + deviceProfileCache.evict(new DeviceId(componentLifecycleMsg.getEntityId().getId())); + } + } + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); + actorContext.tellWithHighPriority(actorMsg); + } + } + protected abstract void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception; @PreDestroy public void destroy() { stopped = true; - stopMainConsumers(); - if (nfConsumer != null) { nfConsumer.unsubscribe(); } - if (consumersExecutor != null) { consumersExecutor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java index b9741d2433..be299b39b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java @@ -68,18 +68,20 @@ public class BatchTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS int listSize = orderedMsgList.size(); int startIdx = Math.min(packIdx.get() * batchSize, listSize); int endIdx = Math.min(startIdx + batchSize, listSize); + Map> tmpPack; synchronized (pendingPack) { pendingPack.clear(); for (int i = startIdx; i < endIdx; i++) { IdMsgPair pair = orderedMsgList.get(i); pendingPack.put(pair.uuid, pair.msg); } + tmpPack = new LinkedHashMap<>(pendingPack); } int submitSize = pendingPack.size(); if (log.isDebugEnabled() && submitSize > 0) { log.debug("[{}] submitting [{}] messages to rule engine", queueName, submitSize); } - pendingPack.forEach(msgConsumer); + tmpPack.forEach(msgConsumer); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java index b6220f5f94..b42615ad8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java @@ -56,7 +56,9 @@ public class TbRuleEngineProcessingStrategyFactory { private final boolean retryTimeout; private final int maxRetries; private final double maxAllowedFailurePercentage; - private final long pauseBetweenRetries; + private final long maxPauseBetweenRetries; + + private long pauseBetweenRetries; private int initialTotalCount; private int retryCount; @@ -69,6 +71,7 @@ public class TbRuleEngineProcessingStrategyFactory { this.maxRetries = configuration.getRetries(); this.maxAllowedFailurePercentage = configuration.getFailurePercentage(); this.pauseBetweenRetries = configuration.getPauseBetweenRetries(); + this.maxPauseBetweenRetries = configuration.getMaxPauseBetweenRetries(); } @Override @@ -108,6 +111,9 @@ public class TbRuleEngineProcessingStrategyFactory { } catch (InterruptedException e) { throw new RuntimeException(e); } + if (maxPauseBetweenRetries > pauseBetweenRetries) { + pauseBetweenRetries = Math.min(maxPauseBetweenRetries, pauseBetweenRetries * 2); + } } return new TbRuleEngineProcessingDecision(false, toReprocess); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index b08ec2625b..b4bc79e06e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -27,6 +27,7 @@ import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -48,6 +50,7 @@ import org.thingsboard.server.controller.HttpValidationCallback; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -72,6 +75,7 @@ import java.util.function.BiConsumer; @Component public class AccessValidator { + public static final String ONLY_SYSTEM_ADMINISTRATOR_IS_ALLOWED_TO_PERFORM_THIS_OPERATION = "Only system administrator is allowed to perform this operation!"; public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; @@ -89,6 +93,9 @@ public class AccessValidator { @Autowired protected DeviceService deviceService; + @Autowired + protected DeviceProfileService deviceProfileService; + @Autowired protected AssetService assetService; @@ -162,6 +169,9 @@ public class AccessValidator { case DEVICE: validateDevice(currentUser, operation, entityId, callback); return; + case DEVICE_PROFILE: + validateDeviceProfile(currentUser, operation, entityId, callback); + return; case ASSET: validateAsset(currentUser, operation, entityId, callback); return; @@ -174,6 +184,9 @@ public class AccessValidator { case TENANT: validateTenant(currentUser, operation, entityId, callback); return; + case TENANT_PROFILE: + validateTenantProfile(currentUser, operation, entityId, callback); + return; case USER: validateUser(currentUser, operation, entityId, callback); return; @@ -206,6 +219,24 @@ public class AccessValidator { } } + private void validateDeviceProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(currentUser.getTenantId(), new DeviceProfileId(entityId.getId())); + if (deviceProfile == null) { + callback.onSuccess(ValidationResult.entityNotFound("Device profile with requested id wasn't found!")); + } else { + try { + accessControlService.checkPermission(currentUser, Resource.DEVICE_PROFILE, operation, entityId, deviceProfile); + } catch (ThingsboardException e) { + callback.onSuccess(ValidationResult.accessDenied(e.getMessage())); + } + callback.onSuccess(ValidationResult.ok(deviceProfile)); + } + } + } + private void validateAsset(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); @@ -313,6 +344,14 @@ public class AccessValidator { } } + private void validateTenantProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.ok(null)); + } else { + callback.onSuccess(ValidationResult.accessDenied(ONLY_SYSTEM_ADMINISTRATOR_IS_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } + } + private void validateUser(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { ListenableFuture userFuture = userService.findUserByIdAsync(currentUser.getTenantId(), new UserId(entityId.getId())); Futures.addCallback(userFuture, getCallback(callback, user -> { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index a66b822fca..96eff6efd0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -31,7 +31,9 @@ public enum Resource { RULE_CHAIN(EntityType.RULE_CHAIN), USER(EntityType.USER), WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE), - WIDGET_TYPE(EntityType.WIDGET_TYPE); + WIDGET_TYPE(EntityType.WIDGET_TYPE), + TENANT_PROFILE(EntityType.TENANT_PROFILE), + DEVICE_PROFILE(EntityType.DEVICE_PROFILE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index cd79a29f0b..766290298a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -39,6 +39,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); put(Resource.WIDGET_TYPE, systemEntityPermissionChecker); + put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 794fb72398..3caa405214 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -42,6 +42,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker); put(Resource.WIDGET_TYPE, widgetsPermissionChecker); + put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 5d283bbf0e..5120698856 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -226,6 +226,11 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer @Override public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback) { + onAttributesUpdate(tenantId, entityId, scope, attributes, true, callback); + } + + @Override + public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback) { onLocalTelemetrySubUpdate(entityId, s -> { if (TbSubscriptionType.ATTRIBUTES.equals(s.getType())) { @@ -254,7 +259,7 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), attribute.getLongValue().orElse(0L)); } } - } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope)) { + } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) , null); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index 5ca6b4f82e..c0ca01685d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -17,13 +17,12 @@ package org.thingsboard.server.service.subscription; import org.springframework.context.ApplicationListener; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import java.util.List; @@ -37,9 +36,13 @@ public interface SubscriptionManagerService extends ApplicationListener attributes, TbCallback callback); + void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback); + void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback empty); void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 16d95a48ce..aecd888dce 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -171,9 +171,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback) { + saveAndNotify(tenantId, entityId, scope, attributes, true, callback); + } + + @Override + public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback) { ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); addMainCallback(saveFuture, callback); - addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes)); + addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice)); } @Override @@ -236,11 +241,11 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer , System.currentTimeMillis())), callback); } - private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes) { + private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); if (currentPartitions.contains(tpi)) { if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, TbCallback.EMPTY); + subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY); } else { log.warn("Possible misconfiguration because subscriptionManagerService is null!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 7399380c26..7d41190ac0 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -21,26 +21,36 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; @@ -74,44 +84,64 @@ public class DefaultTransportApiService implements TransportApiService { private static final ObjectMapper mapper = new ObjectMapper(); //TODO: Constructor dependencies; - @Autowired - private TenantService tenantService; + private final DeviceProfileService deviceProfileService; + private final TenantService tenantService; + private final TenantProfileService tenantProfileService; + private final DeviceService deviceService; + private final RelationService relationService; + private final DeviceCredentialsService deviceCredentialsService; + private final DeviceStateService deviceStateService; + private final DbCallbackExecutorService dbCallbackExecutorService; + private final TbClusterService tbClusterService; + private final DataDecodingEncodingService dataDecodingEncodingService; - @Autowired - private DeviceService deviceService; - - @Autowired - private RelationService relationService; - - @Autowired - private DeviceCredentialsService deviceCredentialsService; - - @Autowired - private DeviceStateService deviceStateService; - - @Autowired - private DbCallbackExecutorService dbCallbackExecutorService; - - @Autowired - protected TbClusterService tbClusterService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); + public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService, + TenantProfileService tenantProfileService, DeviceService deviceService, + RelationService relationService, DeviceCredentialsService deviceCredentialsService, + DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, + TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService) { + this.deviceProfileService = deviceProfileService; + this.tenantService = tenantService; + this.tenantProfileService = tenantProfileService; + this.deviceService = deviceService; + this.relationService = relationService; + this.deviceCredentialsService = deviceCredentialsService; + this.deviceStateService = deviceStateService; + this.dbCallbackExecutorService = dbCallbackExecutorService; + this.tbClusterService = tbClusterService; + this.dataDecodingEncodingService = dataDecodingEncodingService; + } + @Override public ListenableFuture> handle(TbProtoQueueMsg tbProtoQueueMsg) { TransportApiRequestMsg transportApiRequestMsg = tbProtoQueueMsg.getValue(); if (transportApiRequestMsg.hasValidateTokenRequestMsg()) { ValidateDeviceTokenRequestMsg msg = transportApiRequestMsg.getValidateTokenRequestMsg(); - return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasValidateBasicMqttCredRequestMsg()) { + TransportProtos.ValidateBasicMqttCredRequestMsg msg = transportApiRequestMsg.getValidateBasicMqttCredRequestMsg(); + return Futures.transform(validateCredentials(msg), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); - return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasGetTenantRoutingInfoRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } - return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(getEmptyTransportApiResponseFuture(), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } private ListenableFuture validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) { @@ -124,6 +154,62 @@ public class DefaultTransportApiService implements TransportApiService { } } + private ListenableFuture validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); + if (credentials != null) { + if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) { + return getDeviceInfo(credentials.getDeviceId(), credentials); + } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { + if (!checkMqttCredentials(mqtt, credentials)) { + credentials = null; + } + } + } + if (credentials == null) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName())); + if (credentials == null) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId())); + } + } + if (credentials != null) { + return getDeviceInfo(credentials.getDeviceId(), credentials); + } else { + return getEmptyTransportApiResponseFuture(); + } + } + + private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, String credId) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { + if (!checkMqttCredentials(clientCred, deviceCredentials)) { + return null; + } else { + return deviceCredentials; + } + } + return null; + } + + private boolean checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) { + BasicMqttCredentials dbCred = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); + if (!StringUtils.isEmpty(dbCred.getClientId()) && !dbCred.getClientId().equals(clientCred.getClientId())) { + return false; + } + if (!StringUtils.isEmpty(dbCred.getUserName()) && !dbCred.getUserName().equals(clientCred.getUserName())) { + return false; + } + if (!StringUtils.isEmpty(dbCred.getPassword())) { + if (StringUtils.isEmpty(clientCred.getPassword())) { + return false; + } else { + if (!dbCred.getPassword().equals(clientCred.getPassword())) { + return false; + } + } + } + return true; + } + private ListenableFuture handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB())); ListenableFuture gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId); @@ -139,6 +225,8 @@ public class DefaultTransportApiService implements TransportApiService { device.setName(requestMsg.getDeviceName()); device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); + DeviceProfile deviceProfile = deviceProfileService.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + device.setDeviceProfileId(deviceProfile.getId()); device = deviceService.saveDevice(device); relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); deviceStateService.onDeviceAdded(device); @@ -155,10 +243,19 @@ public class DefaultTransportApiService implements TransportApiService { TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, mapper.writeValueAsString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); } + GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() + .setDeviceInfo(getDeviceInfoProto(device)); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile != null) { + builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); + } else { + log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); + } return TransportApiResponseMsg.newBuilder() - .setGetOrCreateDeviceResponseMsg(GetOrCreateDeviceFromGatewayResponseMsg.newBuilder().setDeviceInfo(getDeviceInfoProto(device)).build()).build(); + .setGetOrCreateDeviceResponseMsg(builder.build()) + .build(); } catch (JsonProcessingException e) { - log.warn("[{}][{}] Failed to lookup device by gateway id and name", gatewayId, requestMsg.getDeviceName(), e); + log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); } finally { deviceCreationLock.unlock(); @@ -168,10 +265,23 @@ public class DefaultTransportApiService implements TransportApiService { private ListenableFuture handle(GetTenantRoutingInfoRequestMsg requestMsg) { TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); - ListenableFuture tenantFuture = tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId); - return Futures.transform(tenantFuture, tenant -> TransportApiResponseMsg.newBuilder() - .setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenant.isIsolatedTbCore()) - .setIsolatedTbRuleEngine(tenant.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService); + // TODO: Tenant Profile from cache + ListenableFuture tenantProfileFuture = + Futures.transform(tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId), tenant -> + tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId()), dbCallbackExecutorService); + return Futures.transform(tenantProfileFuture, tenantProfile -> TransportApiResponseMsg.newBuilder() + .setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenantProfile.isIsolatedTbCore()) + .setIsolatedTbRuleEngine(tenantProfile.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService); + } + + private ListenableFuture handle(TransportProtos.GetDeviceProfileRequestMsg requestMsg) { + DeviceProfileId profileId = new DeviceProfileId(new UUID(requestMsg.getProfileIdMSB(), requestMsg.getProfileIdLSB())); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, profileId); + return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() + .setGetDeviceProfileResponseMsg( + TransportProtos.GetDeviceProfileResponseMsg.newBuilder() + .setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))) + .build()).build()); } private ListenableFuture getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) { @@ -183,11 +293,17 @@ public class DefaultTransportApiService implements TransportApiService { try { ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder(); builder.setDeviceInfo(getDeviceInfoProto(device)); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile != null) { + builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); + } else { + log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); + } if (!StringUtils.isEmpty(credentials.getCredentialsValue())) { builder.setCredentialsBody(credentials.getCredentialsValue()); } return TransportApiResponseMsg.newBuilder() - .setValidateTokenResponseMsg(builder.build()).build(); + .setValidateCredResponseMsg(builder.build()).build(); } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by id", deviceId, e); return getEmptyTransportApiResponse(); @@ -203,6 +319,8 @@ public class DefaultTransportApiService implements TransportApiService { .setDeviceIdLSB(device.getId().getId().getLeastSignificantBits()) .setDeviceName(device.getName()) .setDeviceType(device.getType()) + .setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits()) .setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo())) .build(); } @@ -213,6 +331,6 @@ public class DefaultTransportApiService implements TransportApiService { private TransportApiResponseMsg getEmptyTransportApiResponse() { return TransportApiResponseMsg.newBuilder() - .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); + .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java index 4fc4df0048..41f90c642c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java @@ -38,19 +38,14 @@ public abstract class AbstractCleanUpService { @Value("${spring.datasource.password}") protected String dbPassword; - protected long executeQuery(Connection conn, String query) { - long removed = 0L; - try { - Statement statement = conn.createStatement(); - ResultSet resultSet = statement.executeQuery(query); - getWarnings(statement); + protected long executeQuery(Connection conn, String query) throws SQLException { + try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)) { + if (log.isDebugEnabled()) { + getWarnings(statement); + } resultSet.next(); - removed = resultSet.getLong(1); - log.debug("Successfully executed query: {}", query); - } catch (SQLException e) { - log.debug("Failed to execute query: {} due to: {}", query, e.getMessage()); + return resultSet.getLong(1); } - return removed; } protected void getWarnings(Statement statement) throws SQLException { @@ -65,6 +60,6 @@ public abstract class AbstractCleanUpService { } } - protected abstract void doCleanUp(Connection connection); + protected abstract void doCleanUp(Connection connection) throws SQLException; } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java index a608ca257b..0f3cd71f00 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java @@ -52,7 +52,7 @@ public class EventsCleanUpService extends AbstractCleanUpService { } @Override - protected void doCleanUp(Connection connection) { + protected void doCleanUp(Connection connection) throws SQLException { long totalEventsRemoved = executeQuery(connection, "call cleanup_events_by_ttl(" + ttl + ", " + debugTtl + ", 0);"); log.info("Total events removed by TTL: [{}]", totalEventsRemoved); } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java index fb09a7eab4..73a5c73732 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.dao.util.PsqlDao; import org.thingsboard.server.dao.util.SqlTsDao; import java.sql.Connection; +import java.sql.SQLException; @SqlTsDao @PsqlDao @@ -34,10 +35,10 @@ public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpServi private String partitionType; @Override - protected void doCleanUp(Connection connection) { - long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);"); - log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved); - long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); - log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); + protected void doCleanUp(Connection connection) throws SQLException { + long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);"); + log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved); + long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); + log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); } } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java index 7070ef5dc3..40febd1988 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.TimescaleDBTsDao; import java.sql.Connection; +import java.sql.SQLException; @TimescaleDBTsDao @Service @@ -28,8 +29,8 @@ import java.sql.Connection; public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService { @Override - protected void doCleanUp(Connection connection) { + protected void doCleanUp(Connection connection) throws SQLException { long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); } -} \ No newline at end of file +} diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 02101a8eaa..e154de2d9b 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -29,6 +29,8 @@ + + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a1b7633f83..51363d3c83 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -194,8 +194,21 @@ cassandra: url: "${CASSANDRA_URL:127.0.0.1:9042}" # Specify local datacenter name local_datacenter: "${CASSANDRA_LOCAL_DATACENTER:datacenter1}" - # Enable/disable secure connection - ssl: "${CASSANDRA_USE_SSL:false}" + ssl: + # Enable/disable secure connection + enabled: "${CASSANDRA_USE_SSL:false}" + # Enable/disable validation of Cassandra server hostname + # If enabled, hostname of Cassandra server must match CN of server certificate + hostname_validation: "${CASSANDRA_SSL_HOSTNAME_VALIDATION:true}" + # Set trust store for client authentication of server (optional, uses trust store from default SSLContext if not set) + trust_store: "${CASSANDRA_SSL_TRUST_STORE:}" + trust_store_password: "${CASSANDRA_SSL_TRUST_STORE_PASSWORD:}" + # Set key store for server authentication of client (optional, uses key store from default SSLContext if not set) + # A key store is only needed if the Cassandra server requires client authentication + key_store: "${CASSANDRA_SSL_KEY_STORE:}" + key_store_password: "${CASSANDRA_SSL_KEY_STORE_PASSWORD:}" + # Comma separated list of cipher suites (optional, uses Java default cipher suites if not set) + cipher_suites: "${CASSANDRA_SSL_CIPHER_SUITES:}" # Enable/disable JMX jmx: "${CASSANDRA_USE_JMX:false}" # Enable/disable metrics collection. @@ -271,6 +284,8 @@ sql: batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" + # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks + batch_sort: "${SQL_BATCH_SORT:false}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}" # Specify whether to log database queries and their parameters generated by entity query repository @@ -372,6 +387,12 @@ caffeine: securitySettings: timeToLiveInMinutes: 1440 maxSize: 0 + tenantProfiles: + timeToLiveInMinutes: 1440 + maxSize: 0 + deviceProfiles: + timeToLiveInMinutes: 1440 + maxSize: 0 redis: # standalone or cluster @@ -418,6 +439,9 @@ updates: # Enable/disable updates checking. enabled: "${UPDATES_ENABLED:true}" +# spring freemarker configuration +spring.freemarker.checkTemplateLocation: "false" + # spring CORS configuration spring.mvc.cors: mappings: @@ -483,6 +507,7 @@ audit-log: "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" "alarm": "${AUDIT_LOG_MASK_ALARM:W}" "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" + "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" @@ -583,6 +608,8 @@ transport: key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" # Type of the key store key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" # Local CoAP transport parameters coap: # Enable/disable coap transport protocol. @@ -608,6 +635,10 @@ swagger: queue: type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) + in_memory: + stats: + # For debug lvl + print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" kafka: bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" acks: "${TB_KAFKA_ACKS:all}" @@ -627,11 +658,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" @@ -739,6 +770,7 @@ queue: retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; + max-pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:3}"# Max allowed time in seconds for pause between retries. - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" @@ -754,6 +786,7 @@ queue: retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; + max-pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}"# Max allowed time in seconds for pause between retries. - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" @@ -769,6 +802,7 @@ queue: retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; + max-pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}"# Max allowed time in seconds for pause between retries. transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" @@ -793,4 +827,4 @@ management: web: exposure: # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). - include: '${METRICS_ENDPOINTS_EXPOSE:info}' \ No newline at end of file + include: '${METRICS_ENDPOINTS_EXPOSE:info}' diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 992b39c472..8d3391bb65 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -59,8 +60,16 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.context.WebApplicationContext; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageLink; @@ -217,6 +226,10 @@ public abstract class AbstractWebTest { login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); } + protected void loginUser(String userName, String password) throws Exception { + login(userName, password); + } + private Tenant savedDifferentTenant; protected void loginDifferentTenant() throws Exception { @@ -242,15 +255,27 @@ public abstract class AbstractWebTest { protected User createUserAndLogin(User user, String password) throws Exception { User savedUser = doPost("/api/user", user, User.class); logout(); + JsonNode activateRequest = getActivateRequest(password); + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenInfo, user.getEmail()); + return savedUser; + } + + protected User createUser(User user, String password) throws Exception { + User savedUser = doPost("/api/user", user, User.class); + JsonNode activateRequest = getActivateRequest(password); + ResultActions resultActions = doPost("/api/noauth/activate", activateRequest); + resultActions.andExpect(status().isOk()); + return savedUser; + } + + private JsonNode getActivateRequest(String password) throws Exception { doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) .andExpect(status().isSeeOther()) .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); - JsonNode activateRequest = new ObjectMapper().createObjectNode() + return new ObjectMapper().createObjectNode() .put("activateToken", TestMailService.currentActivateToken) .put("password", password); - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); - validateAndSetJwtToken(tokenInfo, user.getEmail()); - return savedUser; } protected void login(String username, String password) throws Exception { @@ -304,6 +329,23 @@ public abstract class AbstractWebTest { } } + protected DeviceProfile createDeviceProfile(String name) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName(name); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setDescription(name + " Test"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + return deviceProfile; + } + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); setJwtToken(getRequest); @@ -416,6 +458,10 @@ public abstract class AbstractWebTest { return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass); } + protected T doPostClaimAsync(String urlTemplate, Object content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); + } + protected T doDelete(String urlTemplate, Class responseClass, String... params) throws Exception { return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); } @@ -486,7 +532,7 @@ public abstract class AbstractWebTest { return mapper.readerFor(type).readValue(content); } - public class IdComparator> implements Comparator { + public class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { return o1.getId().getId().compareTo(o2.getId().getId()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index f87dc5de6d..72a885b062 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -24,6 +24,7 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -185,9 +186,8 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { public void testSaveDeviceWithEmptyType() throws Exception { Device device = new Device(); device.setName("My device"); - doPost("/api/device", device) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Device type should be specified"))); + Device savedDevice = doPost("/api/device", device, Device.class); + Assert.assertEquals("default", savedDevice.getType()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java new file mode 100644 index 0000000000..b2334d7c46 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -0,0 +1,309 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseDeviceProfileControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator deviceProfileInfoIdComparator = new IdComparator<>(); + + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveDeviceProfile() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Assert.assertNotNull(savedDeviceProfile); + Assert.assertNotNull(savedDeviceProfile.getId()); + Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); + Assert.assertEquals(deviceProfile.getName(), savedDeviceProfile.getName()); + Assert.assertEquals(deviceProfile.getDescription(), savedDeviceProfile.getDescription()); + Assert.assertEquals(deviceProfile.getProfileData(), savedDeviceProfile.getProfileData()); + Assert.assertEquals(deviceProfile.isDefault(), savedDeviceProfile.isDefault()); + Assert.assertEquals(deviceProfile.getDefaultRuleChainId(), savedDeviceProfile.getDefaultRuleChainId()); + savedDeviceProfile.setName("New device profile"); + doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class); + DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); + } + + @Test + public void testFindDeviceProfileById() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); + Assert.assertNotNull(foundDeviceProfile); + Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); + } + + @Test + public void testFindDeviceProfileInfoById() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/"+savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class); + Assert.assertNotNull(foundDeviceProfileInfo); + Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfileInfo.getName()); + Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType()); + } + + @Test + public void testFindDefaultDeviceProfileInfo() throws Exception { + DeviceProfileInfo foundDefaultDeviceProfileInfo = doGet("/api/deviceProfileInfo/default", DeviceProfileInfo.class); + Assert.assertNotNull(foundDefaultDeviceProfileInfo); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getId()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getName()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getType()); + Assert.assertEquals(DeviceProfileType.DEFAULT, foundDefaultDeviceProfileInfo.getType()); + Assert.assertEquals("default", foundDefaultDeviceProfileInfo.getName()); + } + + @Test + public void testSetDefaultDeviceProfile() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile defaultDeviceProfile = doPost("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString()+"/default", null, DeviceProfile.class); + Assert.assertNotNull(defaultDeviceProfile); + DeviceProfileInfo foundDefaultDeviceProfile = doGet("/api/deviceProfileInfo/default", DeviceProfileInfo.class); + Assert.assertNotNull(foundDefaultDeviceProfile); + Assert.assertEquals(savedDeviceProfile.getName(), foundDefaultDeviceProfile.getName()); + Assert.assertEquals(savedDeviceProfile.getId(), foundDefaultDeviceProfile.getId()); + Assert.assertEquals(savedDeviceProfile.getType(), foundDefaultDeviceProfile.getType()); + } + + @Test + public void testSaveDeviceProfileWithEmptyName() throws Exception { + DeviceProfile deviceProfile = new DeviceProfile(); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Device profile name should be specified"))); + } + + @Test + public void testSaveDeviceProfileWithSameName() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile"); + doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Device profile with such name already exists"))); + } + + @Ignore + @Test + public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + doPost("/api/device", device, Device.class); + //TODO uncomment once we have other device types; + //savedDeviceProfile.setType(DeviceProfileType.LWM2M); + doPost("/api/deviceProfile", savedDeviceProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't change device profile type because devices referenced it"))); + } + + @Test + public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + doPost("/api/device", device, Device.class); + savedDeviceProfile.setTransportType(DeviceTransportType.MQTT); + doPost("/api/deviceProfile", savedDeviceProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't change device profile transport type because devices referenced it"))); + } + + @Test + public void testDeleteDeviceProfileWithExistingDevice() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + + Device savedDevice = doPost("/api/device", device, Device.class); + + doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("The device profile referenced by the devices cannot be deleted"))); + } + + @Test + public void testDeleteDeviceProfile() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testFindDeviceProfiles() throws Exception { + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + deviceProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i); + deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + } + + List loadedDeviceProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + loadedDeviceProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfiles, idComparator); + + Assert.assertEquals(deviceProfiles, loadedDeviceProfiles); + + for (DeviceProfile deviceProfile : loadedDeviceProfiles) { + if (!deviceProfile.isDefault()) { + doDelete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindDeviceProfileInfos() throws Exception { + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData deviceProfilePageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(deviceProfilePageData.hasNext()); + Assert.assertEquals(1, deviceProfilePageData.getTotalElements()); + deviceProfiles.addAll(deviceProfilePageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i); + deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + } + + List loadedDeviceProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/deviceProfileInfos?", + new TypeReference>(){}, pageLink); + loadedDeviceProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); + + List deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), + deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + + Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); + + for (DeviceProfile deviceProfile : deviceProfiles) { + if (!deviceProfile.isDefault()) { + doDelete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/deviceProfileInfos?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index 1d474b8b31..5aeb12699b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -424,7 +425,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes assertNotNull(accessToken); String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId); + MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence()); MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(accessToken); @@ -466,7 +467,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes assertNotNull(accessToken); String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId); + MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence()); MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(accessToken); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java index e9641646ff..2294987668 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java @@ -15,21 +15,21 @@ */ package org.thingsboard.server.controller; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Assert; +import org.junit.Test; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.junit.Assert; -import org.junit.Test; -import com.fasterxml.jackson.core.type.TypeReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class BaseTenantControllerTest extends AbstractControllerTest { @@ -65,6 +65,19 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } + + @Test + public void testFindTenantInfoById() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + TenantInfo foundTenant = doGet("/api/tenant/info/"+savedTenant.getId().getId().toString(), TenantInfo.class); + Assert.assertNotNull(foundTenant); + Assert.assertEquals(new TenantInfo(savedTenant, "Default"), foundTenant); + doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } @Test public void testSaveTenantWithEmptyTitle() throws Exception { @@ -217,4 +230,48 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testFindTenantInfos() throws Exception { + loginSysAdmin(); + List tenants = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getData().size()); + tenants.addAll(pageData.getData()); + + for (int i=0;i<56;i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("Tenant"+i); + tenants.add(new TenantInfo(doPost("/api/tenant", tenant, Tenant.class), "Default")); + } + + List loadedTenants = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink); + loadedTenants.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenants, idComparator); + Collections.sort(loadedTenants, idComparator); + + Assert.assertEquals(tenants, loadedTenants); + + for (TenantInfo tenant : loadedTenants) { + if (!tenant.getTitle().equals(TEST_TENANT_NAME)) { + doDelete("/api/tenant/"+tenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java new file mode 100644 index 0000000000..a4d615ae2b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java @@ -0,0 +1,294 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.TenantProfileData; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.tenant.TenantProfileService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseTenantProfileControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator tenantProfileInfoIdComparator = new IdComparator<>(); + + @Autowired + private TenantProfileService tenantProfileService; + + @After + @Override + public void teardown() throws Exception { + super.teardown(); + tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveTenantProfile() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + Assert.assertNotNull(savedTenantProfile); + Assert.assertNotNull(savedTenantProfile.getId()); + Assert.assertTrue(savedTenantProfile.getCreatedTime() > 0); + Assert.assertEquals(tenantProfile.getName(), savedTenantProfile.getName()); + Assert.assertEquals(tenantProfile.getDescription(), savedTenantProfile.getDescription()); + Assert.assertEquals(tenantProfile.getProfileData(), savedTenantProfile.getProfileData()); + Assert.assertEquals(tenantProfile.isDefault(), savedTenantProfile.isDefault()); + Assert.assertEquals(tenantProfile.isIsolatedTbCore(), savedTenantProfile.isIsolatedTbCore()); + Assert.assertEquals(tenantProfile.isIsolatedTbRuleEngine(), savedTenantProfile.isIsolatedTbRuleEngine()); + + savedTenantProfile.setName("New tenant profile"); + doPost("/api/tenantProfile", savedTenantProfile, TenantProfile.class); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString(), TenantProfile.class); + Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName()); + } + + @Test + public void testFindTenantProfileById() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString(), TenantProfile.class); + Assert.assertNotNull(foundTenantProfile); + Assert.assertEquals(savedTenantProfile, foundTenantProfile); + } + + @Test + public void testFindTenantProfileInfoById() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + EntityInfo foundTenantProfileInfo = doGet("/api/tenantProfileInfo/"+savedTenantProfile.getId().getId().toString(), EntityInfo.class); + Assert.assertNotNull(foundTenantProfileInfo); + Assert.assertEquals(savedTenantProfile.getId(), foundTenantProfileInfo.getId()); + Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName()); + } + + @Test + public void testFindDefaultTenantProfileInfo() throws Exception { + loginSysAdmin(); + EntityInfo foundDefaultTenantProfile = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + Assert.assertNotNull(foundDefaultTenantProfile); + Assert.assertEquals("Default", foundDefaultTenantProfile.getName()); + } + + @Test + public void testSetDefaultTenantProfile() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile 1"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + TenantProfile defaultTenantProfile = doPost("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString()+"/default", null, TenantProfile.class); + Assert.assertNotNull(defaultTenantProfile); + EntityInfo foundDefaultTenantProfile = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + Assert.assertNotNull(foundDefaultTenantProfile); + Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfile.getName()); + Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfile.getId()); + } + + @Test + public void testSaveTenantProfileWithEmptyName() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = new TenantProfile(); + doPost("/api/tenantProfile", tenantProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Tenant profile name should be specified"))); + } + + @Test + public void testSaveTenantProfileWithSameName() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + doPost("/api/tenantProfile", tenantProfile).andExpect(status().isOk()); + TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile"); + doPost("/api/tenantProfile", tenantProfile2).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Tenant profile with such name already exists"))); + } + + @Test + public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + savedTenantProfile.setIsolatedTbRuleEngine(true); + doPost("/api/tenantProfile", savedTenantProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't update isolatedTbRuleEngine property"))); + } + + @Test + public void testSaveSameTenantProfileWithDifferentIsolatedTbCore() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + savedTenantProfile.setIsolatedTbCore(true); + doPost("/api/tenantProfile", savedTenantProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't update isolatedTbCore property"))); + } + + @Test + public void testDeleteTenantProfileWithExistingTenant() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant with tenant profile"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + + doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("The tenant profile referenced by the tenants cannot be deleted"))); + + doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testDeleteTenantProfile() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testFindTenantProfiles() throws Exception { + loginSysAdmin(); + List tenantProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + tenantProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + List loadedTenantProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + loadedTenantProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfiles, idComparator); + + Assert.assertEquals(tenantProfiles, loadedTenantProfiles); + + for (TenantProfile tenantProfile : loadedTenantProfiles) { + if (!tenantProfile.isDefault()) { + doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindTenantProfileInfos() throws Exception { + loginSysAdmin(); + List tenantProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData tenantProfilePageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(tenantProfilePageData.hasNext()); + Assert.assertEquals(1, tenantProfilePageData.getTotalElements()); + tenantProfiles.addAll(tenantProfilePageData.getData()); + + for (int i=0;i<28;i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + List loadedTenantProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/tenantProfileInfos?", + new TypeReference>(){}, pageLink); + loadedTenantProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfileInfos, tenantProfileInfoIdComparator); + + List tenantProfileInfos = tenantProfiles.stream().map(tenantProfile -> new EntityInfo(tenantProfile.getId(), + tenantProfile.getName())).collect(Collectors.toList()); + + Assert.assertEquals(tenantProfileInfos, loadedTenantProfileInfos); + + for (TenantProfile tenantProfile : tenantProfiles) { + if (!tenantProfile.isDefault()) { + doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenantProfileInfos?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + private TenantProfile createTenantProfile(String name) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName(name); + tenantProfile.setDescription(name + " Test"); + tenantProfile.setProfileData(new TenantProfileData()); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbCore(false); + tenantProfile.setIsolatedTbRuleEngine(false); + return tenantProfile; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index 15da972cf5..64f9b8fddf 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -34,7 +34,7 @@ public class ControllerSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java new file mode 100644 index 0000000000..493c15b578 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseDeviceProfileControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class DeviceProfileControllerSqlTest extends BaseDeviceProfileControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java new file mode 100644 index 0000000000..869fd41470 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseTenantProfileControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TenantProfileControllerSqlTest extends BaseTenantProfileControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java new file mode 100644 index 0000000000..337904718c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java @@ -0,0 +1,246 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.junit.Assert; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest { + + protected static final String MQTT_URL = "tcp://localhost:1883"; + + private static final AtomicInteger atomicInteger = new AtomicInteger(2); + + protected Tenant savedTenant; + protected User tenantAdmin; + + protected Device savedDevice; + protected String accessToken; + + protected Device savedGateway; + protected String gatewayAccessToken; + + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + Device device = new Device(); + device.setName(deviceName); + device.setType("default"); + + Device gateway = new Device(); + gateway.setName(gatewayName); + gateway.setType("default"); + ObjectNode additionalInfo = mapper.createObjectNode(); + additionalInfo.put("gateway", true); + gateway.setAdditionalInfo(additionalInfo); + + if (payloadType != null) { + DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class); + device.setType(savedDeviceProfile.getName()); + device.setDeviceProfileId(savedDeviceProfile.getId()); + gateway.setType(savedDeviceProfile.getName()); + gateway.setDeviceProfileId(savedDeviceProfile.getId()); + } + + savedDevice = doPost("/api/device", device, Device.class); + + DeviceCredentials deviceCredentials = + doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + savedGateway = doPost("/api/device", gateway, Device.class); + + DeviceCredentials gatewayCredentials = + doGet("/api/device/" + savedGateway.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); + accessToken = deviceCredentials.getCredentialsId(); + assertNotNull(accessToken); + + assertEquals(savedGateway.getId(), gatewayCredentials.getDeviceId()); + gatewayAccessToken = gatewayCredentials.getCredentialsId(); + assertNotNull(gatewayAccessToken); + + } + + protected void processAfterTest() throws Exception { + loginSysAdmin(); + if (savedTenant != null) { + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); + } + } + + protected MqttAsyncClient getMqttAsyncClient(String accessToken) throws MqttException { + String clientId = MqttAsyncClient.generateClientId(); + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + client.connect(options).waitForCompletion(); + return client; + } + + protected void publishMqttMsg(MqttAsyncClient client, byte[] payload, String topic) throws MqttException { + MqttMessage message = new MqttMessage(); + message.setPayload(payload); + client.publish(topic, message); + } + + protected List getKvProtos(List expectedKeys) { + List keyValueProtos = new ArrayList<>(); + TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "3.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "4", TransportProtos.KeyValueType.LONG_V); + TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V); + keyValueProtos.add(strKeyValueProto); + keyValueProtos.add(boolKeyValueProto); + keyValueProtos.add(dblKeyValueProto); + keyValueProtos.add(longKeyValueProto); + keyValueProtos.add(jsonKeyValueProto); + return keyValueProtos; + } + + protected TransportProtos.KeyValueProto getKeyValueProto(String key, String strValue, TransportProtos.KeyValueType type) { + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(key); + keyValueProtoBuilder.setType(type); + switch (type) { + case BOOLEAN_V: + keyValueProtoBuilder.setBoolV(Boolean.parseBoolean(strValue)); + break; + case LONG_V: + keyValueProtoBuilder.setLongV(Long.parseLong(strValue)); + break; + case DOUBLE_V: + keyValueProtoBuilder.setDoubleV(Double.parseDouble(strValue)); + break; + case STRING_V: + keyValueProtoBuilder.setStringV(strValue); + break; + case JSON_V: + keyValueProtoBuilder.setJsonV(strValue); + break; + } + return keyValueProtoBuilder.build(); + } + + protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, String telemetryTopic, String attributesTopic) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName(transportPayloadType.name()); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.MQTT); + deviceProfile.setDescription(transportPayloadType.name() + " Test"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration(); + transportConfiguration.setTransportPayloadType(transportPayloadType); + if (!StringUtils.isEmpty(telemetryTopic)) { + transportConfiguration.setDeviceTelemetryTopic(telemetryTopic); + } + if (!StringUtils.isEmpty(attributesTopic)) { + transportConfiguration.setDeviceAttributesTopic(attributesTopic); + } + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfileData.setConfiguration(configuration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + return deviceProfile; + } + + protected TransportProtos.PostAttributeMsg getPostAttributeMsg(List expectedKeys) { + List kvProtos = getKvProtos(expectedKeys); + TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); + builder.addAllKv(kvProtos); + return builder.build(); + } + + protected T doExecuteWithRetriesAndInterval(SupplierWithThrowable supplier, int retries, int intervalMs) throws Exception { + int count = 0; + T result = null; + Throwable lastException = null; + while (count < retries) { + try { + result = supplier.get(); + if (result != null) { + return result; + } + } catch (Throwable e) { + lastException = e; + } + count++; + if (count < retries) { + Thread.sleep(intervalMs); + } + } + if (lastException != null) { + throw new RuntimeException(lastException); + } else { + return result; + } + } + + @FunctionalInterface + public interface SupplierWithThrowable { + T get() throws Throwable; + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java index a062fb7ebe..bebf1ae017 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java @@ -33,7 +33,7 @@ public class MqttNoSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "nosql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java index 0e7234c716..095a5c3e7a 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java @@ -27,13 +27,17 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ "org.thingsboard.server.mqtt.rpc.sql.*Test", - "org.thingsboard.server.mqtt.telemetry.sql.*Test" + "org.thingsboard.server.mqtt.telemetry.timeseries.sql.*Test", + "org.thingsboard.server.mqtt.telemetry.attributes.sql.*Test", + "org.thingsboard.server.mqtt.attributes.updates.sql.*Test", + "org.thingsboard.server.mqtt.attributes.request.sql.*Test", + "org.thingsboard.server.mqtt.claim.sql.*Test" }) public class MqttSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java new file mode 100644 index 0000000000..32488c8eb0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@Slf4j +public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { + + protected static final String POST_ATTRIBUTES_PAYLOAD = "{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73," + + "\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; + + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic); + } + + protected void processAfterTest() throws Exception { + super.processAfterTest(); + } + + protected List getTsKvProtoList() { + TransportProtos.TsKvProto tsKvProtoAttribute1 = getTsKvProto("attribute1", "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.TsKvProto tsKvProtoAttribute2 = getTsKvProto("attribute2", "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.TsKvProto tsKvProtoAttribute3 = getTsKvProto("attribute3", "42.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.TsKvProto tsKvProtoAttribute4 = getTsKvProto("attribute4", "73", TransportProtos.KeyValueType.LONG_V); + TransportProtos.TsKvProto tsKvProtoAttribute5 = getTsKvProto("attribute5", "{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", TransportProtos.KeyValueType.JSON_V); + List tsKvProtoList = new ArrayList<>(); + tsKvProtoList.add(tsKvProtoAttribute1); + tsKvProtoList.add(tsKvProtoAttribute2); + tsKvProtoList.add(tsKvProtoAttribute3); + tsKvProtoList.add(tsKvProtoAttribute4); + tsKvProtoList.add(tsKvProtoAttribute5); + return tsKvProtoList; + } + + + protected TransportProtos.TsKvProto getTsKvProto(String key, String value, TransportProtos.KeyValueType keyValueType) { + TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder(); + TransportProtos.KeyValueProto keyValueProto = getKeyValueProto(key, value, keyValueType); + tsKvProtoBuilder.setKv(keyValueProto); + return tsKvProtoBuilder.build(); + } + + protected TestMqttCallback getTestMqttCallback() { + CountDownLatch latch = new CountDownLatch(1); + return new TestMqttCallback(latch); + } + + protected static class TestMqttCallback implements MqttCallback { + + private final CountDownLatch latch; + private Integer qoS; + private byte[] payloadBytes; + + TestMqttCallback(CountDownLatch latch) { + this.latch = latch; + } + + public int getQoS() { + return qoS; + } + + public byte[] getPayloadBytes() { + return payloadBytes; + } + + public CountDownLatch getLatch() { + return latch; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java new file mode 100644 index 0000000000..9e4529caf2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttAttributesRequestIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Request attribute values from the server", "Gateway Test Request attribute values from the server", null, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServer() throws Exception { + processTestRequestAttributesValuesFromTheServer(); + } + + @Test + public void testRequestAttributesValuesFromTheServerGateway() throws Exception { + processTestGatewayRequestAttributesValuesFromTheServer(); + } + + protected void processTestRequestAttributesValuesFromTheServer() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + postAttributesAndSubscribeToTopic(savedDevice, client); + + Thread.sleep(1000); + + TestMqttCallback callback = getTestMqttCallback(); + client.setCallback(callback); + + validateResponse(client, callback.getLatch(), callback); + } + + protected void processTestGatewayRequestAttributesValuesFromTheServer() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + postGatewayDeviceClientAttributes(client); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + + Thread.sleep(1000); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()); + + TestMqttCallback clientAttributesCallback = getTestMqttCallback(); + client.setCallback(clientAttributesCallback); + validateClientResponseGateway(client, clientAttributesCallback); + + TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); + client.setCallback(sharedAttributesCallback); + validateSharedResponseGateway(client, sharedAttributesCallback); + } + + protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception { + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(POST_ATTRIBUTES_PAYLOAD.getBytes())); + client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + } + + protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { + String postClientAttributes = "{\"" + "Gateway Device Request Attributes" + "\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes())); + } + + protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + String payloadStr = "{\"clientKeys\":\"" + keys + "\", \"sharedKeys\":\"" + keys + "\"}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"client\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}},\"shared\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": true, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": false, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java new file mode 100644 index 0000000000..4b824ea0b5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Request attribute values from the server json", "Gateway Test Request attribute values from the server json", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServer() throws Exception { + processTestRequestAttributesValuesFromTheServer(); + } + + @Test + public void testRequestAttributesValuesFromTheServerGateway() throws Exception { + processTestGatewayRequestAttributesValuesFromTheServer(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java new file mode 100644 index 0000000000..96cc88fa2f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java @@ -0,0 +1,201 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", TransportPayloadType.PROTOBUF, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServer() throws Exception { + processTestRequestAttributesValuesFromTheServer(); + } + + + @Test + public void testRequestAttributesValuesFromTheServerGateway() throws Exception { + processTestGatewayRequestAttributesValuesFromTheServer(); + } + + protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception { + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + List expectedKeys = Arrays.asList(keys.split(",")); + TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); + byte[] payload = postAttributeMsg.toByteArray(); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(payload)); + client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + } + + protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + List expectedKeys = Arrays.asList(keys.split(",")); + TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); + TransportApiProtos.AttributesMsg.Builder attributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + attributesMsgBuilder.setDeviceName("Gateway Device Request Attributes"); + attributesMsgBuilder.setMsg(postAttributeMsg); + TransportApiProtos.AttributesMsg attributesMsg = attributesMsgBuilder.build(); + TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributeMsgBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); + gatewayAttributeMsgBuilder.addMsg(attributesMsg); + byte[] bytes = gatewayAttributeMsgBuilder.build().toByteArray(); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes)); + } + + protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder(); + attributesRequestBuilder.setClientKeys(keys); + attributesRequestBuilder.setSharedKeys(keys); + TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(attributesRequest.toByteArray()); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedAttributesResponse.getRequestId(), actualAttributesResponse.getRequestId()); + List expectedClientKeyValueProtos = expectedAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedKeyValueProtos = expectedAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualClientKeyValueProtos = actualAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualSharedKeyValueProtos = actualAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); + assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); + } + + protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, true); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(true); + TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); + + TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); + assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); + + List expectedClientKeyValueProtos = expectedResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualClientKeyValueProtos = actualResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); + } + + protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, false); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(false); + TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); + + TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); + assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); + + List expectedSharedKeyValueProtos = expectedResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualSharedKeyValueProtos = actualResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); + } + + private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String keys, boolean client) { + return TransportApiProtos.GatewayAttributesRequestMsg.newBuilder() + .setClient(client) + .addAllKeys(Arrays.asList(keys.split(","))) + .setDeviceName("Gateway Device Request Attributes") + .setId(1).build(); + } + + private TransportProtos.GetAttributeResponseMsg getExpectedAttributeResponseMsg() { + TransportProtos.GetAttributeResponseMsg.Builder result = TransportProtos.GetAttributeResponseMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + result.addAllClientAttributeList(tsKvProtoList); + result.addAllSharedAttributeList(tsKvProtoList); + result.setRequestId(1); + return result.build(); + } + + private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(boolean client) { + TransportApiProtos.GatewayAttributeResponseMsg.Builder gatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); + TransportProtos.GetAttributeResponseMsg.Builder getAttributeResponseMsgBuilder = TransportProtos.GetAttributeResponseMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + if (client) { + getAttributeResponseMsgBuilder.addAllClientAttributeList(tsKvProtoList); + } else { + getAttributeResponseMsgBuilder.addAllSharedAttributeList(tsKvProtoList); + } + getAttributeResponseMsgBuilder.setRequestId(1); + TransportProtos.GetAttributeResponseMsg getAttributeResponseMsg = getAttributeResponseMsgBuilder.build(); + gatewayAttributeResponseMsg.setDeviceName("Gateway Device Request Attributes"); + gatewayAttributeResponseMsg.setResponseMsg(getAttributeResponseMsg); + return gatewayAttributeResponseMsg.build(); + } + + protected List getKvProtos(List expectedKeys) { + List keyValueProtos = new ArrayList<>(); + TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "42.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "73", TransportProtos.KeyValueType.LONG_V); + TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V); + keyValueProtos.add(strKeyValueProto); + keyValueProtos.add(boolKeyValueProto); + keyValueProtos.add(dblKeyValueProto); + keyValueProtos.add(longKeyValueProto); + keyValueProtos.add(jsonKeyValueProto); + return keyValueProtos; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java new file mode 100644 index 0000000000..e94ad9519c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; + + +@DaoNoSqlTest +public class MqttAttributesRequestNoSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java new file mode 100644 index 0000000000..d3750d49a3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestJsonSqlIntegrationTest extends AbstractMqttAttributesRequestJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java new file mode 100644 index 0000000000..f52e79b2ab --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestProtoIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestProtoSqlIntegrationTest extends AbstractMqttAttributesRequestProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java new file mode 100644 index 0000000000..af18294b47 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java new file mode 100644 index 0000000000..d2febdf357 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + private static final String RESPONSE_ATTRIBUTES_PAYLOAD_DELETED = "{\"deleted\":[\"attribute5\"]}"; + + private static String getResponseGatewayAttributesUpdatedPayload() { + return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\"," + + "\"data\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + } + + private static String getResponseGatewayAttributesDeletedPayload() { + return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\",\"data\":{\"deleted\":[\"attribute5\"]}}"; + } + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processTestSubscribeToAttributesUpdates(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processGatewayTestSubscribeToAttributesUpdates(); + } + + protected void processTestSubscribeToAttributesUpdates() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateUpdateAttributesResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateDeleteAttributesResponse(onDeleteCallback); + } + + protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response)); + } + + protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response)); + } + + protected void processGatewayTestSubscribeToAttributesUpdates() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + Device device = new Device(); + device.setName("Gateway Device Subscribe to attribute updates"); + device.setType("default"); + + byte[] connectPayloadBytes = getConnectPayloadBytes(); + + publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateGatewayUpdateAttributesResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateGatewayDeleteAttributesResponse(onDeleteCallback); + + } + + protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(getResponseGatewayAttributesUpdatedPayload(), s); + } + + protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(s, getResponseGatewayAttributesDeletedPayload()); + } + + protected byte[] getConnectPayloadBytes() { + String connectPayload = "{\"device\": \"Gateway Device Subscribe to attribute updates\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + return connectPayload.getBytes(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java new file mode 100644 index 0000000000..58379e4016 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processTestSubscribeToAttributesUpdates(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processGatewayTestSubscribeToAttributesUpdates(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java new file mode 100644 index 0000000000..faf8e1ce4d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates; + +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processTestSubscribeToAttributesUpdates(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processGatewayTestSubscribeToAttributesUpdates(); + } + + protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + List actualSharedUpdatedList = actualAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedUpdatedList = expectedAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); + assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); + + } + + protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); + assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + + } + + protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(expectedAttributeUpdateNotificationMsg); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); + TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); + + List actualSharedUpdatedList = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedUpdatedList = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); + assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); + + } + + protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(attributeUpdateNotificationMsg); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); + TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); + + assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); + assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + + } + + protected byte[] getConnectPayloadBytes() { + TransportApiProtos.ConnectMsg connectProto = getConnectProto(); + return connectProto.toByteArray(); + } + + private TransportApiProtos.ConnectMsg getConnectProto() { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName("Gateway Device Subscribe to attribute updates"); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java new file mode 100644 index 0000000000..993e0869e4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; + + +@DaoNoSqlTest +public class MqttAttributesUpdatesNoSqlIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java new file mode 100644 index 0000000000..cdafc3a9ac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesSqlIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..a8fd4687f7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesSqlJsonIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..723e5e3dcd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesProtoIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesSqlProtoIntegrationTest extends AbstractMqttAttributesUpdatesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java new file mode 100644 index 0000000000..dd16c17be2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java @@ -0,0 +1,206 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.ClaimRequest; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.device.claim.ClaimResponse; +import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttClaimDeviceTest extends AbstractMqttIntegrationTest { + + protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; + + protected User customerAdmin; + protected Customer savedCustomer; + + @Before + public void beforeTest() throws Exception { + super.processBeforeTest("Test Claim device", "Test Claim gateway", null, null, null); + createCustomerAndUser(); + } + + protected void createCustomerAndUser() throws Exception { + Customer customer = new Customer(); + customer.setTenantId(savedTenant.getId()); + customer.setTitle("Test Claiming Customer"); + savedCustomer = doPost("/api/customer", customer, Customer.class); + assertNotNull(savedCustomer); + assertEquals(savedTenant.getId(), savedCustomer.getTenantId()); + + User user = new User(); + user.setAuthority(Authority.CUSTOMER_USER); + user.setTenantId(savedTenant.getId()); + user.setCustomerId(savedCustomer.getId()); + user.setEmail("customer@thingsboard.org"); + + customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); + assertNotNull(customerAdmin); + assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testClaimingDevice() throws Exception { + processTestClaimingDevice(false); + } + + @Test + public void testClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestClaimingDevice(true); + } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload", true); + } + + + protected void processTestClaimingDevice(boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + byte[] payloadBytes; + byte[] failurePayloadBytes; + if (emptyPayload) { + payloadBytes = "{}".getBytes(); + failurePayloadBytes = "{\"durationMs\":1}".getBytes(); + } else { + payloadBytes = "{\"secretKey\":\"value\", \"durationMs\":60000}".getBytes(); + failurePayloadBytes = "{\"secretKey\":\"value\", \"durationMs\":1}".getBytes(); + } + validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes); + } + + protected void validateClaimResponse(boolean emptyPayload, MqttAsyncClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { + client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes)); + + loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + ClaimRequest claimRequest; + if (!emptyPayload) { + claimRequest = new ClaimRequest("value"); + } else { + claimRequest = new ClaimRequest(null); + } + + ClaimResponse claimResponse = doExecuteWithRetriesAndInterval( + () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()), + 20, + 100 + ); + + assertEquals(claimResponse, ClaimResponse.FAILURE); + + client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(payloadBytes)); + + ClaimResult claimResult = doExecuteWithRetriesAndInterval( + () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), + 20, + 100 + ); + assertEquals(claimResult.getResponse(), ClaimResponse.SUCCESS); + Device claimedDevice = claimResult.getDevice(); + assertNotNull(claimedDevice); + assertNotNull(claimedDevice.getCustomerId()); + assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + + claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); + assertEquals(claimResponse, ClaimResponse.CLAIMED); + } + + protected void validateGatewayClaimResponse(String deviceName, boolean emptyPayload, MqttAsyncClient client, byte[] failurePayloadBytes, byte[] payloadBytes) throws Exception { + client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes)); + + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100 + ); + + assertNotNull(savedDevice); + + loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + ClaimRequest claimRequest; + if (!emptyPayload) { + claimRequest = new ClaimRequest("value"); + } else { + claimRequest = new ClaimRequest(null); + } + + ClaimResponse claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); + assertEquals(claimResponse, ClaimResponse.FAILURE); + + client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(payloadBytes)); + + ClaimResult claimResult = doExecuteWithRetriesAndInterval( + () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()), + 20, + 100 + ); + + assertEquals(claimResult.getResponse(), ClaimResponse.SUCCESS); + Device claimedDevice = claimResult.getDevice(); + assertNotNull(claimedDevice); + assertNotNull(claimedDevice.getCustomerId()); + assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + + claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); + assertEquals(claimResponse, ClaimResponse.CLAIMED); + } + + protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + byte[] failurePayloadBytes; + byte[] payloadBytes; + String failurePayload; + String payload; + if (emptyPayload) { + failurePayload = "{\"" + deviceName + "\": " + "{\"durationMs\":1}" + "}"; + payload = "{\"" + deviceName + "\": " + "{}" + "}"; + } else { + failurePayload = "{\"" + deviceName + "\": " + "{\"secretKey\":\"value\", \"durationMs\":1}" + "}"; + payload = "{\"" + deviceName + "\": " + "{\"secretKey\":\"value\", \"durationMs\":60000}" + "}"; + } + payloadBytes = payload.getBytes(); + failurePayloadBytes = failurePayload.getBytes(); + validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java new file mode 100644 index 0000000000..f55cfa57c8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +@Slf4j +public abstract class AbstractMqttClaimJsonDeviceTest extends AbstractMqttClaimDeviceTest { + + @Before + public void beforeTest() throws Exception { + super.processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.JSON, null, null); + createCustomerAndUser(); + } + + @After + public void afterTest() throws Exception { + super.afterTest(); + } + + @Test + public void testClaimingDevice() throws Exception { + processTestClaimingDevice(false); + } + + @Test + public void testClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestClaimingDevice(true); + } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device Json", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload Json", true); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java new file mode 100644 index 0000000000..d371c09f37 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.gen.transport.TransportApiProtos; + +@Slf4j +public abstract class AbstractMqttClaimProtoDeviceTest extends AbstractMqttClaimDeviceTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.PROTOBUF, null, null); + createCustomerAndUser(); + } + + @After + public void afterTest() throws Exception { super.afterTest(); } + + @Test + public void testClaimingDevice() throws Exception { + processTestClaimingDevice(false); + } + + @Test + public void testClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestClaimingDevice(true); + } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device Proto", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload Proto", true); + } + + protected void processTestClaimingDevice(boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + byte[] payloadBytes; + if (emptyPayload) { + payloadBytes = getClaimDevice(0, emptyPayload).toByteArray(); + } else { + payloadBytes = getClaimDevice(60000, emptyPayload).toByteArray(); + } + byte[] failurePayloadBytes = getClaimDevice(1, emptyPayload).toByteArray(); + validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes); + } + + protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + byte[] failurePayloadBytes; + byte[] payloadBytes; + if (emptyPayload) { + payloadBytes = getGatewayClaimMsg(deviceName, 0, emptyPayload).toByteArray(); + } else { + payloadBytes = getGatewayClaimMsg(deviceName, 60000, emptyPayload).toByteArray(); + } + failurePayloadBytes = getGatewayClaimMsg(deviceName, 1, emptyPayload).toByteArray(); + + validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); + } + + private TransportApiProtos.GatewayClaimMsg getGatewayClaimMsg(String deviceName, long duration, boolean emptyPayload) { + TransportApiProtos.GatewayClaimMsg.Builder gatewayClaimMsgBuilder = TransportApiProtos.GatewayClaimMsg.newBuilder(); + TransportApiProtos.ClaimDeviceMsg.Builder claimDeviceMsgBuilder = TransportApiProtos.ClaimDeviceMsg.newBuilder(); + TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder(); + if (!emptyPayload) { + claimDeviceBuilder.setSecretKey("value"); + } + if (duration > 0) { + claimDeviceBuilder.setDurationMs(duration); + } + TransportApiProtos.ClaimDevice claimDevice = claimDeviceBuilder.build(); + claimDeviceMsgBuilder.setClaimRequest(claimDevice); + claimDeviceMsgBuilder.setDeviceName(deviceName); + TransportApiProtos.ClaimDeviceMsg claimDeviceMsg = claimDeviceMsgBuilder.build(); + gatewayClaimMsgBuilder.addMsg(claimDeviceMsg); + return gatewayClaimMsgBuilder.build(); + } + + private TransportApiProtos.ClaimDevice getClaimDevice(long duration, boolean emptyPayload) { + TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder(); + if (!emptyPayload) { + claimDeviceBuilder.setSecretKey("value"); + } + if (duration > 0) { + claimDeviceBuilder.setSecretKey("value"); + claimDeviceBuilder.setDurationMs(duration); + } + return claimDeviceBuilder.build(); + } + + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java new file mode 100644 index 0000000000..72b9f95328 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest; + + +@DaoNoSqlTest +public class MqttClaimDeviceNoSqlTest extends AbstractMqttClaimDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java new file mode 100644 index 0000000000..da794288f4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimJsonDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceJsonSqlTest extends AbstractMqttClaimJsonDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java new file mode 100644 index 0000000000..a63978e4de --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimJsonDeviceTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimProtoDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceProtoSqlTest extends AbstractMqttClaimProtoDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java new file mode 100644 index 0000000000..ff0c2becb7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceSqlTest extends AbstractMqttClaimDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java new file mode 100644 index 0000000000..23b93f427e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java @@ -0,0 +1,137 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.InvalidProtocolBufferException; +import com.nimbusds.jose.util.StandardCharset; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.service.security.AccessValidator; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Valerii Sosliuk + */ +@Slf4j +public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("RPC test device", "RPC test gateway", null, null, null); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpcDeviceOffline() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}"; + String deviceId = savedDevice.getId().getId().toString(); + + doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409), + asyncContextTimeoutToUseRpcPlugin); + } + + @Test + public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}"; + String nonExistentDeviceId = Uuids.timeBased().toString(); + + String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class, + status().isNotFound()); + Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + } + + @Test + public void testServerMqttTwoWayRpcDeviceOffline() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}"; + String deviceId = savedDevice.getId().getId().toString(); + + doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409), + asyncContextTimeoutToUseRpcPlugin); + } + + @Test + public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}"; + String nonExistentDeviceId = Uuids.timeBased().toString(); + + String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class, + status().isNotFound()); + Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + } + + @Test + public void testServerMqttOneWayRpc() throws Exception { + processOneWayRpcTest(); + } + + @Test + public void testServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTest(); + } + + @Test + public void testGatewayServerMqttOneWayRpc() throws Exception { + processOneWayRpcTestGateway("Gateway Device OneWay RPC"); + } + + @Test + public void testGatewayServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTestGateway("Gateway Device TwoWay RPC"); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index abd13f99a6..e08f1665a4 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -16,6 +16,10 @@ package org.thingsboard.server.mqtt.rpc; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.InvalidProtocolBufferException; +import com.nimbusds.jose.util.StandardCharset; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -23,15 +27,28 @@ import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.junit.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.controller.AbstractControllerTest; -import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.service.security.AccessValidator; import java.util.Arrays; @@ -47,72 +64,27 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Valerii Sosliuk */ @Slf4j -public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractControllerTest { +public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractMqttIntegrationTest { - private static final String MQTT_URL = "tcp://localhost:1883"; - private static final Long TIME_TO_HANDLE_REQUEST = 500L; + protected static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; - private Tenant savedTenant; - private User tenantAdmin; - private Long asyncContextTimeoutToUseRpcPlugin; - - private static final AtomicInteger atomicInteger = new AtomicInteger(2); - - - @Before - public void beforeTest() throws Exception { - loginSysAdmin(); + protected Long asyncContextTimeoutToUseRpcPlugin; + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic); asyncContextTimeoutToUseRpcPlugin = 10000L; - - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - savedTenant = doPost("/api/tenant", tenant, Tenant.class); - Assert.assertNotNull(savedTenant); - - tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(savedTenant.getId()); - tenantAdmin.setEmail("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org"); - tenantAdmin.setFirstName("Joe"); - tenantAdmin.setLastName("Downs"); - - createUserAndLogin(tenantAdmin, "testPassword1"); } - @After - public void afterTest() throws Exception { - loginSysAdmin(); - if (savedTenant != null) { - doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); - } - } - - @Test - public void testServerMqttOneWayRpc() throws Exception { - Device device = new Device(); - device.setName("Test One-Way Server-Side RPC"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options).waitForCompletion(); + protected void processOneWayRpcTest() throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); CountDownLatch latch = new CountDownLatch(1); TestMqttCallback callback = new TestMqttCallback(client, latch); client.setCallback(callback); - client.subscribe("v1/devices/me/rpc/request/+", MqttQoS.AT_MOST_ONCE.value()); + client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - Thread.sleep(2000); + Thread.sleep(1000); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); @@ -122,100 +94,112 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - @Test - public void testServerMqttOneWayRpcDeviceOffline() throws Exception { - Device device = new Device(); - device.setName("Test One-Way Server-Side RPC Device Offline"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}"; + protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + String payload = "{\"device\":\"" + deviceName + "\"}"; + byte[] payloadBytes = payload.getBytes(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + + protected void processTwoWayRpcTest() throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + + Thread.sleep(1000); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); - doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409), - asyncContextTimeoutToUseRpcPlugin); + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + String expected = "{\"value1\":\"A\",\"value2\":\"B\"}"; + latch.await(3, TimeUnit.SECONDS); + Assert.assertEquals(expected, result); } - @Test - public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception { - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}"; - String nonExistentDeviceId = Uuids.timeBased().toString(); + protected void processTwoWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + String payload = "{\"device\":\"" + deviceName + "\"}"; + byte[] payloadBytes = payload.getBytes(); - String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class, - status().isNotFound()); - Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + validateTwoWayRpcGateway(deviceName, client, payloadBytes); } - @Test - public void testServerMqttTwoWayRpc() throws Exception { - Device device = new Device(); - device.setName("Test Two-Way Server-Side RPC"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); + protected void validateOneWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { + publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> getDeviceByName(deviceName), + 20, + 100 + ); + assertNotNull(savedDevice); - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options).waitForCompletion(); - client.subscribe("v1/devices/me/rpc/request/+", 1); - client.setCallback(new TestMqttCallback(client, new CountDownLatch(1))); + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); - Thread.sleep(2000); + client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; - String deviceId = savedDevice.getId().getId().toString(); + Thread.sleep(1000); - String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result); + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; + String deviceId = savedDevice.getId().getId().toString(); + String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); + Assert.assertTrue(StringUtils.isEmpty(result)); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - @Test - public void testServerMqttTwoWayRpcDeviceOffline() throws Exception { - Device device = new Device(); - device.setName("Test Two-Way Server-Side RPC Device Offline"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}"; - String deviceId = savedDevice.getId().getId().toString(); + protected void validateTwoWayRpcGateway(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { + publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409), - asyncContextTimeoutToUseRpcPlugin); - } + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> getDeviceByName(deviceName), + 20, + 100 + ); + assertNotNull(savedDevice); + + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + + client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - @Test - public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception { - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}"; - String nonExistentDeviceId = Uuids.timeBased().toString(); + Thread.sleep(1000); - String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class, - status().isNotFound()); - Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; + String deviceId = savedDevice.getId().getId().toString(); + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + latch.await(3, TimeUnit.SECONDS); + String expected = "{\"success\":true}"; + assertEquals(expected, result); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - private Device getSavedDevice(Device device) throws Exception { - return doPost("/api/device", device, Device.class); + private Device getDeviceByName(String deviceName) throws Exception { + return doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); } - private DeviceCredentials getDeviceCredentials(Device savedDevice) throws Exception { - return doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); + protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + MqttMessage message = new MqttMessage(); + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { + message.setPayload(DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8)); + } else { + JsonNode requestMsgNode = JacksonUtil.toJsonNode(new String(mqttMessage.getPayload(), StandardCharset.UTF_8)); + String deviceName = requestMsgNode.get("device").asText(); + int requestId = requestMsgNode.get("data").get("id").asInt(); + message.setPayload(("{\"device\": \"" + deviceName + "\", \"id\": " + requestId + ", \"data\": {\"success\": true}}").getBytes(StandardCharset.UTF_8)); + } + return message; } - private static class TestMqttCallback implements MqttCallback { + private class TestMqttCallback implements MqttCallback { private final MqttAsyncClient client; private final CountDownLatch latch; @@ -237,11 +221,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC @Override public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); - MqttMessage message = new MqttMessage(); String responseTopic = requestTopic.replace("request", "response"); - message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes("UTF-8")); qoS = mqttMessage.getQos(); - client.publish(responseTopic, message); + client.publish(responseTopic, processMessageArrived(requestTopic, mqttMessage)); latch.countDown(); } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java new file mode 100644 index 0000000000..d9ff14e1d2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +@Slf4j +public abstract class AbstractMqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpc() throws Exception { + processOneWayRpcTest(); + } + + @Test + public void testServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTest(); + } + + @Test + public void testGatewayServerMqttOneWayRpc() throws Exception { + processOneWayRpcTestGateway("Gateway Device OneWay RPC Json"); + } + + @Test + public void testGatewayServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Json"); + } + + protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + String payload = "{\"device\": \"" + deviceName + "\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + byte[] payloadBytes = payload.getBytes(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java new file mode 100644 index 0000000000..759a5da912 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc; + +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Slf4j +public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpc() throws Exception { + processOneWayRpcTest(); + } + + @Test + public void testServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTest(); + } + + @Test + public void testGatewayServerMqttOneWayRpc() throws Exception { + processOneWayRpcTestGateway("Gateway Device OneWay RPC Proto"); + } + + @Test + public void testGatewayServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Proto"); + } + + protected void processTwoWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + byte[] payloadBytes = connectMsgProto.toByteArray(); + validateTwoWayRpcGateway(deviceName, client, payloadBytes); + } + + protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + byte[] payloadBytes = connectMsgProto.toByteArray(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + + + private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + + protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + MqttMessage message = new MqttMessage(); + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { + TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setPayload(DEVICE_RESPONSE) + .setRequestId(0) + .build(); + message.setPayload(toDeviceRpcResponseMsg.toByteArray()); + } else { + TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); + String deviceName = msg.getDeviceName(); + int requestId = msg.getRpcRequestMsg().getRequestId(); + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.newBuilder() + .setDeviceName(deviceName) + .setId(requestId) + .setData("{\"success\": true}") + .build(); + message.setPayload(gatewayRpcResponseMsg.toByteArray()); + } + return message; + } + + + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java index e90c48a974..6a5cb69c52 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java @@ -16,11 +16,11 @@ package org.thingsboard.server.mqtt.rpc.nosql; import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcDefaultIntegrationTest; /** * Created by Valerii Sosliuk on 8/22/2017. */ @DaoNoSqlTest -public class MqttServerSideRpcNoSqlIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { +public class MqttServerSideRpcNoSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java new file mode 100644 index 0000000000..4d4e900767 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcJsonIntegrationTest; + +@DaoSqlTest +public class MqttServerSideRpcJsonSqlIntegrationTest extends AbstractMqttServerSideRpcJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java new file mode 100644 index 0000000000..7fb91a636c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcProtoIntegrationTest; + + +@DaoSqlTest +public class MqttServerSideRpcProtoSqlIntegrationTest extends AbstractMqttServerSideRpcProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java index dc2511f4c3..7bddfbbe52 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java @@ -16,11 +16,11 @@ package org.thingsboard.server.mqtt.rpc.sql; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcDefaultIntegrationTest; /** * Created by Valerii Sosliuk on 8/22/2017. */ @DaoSqlTest -public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { +public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java deleted file mode 100644 index 47e9537ef9..0000000000 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.mqtt.telemetry; - -import io.netty.handler.codec.mqtt.MqttQoS; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.*; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.springframework.web.util.UriComponentsBuilder; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.controller.AbstractControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -import java.net.URI; -import java.util.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * @author Valerii Sosliuk - */ -@Slf4j -public abstract class AbstractMqttTelemetryIntegrationTest extends AbstractControllerTest { - - private static final String MQTT_URL = "tcp://localhost:1883"; - - private Device savedDevice; - private String accessToken; - - @Before - public void beforeTest() throws Exception { - loginTenantAdmin(); - - Device device = new Device(); - device.setName("Test device"); - device.setType("default"); - savedDevice = doPost("/api/device", device, Device.class); - - DeviceCredentials deviceCredentials = - doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); - - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - } - - @Test - public void testPushMqttRpcData() throws Exception { - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options); - Thread.sleep(3000); - MqttMessage message = new MqttMessage(); - message.setPayload("{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4}".getBytes()); - client.publish("v1/devices/me/telemetry", message); - - String deviceId = savedDevice.getId().getId().toString(); - - Thread.sleep(2000); - List actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", List.class); - Set actualKeySet = new HashSet<>(actualKeys); - - List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4"); - Set expectedKeySet = new HashSet<>(expectedKeys); - - assertEquals(expectedKeySet, actualKeySet); - - String getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + String.join(",", actualKeySet); - Map>> values = doGetAsync(getTelemetryValuesUrl, Map.class); - - assertEquals("value1", values.get("key1").get(0).get("value")); - assertEquals("true", values.get("key2").get(0).get("value")); - assertEquals("3.0", values.get("key3").get(0).get("value")); - assertEquals("4", values.get("key4").get(0).get("value")); - } - - -// @Test - Unstable - public void testMqttQoSLevel() throws Exception { - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); - client.setCallback(callback); - client.connect(options).waitForCompletion(5000); - client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value()); - String payload = "{\"key\":\"uniqueValue\"}"; -// TODO 3.1: we need to acknowledge subscription only after it is processed by device actor and not when the message is pushed to queue. -// MqttClient -> SUB REQUEST -> Transport -> Kafka -> Device Actor (subscribed) -// MqttClient <- SUB_ACK <- Transport - Thread.sleep(5000); - doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk()); - latch.await(10, TimeUnit.SECONDS); - assertEquals(payload, callback.getPayload()); - assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); - } - - private static class TestMqttCallback implements MqttCallback { - - private final MqttAsyncClient client; - private final CountDownLatch latch; - private volatile Integer qoS; - private volatile String payload; - - String getPayload() { - return payload; - } - - TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) { - this.client = client; - this.latch = latch; - } - - int getQoS() { - return qoS; - } - - @Override - public void connectionLost(Throwable throwable) { - log.error("Client connection lost", throwable); - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) { - payload = new String(mqttMessage.getPayload()); - qoS = mqttMessage.getQos(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - - } - } - - -} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java new file mode 100644 index 0000000000..5ac0746a43 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java @@ -0,0 +1,182 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { + + protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + + " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", null, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttAttributes() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + } + + @Test + public void testPushMqttAttributesGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayAttributesJsonPayload(deviceName1, deviceName2); + processGatewayAttributesTest(expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } + + protected void processAttributesTest(String topic, List expectedKeys, byte[] payload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + publishMqttMsg(client, payload, topic); + + DeviceId deviceId = savedDevice.getId(); + + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", List.class); + if (actualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, actualKeySet); + + String getAttributesValuesUrl = getAttributesValuesUrl(deviceId, actualKeySet); + List> values = doGetAsync(getAttributesValuesUrl, List.class); + assertAttributesValues(values, expectedKeySet); + String deleteAttributesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + doDelete(deleteAttributesUrl); + } + + protected void processGatewayAttributesTest(List expectedKeys, byte[] payload, String firstDeviceName, String secondDeviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + publishMqttMsg(client, payload, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC); + + Device firstDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + firstDeviceName, Device.class), + 20, + 100); + + assertNotNull(firstDevice); + + Device secondDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + secondDeviceName, Device.class), + 20, + 100); + + assertNotNull(secondDevice); + + Thread.sleep(2000); + + List firstDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/attributes/CLIENT_SCOPE", List.class); + Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); + + List secondDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/attributes/CLIENT_SCOPE", List.class); + Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); + + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, firstDeviceActualKeySet); + assertEquals(expectedKeySet, secondDeviceActualKeySet); + + String getAttributesValuesUrlFirstDevice = getAttributesValuesUrl(firstDevice.getId(), firstDeviceActualKeySet); + String getAttributesValuesUrlSecondDevice = getAttributesValuesUrl(firstDevice.getId(), secondDeviceActualKeySet); + + List> firstDeviceValues = doGetAsync(getAttributesValuesUrlFirstDevice, List.class); + List> secondDeviceValues = doGetAsync(getAttributesValuesUrlSecondDevice, List.class); + + assertAttributesValues(firstDeviceValues, expectedKeySet); + assertAttributesValues(secondDeviceValues, expectedKeySet); + + } + + protected void assertAttributesValues(List> deviceValues, Set expectedKeySet) { + for (Map map : deviceValues) { + String key = (String) map.get("key"); + Object value = map.get("value"); + assertTrue(expectedKeySet.contains(key)); + switch (key) { + case "key1": + assertEquals("value1", value); + break; + case "key2": + assertEquals(true, value); + break; + case "key3": + assertEquals(3.0, value); + break; + case "key4": + assertEquals(4, value); + break; + case "key5": + assertNotNull(value); + assertEquals(3, ((LinkedHashMap) value).size()); + assertEquals(42, ((LinkedHashMap) value).get("someNumber")); + assertEquals(Arrays.asList(1, 2, 3), ((LinkedHashMap) value).get("someArray")); + LinkedHashMap someNestedObject = (LinkedHashMap) ((LinkedHashMap) value).get("someNestedObject"); + assertEquals("value", someNestedObject.get("key")); + break; + } + } + } + + protected String getGatewayAttributesJsonPayload(String deviceA, String deviceB) { + return "{\"" + deviceA + "\": " + PAYLOAD_VALUES_STR + ", \"" + deviceB + "\": " + PAYLOAD_VALUES_STR + "}"; + } + + private String getAttributesValuesUrl(DeviceId deviceId, Set actualKeySet) { + return "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/attributes/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java new file mode 100644 index 0000000000..fc79131ef1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +public abstract class AbstractMqttAttributesJsonIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + private static final String POST_DATA_ATTRIBUTES_TOPIC = "data/attributes"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.JSON, null, POST_DATA_ATTRIBUTES_TOPIC); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttAttributes() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + } + + @Test + public void testPushMqttAttributesGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayAttributesJsonPayload(deviceName1, deviceName2); + processGatewayAttributesTest(expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java new file mode 100644 index 0000000000..e9adf19359 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesProtoIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + private static final String POST_DATA_ATTRIBUTES_TOPIC = "proto/attributes"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttAttributes() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + TransportProtos.PostAttributeMsg msg = getPostAttributeMsg(expectedKeys); + processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, expectedKeys, msg.toByteArray()); + } + + @Test + public void testPushMqttAttributesGateway() throws Exception { + TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributesMsgProtoBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.AttributesMsg firstDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName1, expectedKeys); + TransportApiProtos.AttributesMsg secondDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName2, expectedKeys); + gatewayAttributesMsgProtoBuilder.addAllMsg(Arrays.asList(firstDeviceAttributesMsgProto, secondDeviceAttributesMsgProto)); + TransportApiProtos.GatewayAttributesMsg gatewayAttributesMsg = gatewayAttributesMsgProtoBuilder.build(); + processGatewayAttributesTest(expectedKeys, gatewayAttributesMsg.toByteArray(), deviceName1, deviceName2); + } + + private TransportApiProtos.AttributesMsg getDeviceAttributesMsgProto(String deviceName, List expectedKeys) { + TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + TransportProtos.PostAttributeMsg msg = getPostAttributeMsg(expectedKeys); + deviceAttributesMsgBuilder.setDeviceName(deviceName); + deviceAttributesMsgBuilder.setMsg(msg); + return deviceAttributesMsgBuilder.build(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlIntegrationTest.java new file mode 100644 index 0000000000..e6dce8d9d5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; + +@DaoNoSqlTest +public class MqttAttributesNoSqlIntegrationTest extends AbstractMqttAttributesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..68ab8cff62 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlJsonIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; + +@DaoNoSqlTest +public class MqttAttributesNoSqlJsonIntegrationTest extends AbstractMqttAttributesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..d508fd76c5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlProtoIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesProtoIntegrationTest; + +@DaoNoSqlTest +public class MqttAttributesNoSqlProtoIntegrationTest extends AbstractMqttAttributesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java new file mode 100644 index 0000000000..24dc362757 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; + +@DaoSqlTest +public class MqttAttributesSqlIntegrationTest extends AbstractMqttAttributesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..dcf1fb3026 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; + +@DaoSqlTest +public class MqttAttributesSqlJsonIntegrationTest extends AbstractMqttAttributesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..5f486916ae --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesProtoIntegrationTest; + +@DaoSqlTest +public class MqttAttributesSqlProtoIntegrationTest extends AbstractMqttAttributesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java new file mode 100644 index 0000000000..f6294cd990 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -0,0 +1,300 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries; + +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqttIntegrationTest { + + protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + + " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Telemetry device", "Test Post Telemetry gateway", null, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttTelemetry() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + } + + @Test + public void testPushMqttTelemetryWithTs() throws Exception { + String payloadStr = "{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}"; + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); + } + + @Test + public void testPushMqttTelemetryGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayTelemetryJsonPayload(deviceName1, deviceName2, "10000", "20000"); + processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } + + @Test + public void testGatewayConnect() throws Exception { + String payload = "{\"device\":\"Device A\"}"; + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + publishMqttMsg(client, payload.getBytes(), MqttTopics.GATEWAY_CONNECT_TOPIC); + + String deviceName = "Device A"; + + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100); + + assertNotNull(device); + } + + protected void processTelemetryTest(String topic, List expectedKeys, byte[] payload, boolean withTs) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + publishMqttMsg(client, payload, topic); + + String deviceId = savedDevice.getId().getId().toString(); + + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", List.class); + if (actualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, actualKeySet); + + String getTelemetryValuesUrl; + if (withTs) { + getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?startTs=0&endTs=15000&keys=" + String.join(",", actualKeySet); + } else { + getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + String.join(",", actualKeySet); + } + Map>> values = doGetAsync(getTelemetryValuesUrl, Map.class); + + if (withTs) { + assertTs(values, expectedKeys, 10000, 0); + } + assertValues(values, 0); + } + + protected void processGatewayTelemetryTest(String topic, List expectedKeys, byte[] payload, String firstDeviceName, String secondDeviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + publishMqttMsg(client, payload, topic); + + Device firstDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + firstDeviceName, Device.class), + 20, + 100); + + assertNotNull(firstDevice); + + Device secondDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + secondDeviceName, Device.class), + 20, + 100); + + assertNotNull(secondDevice); + + Thread.sleep(2000); + + List firstDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/timeseries", List.class); + Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); + + List secondDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/timeseries", List.class); + Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); + + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, firstDeviceActualKeySet); + assertEquals(expectedKeySet, secondDeviceActualKeySet); + + String getTelemetryValuesUrlFirstDevice = getTelemetryValuesUrl(firstDevice.getId(), firstDeviceActualKeySet); + String getTelemetryValuesUrlSecondDevice = getTelemetryValuesUrl(firstDevice.getId(), secondDeviceActualKeySet); + + Map>> firstDeviceValues = doGetAsync(getTelemetryValuesUrlFirstDevice, Map.class); + Map>> secondDeviceValues = doGetAsync(getTelemetryValuesUrlSecondDevice, Map.class); + + assertGatewayDeviceData(firstDeviceValues, expectedKeys); + assertGatewayDeviceData(secondDeviceValues, expectedKeys); + } + + protected String getGatewayTelemetryJsonPayload(String deviceA, String deviceB, String firstTsValue, String secondTsValue) { + String payload = "[{\"ts\": " + firstTsValue + ", \"values\": " + PAYLOAD_VALUES_STR + "}, " + + "{\"ts\": " + secondTsValue + ", \"values\": " + PAYLOAD_VALUES_STR + "}]"; + return "{\"" + deviceA + "\": " + payload + ", \"" + deviceB + "\": " + payload + "}"; + } + + private String getTelemetryValuesUrl(DeviceId deviceId, Set actualKeySet) { + return "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?startTs=0&endTs=25000&keys=" + String.join(",", actualKeySet); + } + + private void assertGatewayDeviceData(Map>> deviceValues, List expectedKeys) { + + assertEquals(2, deviceValues.get(expectedKeys.get(0)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(1)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(2)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(3)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(4)).size()); + + assertTs(deviceValues, expectedKeys, 20000, 0); + assertTs(deviceValues, expectedKeys, 10000, 1); + + assertValues(deviceValues, 0); + assertValues(deviceValues, 1); + + } + + private void assertValues(Map>> deviceValues, int arrayIndex) { + for (Map.Entry>> entry : deviceValues.entrySet()) { + String key = entry.getKey(); + List> tsKv = entry.getValue(); + String value = tsKv.get(arrayIndex).get("value"); + switch (key) { + case "key1": + assertEquals("value1", value); + break; + case "key2": + assertEquals("true", value); + break; + case "key3": + assertEquals("3.0", value); + break; + case "key4": + assertEquals("4", value); + break; + case "key5": + assertEquals("{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", value); + break; + } + } + } + + private void assertTs(Map>> deviceValues, List expectedKeys, int ts, int arrayIndex) { + assertEquals(ts, deviceValues.get(expectedKeys.get(0)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(1)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(2)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(3)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(4)).get(arrayIndex).get("ts")); + } + + // @Test - Unstable + public void testMqttQoSLevel() throws Exception { + String clientId = MqttAsyncClient.generateClientId(); + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + client.connect(options).waitForCompletion(5000); + client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value()); + String payload = "{\"key\":\"uniqueValue\"}"; +// TODO 3.1: we need to acknowledge subscription only after it is processed by device actor and not when the message is pushed to queue. +// MqttClient -> SUB REQUEST -> Transport -> Kafka -> Device Actor (subscribed) +// MqttClient <- SUB_ACK <- Transport + Thread.sleep(5000); + doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk()); + latch.await(10, TimeUnit.SECONDS); + assertEquals(payload, callback.getPayload()); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + } + + private static class TestMqttCallback implements MqttCallback { + + private final MqttAsyncClient client; + private final CountDownLatch latch; + private volatile Integer qoS; + private volatile String payload; + + String getPayload() { + return payload; + } + + TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + this.client = client; + this.latch = latch; + } + + int getQoS() { + return qoS; + } + + @Override + public void connectionLost(Throwable throwable) { + log.error("Client connection lost", throwable); + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { + payload = new String(mqttMessage.getPayload()); + qoS = mqttMessage.getQos(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java new file mode 100644 index 0000000000..edab8405cd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Slf4j +public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { + + private static final String POST_DATA_TELEMETRY_TOPIC = "data/telemetry"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Telemetry device json payload", "Test Post Telemetry gateway json payload", TransportPayloadType.JSON, POST_DATA_TELEMETRY_TOPIC, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttTelemetry() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + } + + @Test + public void testPushMqttTelemetryWithTs() throws Exception { + String payloadStr = "{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}"; + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); + } + + @Test + public void testPushMqttTelemetryGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayTelemetryJsonPayload(deviceName1, deviceName2, "10000", "20000"); + processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } + + @Test + public void testGatewayConnect() throws Exception { + String payload = "{\"device\":\"Device A\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + publishMqttMsg(client, payload.getBytes(), MqttTopics.GATEWAY_CONNECT_TOPIC); + + String deviceName = "Device A"; + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100); + assertNotNull(device); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java new file mode 100644 index 0000000000..2257350d31 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Slf4j +public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { + + private static final String POST_DATA_TELEMETRY_TOPIC = "proto/telemetry"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttTelemetry() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + TransportProtos.TsKvListProto tsKvListProto = getTsKvListProto(expectedKeys, 0); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, tsKvListProto.toByteArray(), false); + } + + @Test + public void testPushMqttTelemetryWithTs() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + TransportProtos.TsKvListProto tsKvListProto = getTsKvListProto(expectedKeys, 10000); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, tsKvListProto.toByteArray(), true); + } + + @Test + public void testPushMqttTelemetryGateway() throws Exception { + TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.TelemetryMsg deviceATelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName1, expectedKeys, 10000, 20000); + TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000); + gatewayTelemetryMsgProtoBuilder.addAllMsg(Arrays.asList(deviceATelemetryMsgProto, deviceBTelemetryMsgProto)); + TransportApiProtos.GatewayTelemetryMsg gatewayTelemetryMsg = gatewayTelemetryMsgProtoBuilder.build(); + processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, gatewayTelemetryMsg.toByteArray(), deviceName1, deviceName2); + } + + @Test + public void testGatewayConnect() throws Exception { + String deviceName = "Device A"; + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + publishMqttMsg(client, connectMsgProto.toByteArray(), MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100); + + assertNotNull(device); + } + + private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + + private TransportApiProtos.TelemetryMsg getDeviceTelemetryMsgProto(String deviceName, List expectedKeys, long firstTs, long secondTs) { + TransportApiProtos.TelemetryMsg.Builder deviceTelemetryMsgBuilder = TransportApiProtos.TelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto tsKvListProto1 = getTsKvListProto(expectedKeys, firstTs); + TransportProtos.TsKvListProto tsKvListProto2 = getTsKvListProto(expectedKeys, secondTs); + TransportProtos.PostTelemetryMsg.Builder msg = TransportProtos.PostTelemetryMsg.newBuilder(); + msg.addAllTsKvList(Arrays.asList(tsKvListProto1, tsKvListProto2)); + deviceTelemetryMsgBuilder.setDeviceName(deviceName); + deviceTelemetryMsgBuilder.setMsg(msg); + return deviceTelemetryMsgBuilder.build(); + } + + private TransportProtos.TsKvListProto getTsKvListProto(List expectedKeys, long ts) { + List kvProtos = getKvProtos(expectedKeys); + TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); + builder.addAllKv(kvProtos); + builder.setTs(ts); + return builder.build(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/nosql/MqttTelemetryNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlIntegrationTest.java similarity index 74% rename from application/src/test/java/org/thingsboard/server/mqtt/telemetry/nosql/MqttTelemetryNoSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlIntegrationTest.java index 7f978f4fc7..77acef04d9 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/nosql/MqttTelemetryNoSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlIntegrationTest.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.mqtt.telemetry.nosql; +package org.thingsboard.server.mqtt.telemetry.timeseries.nosql; import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesIntegrationTest; /** * Created by Valerii Sosliuk on 8/22/2017. */ @DaoNoSqlTest -public class MqttTelemetryNoSqlIntegrationTest extends AbstractMqttTelemetryIntegrationTest { +public class MqttTimeseriesNoSqlIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..4ac82c2518 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlJsonIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesJsonIntegrationTest; + +@DaoNoSqlTest +public class MqttTimeseriesNoSqlJsonIntegrationTest extends AbstractMqttTimeseriesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..c208574e5c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlProtoIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesProtoIntegrationTest; + +@DaoNoSqlTest +public class MqttTimeseriesNoSqlProtoIntegrationTest extends AbstractMqttTimeseriesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/sql/MqttTelemetrySqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlIntegrationTest.java similarity index 72% rename from application/src/test/java/org/thingsboard/server/mqtt/telemetry/sql/MqttTelemetrySqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlIntegrationTest.java index b4b48365ac..cfb39a29db 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/sql/MqttTelemetrySqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlIntegrationTest.java @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.mqtt.telemetry.sql; +package org.thingsboard.server.mqtt.telemetry.timeseries.sql; -import org.thingsboard.server.dao.service.DaoNoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesIntegrationTest; /** * Created by Valerii Sosliuk on 8/22/2017. */ @DaoSqlTest -public class MqttTelemetrySqlIntegrationTest extends AbstractMqttTelemetryIntegrationTest { +public class MqttTimeseriesSqlIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..f313cc5d29 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlJsonIntegrationTest.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesJsonIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoSqlTest +public class MqttTimeseriesSqlJsonIntegrationTest extends AbstractMqttTimeseriesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..3c7d5e2e2a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlProtoIntegrationTest.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesJsonIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesProtoIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoSqlTest +public class MqttTimeseriesSqlProtoIntegrationTest extends AbstractMqttTimeseriesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java index 72484e3f21..384573b190 100644 --- a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java @@ -32,7 +32,7 @@ public class RuleEngineSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java index ee97cd3c2f..11377c4ba0 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.rules.flow.sql; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest; /** diff --git a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java index 43cd62ea97..595f64b3a4 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java @@ -51,7 +51,7 @@ public class TbMsgPackProcessingContextTest { messages.put(UUID.randomUUID(), new TbProtoQueueMsg<>(UUID.randomUUID(), null)); } when(strategyMock.getPendingMap()).thenReturn(messages); - TbMsgPackProcessingContext context = new TbMsgPackProcessingContext(strategyMock); + TbMsgPackProcessingContext context = new TbMsgPackProcessingContext("Main", strategyMock); for (UUID uuid : messages.keySet()) { for (int i = 0; i < parallelCount; i++) { executorService.submit(() -> context.onSuccess(uuid)); diff --git a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java index 36d24e9b00..6aa451747f 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java @@ -33,7 +33,7 @@ public class SystemSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/common/actor/pom.xml b/common/actor/pom.xml index b69d0c7e62..9258ee75a4 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 2c42f494d1..650b05c04e 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java index 21b20f7427..6db7f84dc3 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java @@ -80,8 +80,22 @@ public class CassandraDriverOptions { @Value("${cassandra.compression}") private String compression; - @Value("${cassandra.ssl}") + + @Value("${cassandra.ssl.enabled}") private Boolean ssl; + @Value("${cassandra.ssl.key_store}") + private String sslKeyStore; + @Value("${cassandra.ssl.key_store_password}") + private String sslKeyStorePassword; + @Value("${cassandra.ssl.trust_store}") + private String sslTrustStore; + @Value("${cassandra.ssl.trust_store_password}") + private String sslTrustStorePassword; + @Value("${cassandra.ssl.hostname_validation}") + private Boolean sslHostnameValidation; + @Value("${cassandra.ssl.cipher_suites}") + private List sslCipherSuites; + @Value("${cassandra.metrics}") private Boolean metrics; @@ -120,7 +134,19 @@ public class CassandraDriverOptions { if (this.ssl) { driverConfigBuilder.withString(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS, - "DefaultSslEngineFactory"); + "DefaultSslEngineFactory") + .withBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, this.sslHostnameValidation); + if(!this.sslTrustStore.isEmpty()) { + driverConfigBuilder.withString(DefaultDriverOption.SSL_TRUSTSTORE_PATH, this.sslTrustStore) + .withString(DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD, this.sslTrustStorePassword); + } + if(!this.sslKeyStore.isEmpty()) { + driverConfigBuilder.withString(DefaultDriverOption.SSL_KEYSTORE_PATH, this.sslKeyStore) + .withString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, this.sslKeyStorePassword); + } + if(!this.sslCipherSuites.isEmpty()) { + driverConfigBuilder.withStringList(DefaultDriverOption.SSL_CIPHER_SUITES, this.sslCipherSuites); + } } if (this.metrics) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java new file mode 100644 index 0000000000..e38bac68e5 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface DeviceProfileService { + + DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId); + + DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName); + + DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId); + + DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile); + + void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId); + + PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink); + + PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink); + + DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName); + + DeviceProfile createDefaultDeviceProfile(TenantId tenantId); + + DeviceProfile findDefaultDeviceProfile(TenantId tenantId); + + DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId); + + boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId); + + void deleteDeviceProfilesByTenantId(TenantId tenantId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 2480c67350..ba2f09ae73 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -56,6 +57,8 @@ public interface DeviceService { PageData findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + PageData findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink); + ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds); void deleteDevicesByTenantId(TenantId tenantId); @@ -68,6 +71,8 @@ public interface DeviceService { PageData findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink); + ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 9d9ba00322..05fda67939 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.rule; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -23,6 +24,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -63,4 +66,8 @@ public interface RuleChainService { void deleteRuleChainsByTenantId(TenantId tenantId); + RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException; + + List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java new file mode 100644 index 0000000000..07138a1a11 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; + +public interface RuleNodeStateService { + + PageData findByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId, PageLink pageLink); + + RuleNodeState findByRuleNodeIdAndEntityId(TenantId tenantId, RuleNodeId ruleNodeId, EntityId entityId); + + RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java new file mode 100644 index 0000000000..5ac3acfcb9 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface TenantProfileService { + + TenantProfile findTenantProfileById(TenantId tenantId, TenantProfileId tenantProfileId); + + EntityInfo findTenantProfileInfoById(TenantId tenantId, TenantProfileId tenantProfileId); + + TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile); + + void deleteTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); + + PageData findTenantProfiles(TenantId tenantId, PageLink pageLink); + + PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink); + + TenantProfile findOrCreateDefaultTenantProfile(TenantId tenantId); + + TenantProfile findDefaultTenantProfile(TenantId tenantId); + + EntityInfo findDefaultTenantProfileInfo(TenantId tenantId); + + boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); + + void deleteTenantProfiles(TenantId tenantId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index 5bf811da80..eaf835812b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.tenant; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -25,6 +26,8 @@ public interface TenantService { Tenant findTenantById(TenantId tenantId); + TenantInfo findTenantInfoById(TenantId tenantId); + ListenableFuture findTenantByIdAsync(TenantId callerId, TenantId tenantId); Tenant saveTenant(Tenant tenant); @@ -32,6 +35,8 @@ public interface TenantService { void deleteTenant(TenantId tenantId); PageData findTenants(PageLink pageLink); + + PageData findTenantInfos(PageLink pageLink); void deleteTenants(); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 18f4a26200..eb7a3b226c 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 1890e7ba37..fe9f3df3ae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -24,4 +24,6 @@ public class CacheConstants { public static final String ENTITY_VIEW_CACHE = "entityViews"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; + public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; + public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index e99db672a3..8dc492093c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -28,6 +28,10 @@ public class DataConstants { public static final String SERVER_SCOPE = "SERVER_SCOPE"; public static final String SHARED_SCOPE = "SHARED_SCOPE"; public static final String LATEST_TS = "LATEST_TS"; + public static final String IS_NEW_ALARM = "isNewAlarm"; + public static final String IS_EXISTING_ALARM = "isExistingAlarm"; + public static final String IS_SEVERITY_UPDATED_ALARM = "isSeverityUpdated"; + public static final String IS_CLEARED_ALARM = "isClearedAlarm"; public static final String[] allScopes() { return new String[]{CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE}; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index cd617ec345..ca8e5f5575 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -15,12 +15,22 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.ByteArrayInputStream; +import java.io.IOException; + @EqualsAndHashCode(callSuper = true) +@Slf4j public class Device extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { private static final long serialVersionUID = 2807343040519543363L; @@ -30,6 +40,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen private String name; private String type; private String label; + private DeviceProfileId deviceProfileId; + private transient DeviceData deviceData; + @JsonIgnore + private byte[] deviceDataBytes; public Device() { super(); @@ -46,6 +60,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.name = device.getName(); this.type = device.getType(); this.label = device.getLabel(); + this.deviceProfileId = device.getDeviceProfileId(); + this.setDeviceData(device.getDeviceData()); } public TenantId getTenantId() { @@ -89,6 +105,41 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.label = label; } + public DeviceProfileId getDeviceProfileId() { + return deviceProfileId; + } + + public void setDeviceProfileId(DeviceProfileId deviceProfileId) { + this.deviceProfileId = deviceProfileId; + } + + public DeviceData getDeviceData() { + if (deviceData != null) { + return deviceData; + } else { + if (deviceDataBytes != null) { + try { + deviceData = mapper.readValue(new ByteArrayInputStream(deviceDataBytes), DeviceData.class); + } catch (IOException e) { + log.warn("Can't deserialize device data: ", e); + return null; + } + return deviceData; + } else { + return null; + } + } + } + + public void setDeviceData(DeviceData data) { + this.deviceData = data; + try { + this.deviceDataBytes = data != null ? mapper.writeValueAsBytes(data) : null; + } catch (JsonProcessingException e) { + log.warn("Can't serialize device data: ", e); + } + } + @Override public String getSearchText() { return getName(); @@ -107,6 +158,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen builder.append(type); builder.append(", label="); builder.append(label); + builder.append(", deviceProfileId="); + builder.append(deviceProfileId); + builder.append(", deviceData="); + builder.append(deviceData); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); builder.append(", createdTime="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java index 406fa26086..56fb4bc11c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java @@ -23,6 +23,7 @@ public class DeviceInfo extends Device { private String customerTitle; private boolean customerIsPublic; + private String deviceProfileName; public DeviceInfo() { super(); @@ -32,9 +33,10 @@ public class DeviceInfo extends Device { super(deviceId); } - public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic) { + public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic, String deviceProfileName) { super(device); this.customerTitle = customerTitle; this.customerIsPublic = customerIsPublic; + this.deviceProfileName = deviceProfileName; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java new file mode 100644 index 0000000000..097d64b198 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; + +@Data +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId { + + private TenantId tenantId; + private String name; + private String description; + private boolean isDefault; + private DeviceProfileType type; + private DeviceTransportType transportType; + private RuleChainId defaultRuleChainId; + private transient DeviceProfileData profileData; + @JsonIgnore + private byte[] profileDataBytes; + + public DeviceProfile() { + super(); + } + + public DeviceProfile(DeviceProfileId deviceProfileId) { + super(deviceProfileId); + } + + public DeviceProfile(DeviceProfile deviceProfile) { + super(deviceProfile); + this.tenantId = deviceProfile.getTenantId(); + this.name = deviceProfile.getName(); + this.description = deviceProfile.getDescription(); + this.isDefault = deviceProfile.isDefault(); + this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); + this.setProfileData(deviceProfile.getProfileData()); + } + + @Override + public String getSearchText() { + return getName(); + } + + @Override + public String getName() { + return name; + } + + public DeviceProfileData getProfileData() { + if (profileData != null) { + return profileData; + } else { + if (profileDataBytes != null) { + try { + profileData = mapper.readValue(new ByteArrayInputStream(profileDataBytes), DeviceProfileData.class); + } catch (IOException e) { + log.warn("Can't deserialize device profile data: ", e); + return null; + } + return profileData; + } else { + return null; + } + } + } + + public void setProfileData(DeviceProfileData data) { + this.profileData = data; + try { + this.profileDataBytes = data != null ? mapper.writeValueAsBytes(data) : null; + } catch (JsonProcessingException e) { + log.warn("Can't serialize device profile data: ", e); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java new file mode 100644 index 0000000000..310c9ece60 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; + +import java.util.UUID; + +@Value +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeviceProfileInfo extends EntityInfo { + + private final DeviceProfileType type; + private final DeviceTransportType transportType; + + @JsonCreator + public DeviceProfileInfo(@JsonProperty("id") EntityId id, + @JsonProperty("name") String name, + @JsonProperty("type") DeviceProfileType type, + @JsonProperty("transportType") DeviceTransportType transportType) { + super(id, name); + this.type = type; + this.transportType = transportType; + } + + public DeviceProfileInfo(UUID uuid, String name, DeviceProfileType type, DeviceTransportType transportType) { + super(EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE_PROFILE, uuid), name); + this.type = type; + this.transportType = transportType; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileType.java new file mode 100644 index 0000000000..93ca102082 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum DeviceProfileType { + DEFAULT +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java new file mode 100644 index 0000000000..f4a0f99f69 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum DeviceTransportType { + DEFAULT, + MQTT, + LWM2M +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java new file mode 100644 index 0000000000..3bb3b134d4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.HasId; + +import java.util.UUID; + +@Data +public class EntityInfo implements HasId, HasName { + + private final EntityId id; + private final String name; + + @JsonCreator + public EntityInfo(@JsonProperty("id") EntityId id, @JsonProperty("name") String name) { + this.id = id; + this.name = name; + } + + public EntityInfo(UUID uuid, String entityType, String name) { + this.id = EntityIdFactory.getByTypeAndUuid(entityType, uuid); + this.name = name; + } + +} 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 7e43c59797..cfe12a14cb 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 @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java index 8dc9bf6abc..4efd673d7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java @@ -35,7 +35,7 @@ import java.util.function.Consumer; @Slf4j public abstract class SearchTextBasedWithAdditionalInfo extends SearchTextBased implements HasAdditionalInfo { - private static final ObjectMapper mapper = new ObjectMapper(); + public static final ObjectMapper mapper = new ObjectMapper(); private transient JsonNode additionalInfo; @JsonIgnore private byte[] additionalInfoBytes; @@ -84,7 +84,7 @@ public abstract class SearchTextBasedWithAdditionalInfo ext byte[] data = binaryData.get(); if (data != null) { try { - return new ObjectMapper().readTree(new ByteArrayInputStream(data)); + return mapper.readTree(new ByteArrayInputStream(data)); } catch (IOException e) { log.warn("Can't deserialize json data: ", e); return null; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 766d405e25..e7d71ab457 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -19,8 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.TenantId; - -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.TenantProfileId; @EqualsAndHashCode(callSuper = true) public class Tenant extends ContactBased implements HasTenantId { @@ -29,8 +28,7 @@ public class Tenant extends ContactBased implements HasTenantId { private String title; private String region; - private boolean isolatedTbCore; - private boolean isolatedTbRuleEngine; + private TenantProfileId tenantProfileId; public Tenant() { super(); @@ -44,6 +42,7 @@ public class Tenant extends ContactBased implements HasTenantId { super(tenant); this.title = tenant.getTitle(); this.region = tenant.getRegion(); + this.tenantProfileId = tenant.getTenantProfileId(); } public String getTitle() { @@ -74,20 +73,12 @@ public class Tenant extends ContactBased implements HasTenantId { this.region = region; } - public boolean isIsolatedTbCore() { - return isolatedTbCore; - } - - public void setIsolatedTbCore(boolean isolatedTbCore) { - this.isolatedTbCore = isolatedTbCore; - } - - public boolean isIsolatedTbRuleEngine() { - return isolatedTbRuleEngine; + public TenantProfileId getTenantProfileId() { + return tenantProfileId; } - public void setIsolatedTbRuleEngine(boolean isolatedTbRuleEngine) { - this.isolatedTbRuleEngine = isolatedTbRuleEngine; + public void setTenantProfileId(TenantProfileId tenantProfileId) { + this.tenantProfileId = tenantProfileId; } @Override @@ -102,10 +93,8 @@ public class Tenant extends ContactBased implements HasTenantId { builder.append(title); builder.append(", region="); builder.append(region); - builder.append(", isolatedTbCore="); - builder.append(isolatedTbCore); - builder.append(", isolatedTbRuleEngine="); - builder.append(isolatedTbRuleEngine); + builder.append(", tenantProfileId="); + builder.append(tenantProfileId); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); builder.append(", country="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java new file mode 100644 index 0000000000..ee94b26f18 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class TenantInfo extends Tenant { + + private String tenantProfileName; + + public TenantInfo() { + super(); + } + + public TenantInfo(TenantId tenantId) { + super(tenantId); + } + + public TenantInfo(Tenant tenant, String tenantProfileName) { + super(tenant); + this.tenantProfileName = tenantProfileName; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java new file mode 100644 index 0000000000..32f620f869 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.TenantProfileId; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; + +@Data +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class TenantProfile extends SearchTextBased implements HasName { + + private String name; + private String description; + private boolean isDefault; + private boolean isolatedTbCore; + private boolean isolatedTbRuleEngine; + private transient TenantProfileData profileData; + @JsonIgnore + private byte[] profileDataBytes; + + public TenantProfile() { + super(); + } + + public TenantProfile(TenantProfileId tenantProfileId) { + super(tenantProfileId); + } + + public TenantProfile(TenantProfile tenantProfile) { + super(tenantProfile); + this.name = tenantProfile.getName(); + this.description = tenantProfile.getDescription(); + this.isDefault = tenantProfile.isDefault(); + this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); + this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); + this.setProfileData(tenantProfile.getProfileData()); + } + + @Override + public String getSearchText() { + return getName(); + } + + @Override + public String getName() { + return name; + } + + public TenantProfileData getProfileData() { + if (profileData != null) { + return profileData; + } else { + if (profileDataBytes != null) { + try { + profileData = mapper.readValue(new ByteArrayInputStream(profileDataBytes), TenantProfileData.class); + } catch (IOException e) { + log.warn("Can't deserialize tenant profile data: ", e); + return null; + } + return profileData; + } else { + return null; + } + } + } + + public void setProfileData(TenantProfileData data) { + this.profileData = data; + try { + this.profileDataBytes = data != null ? mapper.writeValueAsBytes(data) : null; + } catch (JsonProcessingException e) { + log.warn("Can't serialize tenant profile data: ", e); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileData.java new file mode 100644 index 0000000000..54319276f1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileData.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class TenantProfileData { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TransportPayloadType.java b/common/data/src/main/java/org/thingsboard/server/common/data/TransportPayloadType.java new file mode 100644 index 0000000000..99939914f3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TransportPayloadType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum TransportPayloadType { + JSON, + PROTOBUF +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java new file mode 100644 index 0000000000..eeef8617b5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.credentials; + +import lombok.Data; + +@Data +public class BasicMqttCredentials { + + private String clientId; + private String userName; + private String password; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java new file mode 100644 index 0000000000..61a2481922 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; + +@Data +public class DefaultDeviceConfiguration implements DeviceConfiguration { + + @Override + public DeviceProfileType getType() { + return DeviceProfileType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java new file mode 100644 index 0000000000..1825193e01 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@Data +public class DefaultDeviceTransportConfiguration implements DeviceTransportConfiguration { + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java new file mode 100644 index 0000000000..1ea2ee4f97 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceConfiguration.class, name = "DEFAULT")}) +public interface DeviceConfiguration { + + @JsonIgnore + DeviceProfileType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java new file mode 100644 index 0000000000..6c24ba5e28 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import lombok.Data; + +@Data +public class DeviceData { + + private DeviceConfiguration configuration; + private DeviceTransportConfiguration transportConfiguration; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java new file mode 100644 index 0000000000..e9bd1a3245 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceTransportType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceTransportConfiguration.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = MqttDeviceTransportConfiguration.class, name = "MQTT"), + @JsonSubTypes.Type(value = Lwm2mDeviceTransportConfiguration.class, name = "LWM2M")}) +public interface DeviceTransportConfiguration { + + @JsonIgnore + DeviceTransportType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java new file mode 100644 index 0000000000..e37ef14933 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class Lwm2mDeviceTransportConfiguration implements DeviceTransportConfiguration { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.LWM2M; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java new file mode 100644 index 0000000000..3d27193ae8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceTransportType; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class MqttDeviceTransportConfiguration implements DeviceTransportConfiguration { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.MQTT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java new file mode 100644 index 0000000000..32db0f730f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.thingsboard.server.common.data.query.KeyFilter; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class AlarmCondition { + + private List condition; + private AlarmConditionSpec spec; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java new file mode 100644 index 0000000000..915f62681b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), + @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), + @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) +public interface AlarmConditionSpec { + + @JsonIgnore + AlarmConditionSpecType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java new file mode 100644 index 0000000000..11ea8e6347 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +public enum AlarmConditionSpecType { + + SIMPLE, + DURATION, + REPEATING + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java new file mode 100644 index 0000000000..cf830ab8de --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +@Data +public class AlarmRule { + + private AlarmCondition condition; + private AlarmSchedule schedule; + // Advanced + private String alarmDetails; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java new file mode 100644 index 0000000000..2c7d460df0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), + @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), + @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) +public interface AlarmSchedule { + + AlarmScheduleType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java new file mode 100644 index 0000000000..e72502a954 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +public enum AlarmScheduleType { + + ANY_TIME, + SPECIFIC_TIME, + CUSTOM + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java new file mode 100644 index 0000000000..fb7e10bc22 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +public class AnyTimeSchedule implements AlarmSchedule { + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.ANY_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java new file mode 100644 index 0000000000..7d41a72f46 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; + +@Data +public class CustomTimeSchedule implements AlarmSchedule { + + private String timezone; + private List items; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.CUSTOM; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java new file mode 100644 index 0000000000..ba5735988d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; + +@Data +public class CustomTimeScheduleItem { + + private boolean enabled; + private int dayOfWeek; + private long startsOn; + private long endsOn; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java new file mode 100644 index 0000000000..d8e6cef10b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; + +@Data +public class DefaultDeviceProfileConfiguration implements DeviceProfileConfiguration { + + @Override + public DeviceProfileType getType() { + return DeviceProfileType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..5610e2555f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@Data +public class DefaultDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java new file mode 100644 index 0000000000..b6437ae7bb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; + +import java.util.List; +import java.util.Map; + +@Data +public class DeviceProfileAlarm { + + private String id; + private String alarmType; + + private Map createRules; + private AlarmRule clearRule; + + // Hidden in advanced settings + private boolean propagate; + private List propagateRelationTypes; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java new file mode 100644 index 0000000000..3bb3d29c34 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceProfileConfiguration.class, name = "DEFAULT")}) +public interface DeviceProfileConfiguration { + + @JsonIgnore + DeviceProfileType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java new file mode 100644 index 0000000000..275e6269d6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; + +@Data +public class DeviceProfileData { + + private DeviceProfileConfiguration configuration; + private DeviceProfileTransportConfiguration transportConfiguration; + private List alarms; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..34854958d1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceProfileTransportConfiguration.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"), + @JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M")}) +public interface DeviceProfileTransportConfiguration { + + @JsonIgnore + DeviceTransportType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java new file mode 100644 index 0000000000..cb27e45538 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class DurationAlarmConditionSpec implements AlarmConditionSpec { + + private TimeUnit unit; + private long value; + + @Override + public AlarmConditionSpecType getType() { + return AlarmConditionSpecType.DURATION; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..b2bdd63009 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.LWM2M; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..d88ac24cbb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@Data +public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + + private TransportPayloadType transportPayloadType = TransportPayloadType.JSON; + + private String deviceTelemetryTopic = MqttTopics.DEVICE_TELEMETRY_TOPIC; + private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.MQTT; + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTopics.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java similarity index 55% rename from common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTopics.java rename to common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java index 946b906871..ba8ac7f3be 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTopics.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java @@ -13,35 +13,55 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.mqtt; +package org.thingsboard.server.common.data.device.profile; /** * Created by ashvayka on 19.01.17. */ public class MqttTopics { + private static final String RPC = "/rpc"; + private static final String CONNECT = "/connect"; + private static final String DISCONNECT = "/disconnect"; + private static final String TELEMETRY = "/telemetry"; + private static final String ATTRIBUTES = "/attributes"; + private static final String CLAIM = "/claim"; + private static final String SUB_TOPIC = "+"; + private static final String ATTRIBUTES_RESPONSE = "/attributes/response"; + private static final String ATTRIBUTES_REQUEST = "/attributes/request"; + + private static final String DEVICE_RPC_RESPONSE = "/rpc/response/"; + private static final String DEVICE_RPC_REQUEST = "/rpc/request/"; + + private static final String DEVICE_ATTRIBUTES_RESPONSE = ATTRIBUTES_RESPONSE + "/"; + private static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/"; + + // V1_JSON topics + public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; - public static final String DEVICE_RPC_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + "/rpc/response/"; - public static final String DEVICE_RPC_RESPONSE_SUB_TOPIC = DEVICE_RPC_RESPONSE_TOPIC + "+"; - public static final String DEVICE_RPC_REQUESTS_TOPIC = BASE_DEVICE_API_TOPIC + "/rpc/request/"; - public static final String DEVICE_RPC_REQUESTS_SUB_TOPIC = DEVICE_RPC_REQUESTS_TOPIC + "+"; - public static final String DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + "/attributes/response/"; - public static final String DEVICE_ATTRIBUTES_RESPONSES_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + "+"; - public static final String DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + "/attributes/request/"; - public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + "/telemetry"; - public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + "/claim"; - public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + "/attributes"; - public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway"; - public static final String GATEWAY_CONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + "/connect"; - public static final String GATEWAY_DISCONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + "/disconnect"; - public static final String GATEWAY_ATTRIBUTES_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes"; - public static final String GATEWAY_TELEMETRY_TOPIC = BASE_GATEWAY_API_TOPIC + "/telemetry"; - public static final String GATEWAY_CLAIM_TOPIC = BASE_GATEWAY_API_TOPIC + "/claim"; - public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + "/rpc"; - public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/request"; - public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/response"; + public static final String DEVICE_RPC_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + DEVICE_RPC_RESPONSE; + public static final String DEVICE_RPC_RESPONSE_SUB_TOPIC = DEVICE_RPC_RESPONSE_TOPIC + SUB_TOPIC; + public static final String DEVICE_RPC_REQUESTS_TOPIC = BASE_DEVICE_API_TOPIC + DEVICE_RPC_REQUEST; + public static final String DEVICE_RPC_REQUESTS_SUB_TOPIC = DEVICE_RPC_REQUESTS_TOPIC + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + DEVICE_ATTRIBUTES_RESPONSE; + public static final String DEVICE_ATTRIBUTES_RESPONSES_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + DEVICE_ATTRIBUTES_REQUEST; + public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRY; + public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + CLAIM; + public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + ATTRIBUTES; + + // V1_JSON gateway topics + public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway"; + public static final String GATEWAY_CONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + CONNECT; + public static final String GATEWAY_DISCONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + DISCONNECT; + public static final String GATEWAY_ATTRIBUTES_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES; + public static final String GATEWAY_TELEMETRY_TOPIC = BASE_GATEWAY_API_TOPIC + TELEMETRY; + public static final String GATEWAY_CLAIM_TOPIC = BASE_GATEWAY_API_TOPIC + CLAIM; + public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + RPC; + public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_REQUEST; + public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_RESPONSE; private MqttTopics() { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java new file mode 100644 index 0000000000..3883676ee5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { + + private int count; + + @Override + public AlarmConditionSpecType getType() { + return AlarmConditionSpecType.REPEATING; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java new file mode 100644 index 0000000000..547044581a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SimpleAlarmConditionSpec implements AlarmConditionSpec { + @Override + public AlarmConditionSpecType getType() { + return AlarmConditionSpecType.SIMPLE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java new file mode 100644 index 0000000000..c099b57680 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; +import java.util.Set; + +@Data +public class SpecificTimeSchedule implements AlarmSchedule { + + private String timezone; + private Set daysOfWeek; + private long startsOn; + private long endsOn; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.SPECIFIC_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java new file mode 100644 index 0000000000..0350e37205 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +public class DeviceProfileId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public DeviceProfileId(@JsonProperty("id") UUID id) { + super(id); + } + + public static DeviceProfileId fromString(String deviceProfileId) { + return new DeviceProfileId(UUID.fromString(deviceProfileId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.DEVICE_PROFILE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index a799a40b18..8c754db18c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -29,7 +29,7 @@ import java.util.UUID; @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) -public interface EntityId extends Serializable { //NOSONAR, the constant is closely related to EntityId +public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 11db1da1a0..dc1589814f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -62,6 +62,10 @@ public class EntityIdFactory { return new WidgetsBundleId(uuid); case WIDGET_TYPE: return new WidgetTypeId(uuid); + case DEVICE_PROFILE: + return new DeviceProfileId(uuid); + case TENANT_PROFILE: + return new TenantProfileId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/HasId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasId.java new file mode 100644 index 0000000000..86d2269079 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasId.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.io.Serializable; + +public interface HasId extends Serializable { + + I getId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/HasUUID.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasUUID.java new file mode 100644 index 0000000000..e43294603d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasUUID.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +public interface HasUUID { + + UUID getId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java index 79b8a3cce1..8957253bbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import java.io.Serializable; import java.util.UUID; -public abstract class IdBased implements Serializable { +public abstract class IdBased implements HasId { protected I id; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeStateId.java new file mode 100644 index 0000000000..7bdf411353 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeStateId.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class RuleNodeStateId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public RuleNodeStateId(@JsonProperty("id") UUID id) { + super(id); + } + + public static RuleNodeStateId fromString(String eventId) { + return new RuleNodeStateId(UUID.fromString(eventId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java new file mode 100644 index 0000000000..8f3d27891c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +public class TenantProfileId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public TenantProfileId(@JsonProperty("id") UUID id) { + super(id); + } + + public static TenantProfileId fromString(String tenantProfileId) { + return new TenantProfileId(UUID.fromString(tenantProfileId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.TENANT_PROFILE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index 7e07d216f4..d70b9d0d30 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.id; import java.io.Serializable; import java.util.UUID; -public abstract class UUIDBased implements Serializable { +public abstract class UUIDBased implements HasUUID, Serializable { private static final long serialVersionUID = 1L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java new file mode 100644 index 0000000000..0239d42451 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public enum EntityKeyValueType { + STRING, + NUMERIC, + BOOLEAN, + DATE_TIME +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java index 120f5e1fed..6ab5ce7736 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java @@ -21,6 +21,7 @@ import lombok.Data; public class KeyFilter { private EntityKey key; + private EntityKeyValueType valueType; private KeyFilterPredicate predicate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java new file mode 100644 index 0000000000..0a921c6526 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +@Data +@Slf4j +public class DefaultRuleChainCreateRequest implements Serializable { + + private static final long serialVersionUID = 5600333716030561537L; + + private String name; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java new file mode 100644 index 0000000000..e4b9ec3442 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.Data; + +import java.util.List; + +@Data +public class RuleChainData { + + List ruleChains; + List metadata; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java new file mode 100644 index 0000000000..53b899e04b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; + +@Data +@AllArgsConstructor +public class RuleChainImportResult { + + private TenantId tenantId; + private RuleChainId ruleChainId; + private ComponentLifecycleEvent lifecycleEvent; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeState.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeState.java new file mode 100644 index 0000000000..a3432760be --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeState.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.RuleNodeStateId; + +@Data +public class RuleNodeState extends BaseData { + + private RuleNodeId ruleNodeId; + private EntityId entityId; + private String stateData; + + public RuleNodeState() { + super(); + } + + public RuleNodeState(RuleNodeStateId id) { + super(id); + } + + public RuleNodeState(RuleNodeState event) { + super(event); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java index 8d647dcb1a..e7faf1b5ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.security; public enum DeviceCredentialsType { ACCESS_TOKEN, - X509_CERTIFICATE + X509_CERTIFICATE, + MQTT_BASIC } diff --git a/common/message/pom.xml b/common/message/pom.xml index 121fb19af1..17a0679630 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index a309c5b830..f60d18fbd4 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.msg; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.crypto.digests.SHA3Digest; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; + /** * @author Valerii Sosliuk */ @@ -30,8 +31,8 @@ public class EncryptionUtil { public static String trimNewLines(String input) { return input.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") - .replaceAll("\n","") - .replaceAll("\r",""); + .replaceAll("\n", "") + .replaceAll("\r", ""); } public static String getSha3Hash(String data) { @@ -45,4 +46,20 @@ public class EncryptionUtil { String sha3Hash = ByteUtils.toHexString(hashedBytes); return sha3Hash; } + + public static String getSha3Hash(String delim, String... tokens) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String token : tokens) { + if (token != null && !token.isEmpty()) { + if (first) { + first = false; + } else { + sb.append(delim); + } + sb.append(token); + } + } + return getSha3Hash(sb.toString()); + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 23710cc5ea..f0af529be9 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.msg.gen.MsgProtos; +import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -84,6 +85,11 @@ public final class TbMsg implements Serializable { data, origMsg.getRuleChainId(), origMsg.getRuleNodeId(), origMsg.getCallback()); } + public static TbMsg transformMsg(TbMsg origMsg, RuleChainId ruleChainId) { + return new TbMsg(origMsg.queueName, origMsg.id, origMsg.ts, origMsg.type, origMsg.originator, origMsg.metaData, origMsg.dataType, + origMsg.data, ruleChainId, null, origMsg.getCallback()); + } + public static TbMsg newMsg(TbMsg tbMsg, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(tbMsg.getQueueName(), UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.getMetaData().copy(), tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, TbMsgCallback.EMPTY); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index 10b198ec28..f815dd6192 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -31,6 +32,8 @@ import java.util.concurrent.ConcurrentHashMap; @NoArgsConstructor public final class TbMsgMetaData implements Serializable { + public static final TbMsgMetaData EMPTY = new TbMsgMetaData(Collections.emptyMap()); + private final Map data = new ConcurrentHashMap<>(); public TbMsgMetaData(Map data) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeInfo.java index 0c2d38b3b2..9af3f20363 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeInfo.java @@ -15,12 +15,16 @@ */ package org.thingsboard.server.common.msg.queue; +import lombok.Getter; import org.thingsboard.server.common.data.id.RuleNodeId; public class RuleNodeInfo { private final String label; + @Getter + private final RuleNodeId ruleNodeId; public RuleNodeInfo(RuleNodeId id, String ruleChainName, String ruleNodeName) { + this.ruleNodeId = id; this.label = "[RuleChain: " + ruleChainName + "|RuleNode: " + ruleNodeName + "(" + id + ")]"; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java index 4103a4efc1..3f6927adb1 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.msg.queue; +import org.thingsboard.server.common.data.id.RuleNodeId; + public interface TbMsgCallback { TbMsgCallback EMPTY = new TbMsgCallback() { @@ -34,7 +36,11 @@ public interface TbMsgCallback { void onFailure(RuleEngineException e); - default void visit(RuleNodeInfo ruleNodeInfo) { + default void onProcessingStart(RuleNodeInfo ruleNodeInfo) { + } + + default void onProcessingEnd(RuleNodeId ruleNodeId) { } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java index b27b957500..d124fd48b0 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.msg.session; +import org.thingsboard.server.common.data.DeviceProfile; + import java.util.UUID; public interface SessionContext { @@ -22,4 +24,6 @@ public interface SessionContext { UUID getSessionId(); int nextMsgId(); + + void onProfileUpdate(DeviceProfile deviceProfile); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java new file mode 100644 index 0000000000..fea6fcb337 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg.tools; + +import java.time.ZoneId; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class SchedulerUtils { + + private static final ConcurrentMap tzMap = new ConcurrentHashMap<>(); + + public static ZoneId getZoneId(String tz) { + return tzMap.computeIfAbsent(tz == null || tz.isEmpty() ? "UTC" : tz, ZoneId::of); + } + +} diff --git a/common/pom.xml b/common/pom.xml index 196ec8d2b8..52685d3891 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e1b47bcded..aaf46710f7 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index b92b094af1..bee293bd6d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -37,6 +37,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { private final AdminClient client; private final Map topicConfigs; private final Set topics = ConcurrentHashMap.newKeySet(); + private final int numPartitions; private final short replicationFactor; @@ -50,6 +51,13 @@ public class TbKafkaAdmin implements TbQueueAdmin { log.error("Failed to get all topics.", e); } + String numPartitionsStr = topicConfigs.get("partitions"); + if (numPartitionsStr != null) { + numPartitions = Integer.parseInt(numPartitionsStr); + topicConfigs.remove("partitions"); + } else { + numPartitions = 1; + } replicationFactor = settings.getReplicationFactor(); } @@ -59,7 +67,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { return; } try { - NewTopic newTopic = new NewTopic(topic, 1, replicationFactor).configs(topicConfigs); + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(topicConfigs); createTopic(newTopic).values().get(topic).get(); topics.add(topic); } catch (ExecutionException ee) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java index 994ca26305..31f7958bbf 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java @@ -23,27 +23,21 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; @Slf4j public final class InMemoryStorage { private static InMemoryStorage instance; private final ConcurrentHashMap> storage; - private static ScheduledExecutorService statExecutor; private InMemoryStorage() { storage = new ConcurrentHashMap<>(); - statExecutor = Executors.newSingleThreadScheduledExecutor(); - statExecutor.scheduleAtFixedRate(this::printStats, 60, 60, TimeUnit.SECONDS); } - private void printStats() { + public void printStats() { storage.forEach((topic, queue) -> { if (queue.size() > 0) { - log.debug("Topic: [{}], Queue size: [{}]", topic, queue.size()); + log.debug("[{}] Queue Size [{}]", topic, queue.size()); } }); } @@ -90,9 +84,4 @@ public final class InMemoryStorage { storage.clear(); } - public void destroy() { - if (statExecutor != null) { - statExecutor.shutdownNow(); - } - } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueProducer.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueProducer.java index 84a9a1fdf0..cfcd788a16 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueProducer.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueProducer.java @@ -53,6 +53,6 @@ public class InMemoryTbQueueProducer implements TbQueuePro @Override public void stop() { - storage.destroy(); + } } 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 fbdbeaab0c..8ebda6e7b9 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.provider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; @@ -28,6 +29,7 @@ import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; @@ -47,6 +49,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final InMemoryStorage storage; public InMemoryMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -59,6 +62,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.ruleEngineSettings = ruleEngineSettings; this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; + this.storage = InMemoryStorage.getInstance(); } @Override @@ -120,4 +124,9 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE public TbQueueRequestTemplate, TbProtoQueueMsg> createRemoteJsRequestTemplate() { return null; } + + @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/settings/TbRuleEngineQueueAckStrategyConfiguration.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbRuleEngineQueueAckStrategyConfiguration.java index 0d21c59c9c..978794662a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbRuleEngineQueueAckStrategyConfiguration.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbRuleEngineQueueAckStrategyConfiguration.java @@ -24,5 +24,6 @@ public class TbRuleEngineQueueAckStrategyConfiguration { private int retries; private double failurePercentage; private long pauseBetweenRetries; + private long maxPauseBetweenRetries; } diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 8001bc57d1..331c17b9d7 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -51,6 +51,8 @@ message SessionInfoProto { string deviceType = 9; int64 gwSessionIdMSB = 10; int64 gwSessionIdLSB = 11; + int64 deviceProfileIdMSB = 12; + int64 deviceProfileIdLSB = 13; } enum SessionEvent { @@ -99,6 +101,8 @@ message DeviceInfoProto { string deviceName = 5; string deviceType = 6; string additionalInfo = 7; + int64 deviceProfileIdMSB = 8; + int64 deviceProfileIdLSB = 9; } /** @@ -143,9 +147,16 @@ message ValidateDeviceX509CertRequestMsg { string hash = 1; } +message ValidateBasicMqttCredRequestMsg { + string clientId = 1; + string userName = 2; + string password = 3; +} + message ValidateDeviceCredentialsResponseMsg { DeviceInfoProto deviceInfo = 1; string credentialsBody = 2; + bytes profileBody = 3; } message GetOrCreateDeviceFromGatewayRequestMsg { @@ -157,6 +168,7 @@ message GetOrCreateDeviceFromGatewayRequestMsg { message GetOrCreateDeviceFromGatewayResponseMsg { DeviceInfoProto deviceInfo = 1; + bytes profileBody = 2; } message GetTenantRoutingInfoRequestMsg { @@ -169,6 +181,24 @@ message GetTenantRoutingInfoResponseMsg { bool isolatedTbRuleEngine = 2; } +message GetDeviceProfileRequestMsg { + int64 profileIdMSB = 1; + int64 profileIdLSB = 2; +} + +message GetDeviceProfileResponseMsg { + bytes data = 1; +} + +message DeviceProfileUpdateMsg { + bytes data = 1; +} + +message DeviceProfileDeleteMsg { + int64 profileIdMSB = 1; + int64 profileIdLSB = 2; +} + message SessionCloseNotificationProto { string message = 1; } @@ -398,6 +428,7 @@ message FromDeviceRPCResponseProto { string response = 3; int32 error = 4; } + /** * Main messages; */ @@ -408,13 +439,16 @@ message TransportApiRequestMsg { ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2; GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4; + GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5; + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; } /* Response from ThingsBoard Core Service to Transport Service */ message TransportApiResponseMsg { - ValidateDeviceCredentialsResponseMsg validateTokenResponseMsg = 1; + ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1; GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4; + GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5; } /* Messages that are handled by ThingsBoard Core Service */ @@ -455,4 +489,6 @@ message ToTransportMsg { AttributeUpdateNotificationMsg attributeUpdateNotification = 5; ToDeviceRpcRequestMsg toDeviceRequest = 6; ToServerRpcResponseMsg toServerResponse = 7; + DeviceProfileUpdateMsg deviceProfileUpdateMsg = 8; + DeviceProfileDeleteMsg deviceProfileDeleteMsg = 9; } diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 0ab8859238..10c3ccb040 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 221f126469..3b04a20abe 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 3b4b26aa47..b846bac38c 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -24,6 +24,7 @@ import org.eclipse.californium.core.network.ExchangeObserver; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.springframework.util.ReflectionUtils; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.SessionMsgType; @@ -32,6 +33,8 @@ import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import java.lang.reflect.Field; @@ -143,7 +146,7 @@ public class CoapTransportResource extends CoapResource { return; } - transportService.process(TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), + transportService.process(DeviceTransportType.DEFAULT, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), new DeviceAuthCallback(transportContext, exchange, sessionInfo -> { UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); try { @@ -295,7 +298,7 @@ public class CoapTransportResource extends CoapResource { return this; } - private static class DeviceAuthCallback implements TransportServiceCallback { + private static class DeviceAuthCallback implements TransportServiceCallback { private final TransportContext transportContext; private final CoapExchange exchange; private final Consumer onSuccess; @@ -307,22 +310,9 @@ public class CoapTransportResource extends CoapResource { } @Override - public void onSuccess(TransportProtos.ValidateDeviceCredentialsResponseMsg msg) { + public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (msg.hasDeviceInfo()) { - UUID sessionId = UUID.randomUUID(); - TransportProtos.DeviceInfoProto deviceInfoProto = msg.getDeviceInfo(); - TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() - .setNodeId(transportContext.getNodeId()) - .setTenantIdMSB(deviceInfoProto.getTenantIdMSB()) - .setTenantIdLSB(deviceInfoProto.getTenantIdLSB()) - .setDeviceIdMSB(deviceInfoProto.getDeviceIdMSB()) - .setDeviceIdLSB(deviceInfoProto.getDeviceIdLSB()) - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceName(msg.getDeviceInfo().getDeviceName()) - .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .build(); - onSuccess.accept(sessionInfo); + onSuccess.accept(SessionInfoCreator.create(msg, transportContext, UUID.randomUUID())); } else { exchange.respond(ResponseCode.UNAUTHORIZED); } diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 9e2ba641dd..040f7c372e 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index be2dd20f15..404024b202 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -30,12 +30,15 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; @@ -76,7 +79,7 @@ public class DeviceApiController { @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { GetAttributeRequestMsg.Builder request = GetAttributeRequestMsg.newBuilder().setRequestId(0); List clientKeySet = !StringUtils.isEmpty(clientKeys) ? Arrays.asList(clientKeys.split(",")) : null; @@ -98,7 +101,7 @@ public class DeviceApiController { public DeferredResult postDeviceAttributes(@PathVariable("deviceToken") String deviceToken, @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)), @@ -112,7 +115,7 @@ public class DeviceApiController { public DeferredResult postTelemetry(@PathVariable("deviceToken") String deviceToken, @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)), @@ -126,7 +129,7 @@ public class DeviceApiController { public DeferredResult claimDevice(@PathVariable("deviceToken") String deviceToken, @RequestBody(required = false) String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); @@ -141,7 +144,7 @@ public class DeviceApiController { @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter), @@ -158,7 +161,7 @@ public class DeviceApiController { @PathVariable("requestId") Integer requestId, @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.process(sessionInfo, ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(json).build(), new HttpOkCallback(responseWriter)); @@ -170,7 +173,7 @@ public class DeviceApiController { public DeferredResult postRpcRequest(@PathVariable("deviceToken") String deviceToken, @RequestBody String json, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { JsonObject request = new JsonParser().parse(json).getAsJsonObject(); TransportService transportService = transportContext.getTransportService(); @@ -188,7 +191,7 @@ public class DeviceApiController { @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter), @@ -200,7 +203,7 @@ public class DeviceApiController { return responseWriter; } - private static class DeviceAuthCallback implements TransportServiceCallback { + private static class DeviceAuthCallback implements TransportServiceCallback { private final TransportContext transportContext; private final DeferredResult responseWriter; private final Consumer onSuccess; @@ -212,22 +215,9 @@ public class DeviceApiController { } @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { + public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (msg.hasDeviceInfo()) { - UUID sessionId = UUID.randomUUID(); - DeviceInfoProto deviceInfoProto = msg.getDeviceInfo(); - SessionInfoProto sessionInfo = SessionInfoProto.newBuilder() - .setNodeId(transportContext.getNodeId()) - .setTenantIdMSB(deviceInfoProto.getTenantIdMSB()) - .setTenantIdLSB(deviceInfoProto.getTenantIdLSB()) - .setDeviceIdMSB(deviceInfoProto.getDeviceIdMSB()) - .setDeviceIdLSB(deviceInfoProto.getDeviceIdLSB()) - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceName(msg.getDeviceInfo().getDeviceName()) - .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .build(); - onSuccess.accept(sessionInfo); + onSuccess.accept(SessionInfoCreator.create(msg, transportContext, UUID.randomUUID())); } else { responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); } diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 3b09bdff40..8a02c940a7 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 0c872c351f..119f841c1e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -24,9 +24,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.util.SslUtil; @@ -156,12 +158,12 @@ public class MqttSslHandlerProvider { String sha3Hash = EncryptionUtil.getSha3Hash(strCert); final String[] credentialsBodyHolder = new String[1]; CountDownLatch latch = new CountDownLatch(1); - transportService.process(TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { + transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback() { @Override - public void onSuccess(TransportProtos.ValidateDeviceCredentialsResponseMsg msg) { - if (!StringUtils.isEmpty(msg.getCredentialsBody())) { - credentialsBodyHolder[0] = msg.getCredentialsBody(); + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + credentialsBodyHolder[0] = msg.getCredentials(); } latch.countDown(); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index 134c7bb701..6695fa24ac 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -24,7 +24,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; -import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; /** * Created by ashvayka on 04.10.18. @@ -40,12 +41,20 @@ public class MqttTransportContext extends TransportContext { @Getter @Autowired - private MqttTransportAdaptor adaptor; + private JsonMqttAdaptor jsonMqttAdaptor; + + @Getter + @Autowired + private ProtoMqttAdaptor protoMqttAdaptor; @Getter @Value("${transport.mqtt.netty.max_payload_size}") private Integer maxPayloadSize; + @Getter + @Value("${transport.mqtt.netty.skip_validity_check_for_client_cert:false}") + private boolean skipValidityCheckForClientCert; + @Getter @Setter private SslHandler sslHandler; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index d62ab6a7ee..6a09bdda40 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -38,19 +38,20 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; -import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; @@ -67,9 +68,9 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.Date; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; -import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; @@ -90,24 +91,21 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final UUID sessionId; private final MqttTransportContext context; - private final MqttTransportAdaptor adaptor; private final TransportService transportService; private final SslHandler sslHandler; private final ConcurrentMap mqttQoSMap; - private volatile SessionInfoProto sessionInfo; + private final DeviceSessionCtx deviceSessionCtx; private volatile InetSocketAddress address; - private volatile DeviceSessionCtx deviceSessionCtx; private volatile GatewaySessionHandler gatewaySessionHandler; MqttTransportHandler(MqttTransportContext context, SslHandler sslHandler) { this.sessionId = UUID.randomUUID(); this.context = context; this.transportService = context.getTransportService(); - this.adaptor = context.getAdaptor(); this.sslHandler = sslHandler; this.mqttQoSMap = new ConcurrentHashMap<>(); - this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap); + this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap, context); } @Override @@ -148,7 +146,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement case PINGREQ: if (checkConnected(ctx, msg)) { ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } break; case DISCONNECT: @@ -172,7 +170,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (topicName.startsWith(MqttTopics.BASE_GATEWAY_API_TOPIC)) { if (gatewaySessionHandler != null) { handleGatewayPublishMsg(topicName, msgId, mqttMsg); - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } } else { processDevicePublish(ctx, mqttMsg, topicName, msgId); @@ -211,26 +209,27 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { - if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_TOPIC)) { - TransportProtos.PostTelemetryMsg postTelemetryMsg = adaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); - } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_TOPIC)) { - TransportProtos.PostAttributeMsg postAttributeMsg = adaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + MqttTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor(); + if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) { + TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); + } else if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { + TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { - TransportProtos.GetAttributeRequestMsg getAttributeMsg = adaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); + TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_TOPIC)) { - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = adaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) { - TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = adaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); } else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) { - TransportProtos.ClaimDeviceMsg claimDeviceMsg = adaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); } else { - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } } catch (AdaptorException e) { log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); @@ -239,6 +238,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final int msgId, final T msg) { return new TransportServiceCallback() { @Override @@ -270,22 +270,22 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement try { switch (topic) { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); registerSubQoS(topic, grantedQoSList, reqQoS); activityReported = true; break; } case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); registerSubQoS(topic, grantedQoSList, reqQoS); activityReported = true; break; } case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: case MqttTopics.GATEWAY_RPC_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: registerSubQoS(topic, grantedQoSList, reqQoS); break; default: @@ -299,7 +299,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } if (!activityReported) { - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList)); } @@ -320,12 +320,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement try { switch (topicName) { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), + TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), null); activityReported = true; break; } case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), + TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), null); activityReported = true; break; } @@ -335,14 +337,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } if (!activityReported) { - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId())); } private MqttMessage createUnSubAckMessage(int msgId) { MqttFixedHeader mqttFixedHeader = - new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0); + new MqttFixedHeader(UNSUBACK, false, AT_MOST_ONCE, false, 0); MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); } @@ -360,35 +362,40 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { String userName = msg.payload().userName(); log.info("[{}] Processing connect msg for client with user name: {}!", sessionId, userName); - if (StringUtils.isEmpty(userName)) { - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); - ctx.close(); - } else { - transportService.process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(userName).build(), - new TransportServiceCallback() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { - onValidateDeviceResponse(msg, ctx); - } - - @Override - public void onError(Throwable e) { - log.trace("[{}] Failed to process credentials: {}", address, userName, e); - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); - ctx.close(); - } - }); + TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder() + .setClientId(msg.payload().clientIdentifier()) + .setUserName(userName); + String password = msg.payload().password(); + if (password != null) { + request.setPassword(password); } + transportService.process(DeviceTransportType.MQTT, request.build(), + new TransportServiceCallback() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + onValidateDeviceResponse(msg, ctx); + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to process credentials: {}", address, userName, e); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); + ctx.close(); + } + }); } private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) { try { + if(!context.isSkipValidityCheckForClientCert()){ + cert.checkValidity(); + } String strCert = SslUtil.getX509CertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - transportService.process(ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { + transportService.process(DeviceTransportType.MQTT, ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback() { @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { + public void onSuccess(ValidateDeviceCredentialsResponse msg) { onValidateDeviceResponse(msg, ctx); } @@ -445,7 +452,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private static MqttSubAckMessage createSubAckMessage(Integer msgId, List grantedQoSList) { MqttFixedHeader mqttFixedHeader = - new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0); + new MqttFixedHeader(SUBACK, false, AT_MOST_ONCE, false, 0); MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(grantedQoSList); return new MqttSubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubAckPayload); @@ -457,7 +464,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public static MqttPubAckMessage createMqttPubAckMsg(int requestId) { MqttFixedHeader mqttFixedHeader = - new MqttFixedHeader(PUBACK, false, AT_LEAST_ONCE, false, 0); + new MqttFixedHeader(PUBACK, false, AT_MOST_ONCE, false, 0); MqttMessageIdVariableHeader mqttMsgIdVariableHeader = MqttMessageIdVariableHeader.from(requestId); return new MqttPubAckMessage(mqttFixedHeader, mqttMsgIdVariableHeader); @@ -474,13 +481,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } private void checkGatewaySession() { - DeviceInfoProto device = deviceSessionCtx.getDeviceInfo(); + TransportDeviceInfo device = deviceSessionCtx.getDeviceInfo(); try { JsonNode infoNode = context.getMapper().readTree(device.getAdditionalInfo()); if (infoNode != null) { JsonNode gatewayNode = infoNode.get("gateway"); if (gatewayNode != null && gatewayNode.asBoolean()) { - gatewaySessionHandler = new GatewaySessionHandler(context, deviceSessionCtx, sessionId); + gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId); } } } catch (IOException e) { @@ -495,8 +502,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void doDisconnect() { if (deviceSessionCtx.isConnected()) { - transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); - transportService.deregisterSession(sessionInfo); + transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); if (gatewaySessionHandler != null) { gatewaySessionHandler.onGatewayDisconnect(); } @@ -504,27 +511,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private void onValidateDeviceResponse(ValidateDeviceCredentialsResponseMsg msg, ChannelHandlerContext ctx) { + private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx) { if (!msg.hasDeviceInfo()) { ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED)); ctx.close(); } else { deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); - sessionInfo = SessionInfoProto.newBuilder() - .setNodeId(context.getNodeId()) - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceIdMSB(msg.getDeviceInfo().getDeviceIdMSB()) - .setDeviceIdLSB(msg.getDeviceInfo().getDeviceIdLSB()) - .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB()) - .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB()) - .setDeviceName(msg.getDeviceInfo().getDeviceName()) - .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .build(); - transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), new TransportServiceCallback() { + deviceSessionCtx.setDeviceProfile(msg.getDeviceProfile()); + deviceSessionCtx.setSessionInfo(SessionInfoCreator.create(msg, context, sessionId)); + transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), new TransportServiceCallback() { @Override public void onSuccess(Void msg) { - transportService.registerAsyncSession(sessionInfo, MqttTransportHandler.this); + transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); checkGatewaySession(); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); log.info("[{}] Client connected!", sessionId); @@ -543,7 +541,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { try { - adaptor.convertToPublish(deviceSessionCtx, response).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, response).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } @@ -552,7 +550,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg notification) { try { - adaptor.convertToPublish(deviceSessionCtx, notification).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, notification).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); } @@ -568,19 +566,24 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { log.trace("[{}] Received RPC command to device", sessionId); try { - adaptor.convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { - log.trace("[{}] Failed to convert device RPC commandto MQTT msg", sessionId, e); + log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); } } @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { - log.trace("[{}] Received RPC command to device", sessionId); + log.trace("[{}] Received RPC command to server", sessionId); try { - adaptor.convertToPublish(deviceSessionCtx, rpcResponse).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcResponse).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { - log.trace("[{}] Failed to convert device RPC commandto MQTT msg", sessionId, e); + log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); } } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + deviceSessionCtx.onProfileUpdate(deviceProfile); + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index caa627451f..811e29cdae 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -34,10 +34,11 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.transport.mqtt.MqttTopics; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; import java.util.Optional; @@ -47,12 +48,13 @@ import java.util.UUID; /** * @author Andrew Shvayka */ -@Component("JsonMqttAdaptor") +@Component @Slf4j public class JsonMqttAdaptor implements MqttTransportAdaptor { + protected static final Charset UTF8 = StandardCharsets.UTF_8; + private static final Gson GSON = new Gson(); - private static final Charset UTF8 = Charset.forName("UTF-8"); private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); @Override @@ -75,12 +77,82 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } + @Override + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), true); + try { + return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + @Override public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + return processGetAttributeRequestMsg(inbound, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + } + + @Override + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + return processToDeviceRpcResponseMsg(inbound, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); + } + + @Override + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + return processToServerRpcRequestMsg(ctx, inbound, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + return processConvertFromAttributeResponseMsg(ctx, responseMsg, MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, JsonConverter.toJson(notificationMsg))); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, notificationMsg); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, result)); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, JsonConverter.toGatewayJson(deviceName, rpcRequest))); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); + } + + public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { + String payload = validatePayload(sessionId, payloadData, false); + try { + return new JsonParser().parse(payload); + } catch (JsonSyntaxException ex) { + log.warn("Payload is in incorrect format: {}", payload); + throw new AdaptorException(ex); + } + } + + protected TransportProtos.GetAttributeRequestMsg processGetAttributeRequestMsg(MqttPublishMessage inbound, String topic) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); try { TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(Integer.valueOf(topicName.substring(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX.length()))); + result.setRequestId(getRequestId(topicName, topic)); String payload = inbound.payload().toString(UTF8); JsonElement requestBody = new JsonParser().parse(payload); Set clientKeys = toStringSet(requestBody, "clientKeys"); @@ -98,93 +170,53 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + protected TransportProtos.ToDeviceRpcResponseMsg processToDeviceRpcResponseMsg(MqttPublishMessage inbound, String topic) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); try { - Integer requestId = Integer.valueOf(topicName.substring(MqttTopics.DEVICE_RPC_RESPONSE_TOPIC.length())); + int requestId = getRequestId(topicName, topic); String payload = inbound.payload().toString(UTF8); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(payload).build(); } catch (RuntimeException e) { - log.warn("Failed to decode get attributes request", e); + log.warn("Failed to decode Rpc response", e); throw new AdaptorException(e); } } - @Override - public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + protected TransportProtos.ToServerRpcRequestMsg processToServerRpcRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topic) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - Integer requestId = Integer.valueOf(topicName.substring(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC.length())); + int requestId = getRequestId(topicName, topic); return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); } catch (IllegalStateException | JsonSyntaxException ex) { throw new AdaptorException(ex); } } - @Override - public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { - String payload = validatePayload(ctx.getSessionId(), inbound.payload(), true); - try { - return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); - } catch (IllegalStateException | JsonSyntaxException ex) { - throw new AdaptorException(ex); - } - } - - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + protected Optional processConvertFromAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topic) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { - Integer requestId = responseMsg.getRequestId(); + int requestId = responseMsg.getRequestId(); if (requestId >= 0) { return Optional.of(createMqttPublishMsg(ctx, - MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId, + topic + requestId, JsonConverter.toJson(responseMsg))); } return Optional.empty(); } } - @Override - public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + protected Optional processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg, String topic) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg); - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, result)); + return Optional.of(createMqttPublishMsg(ctx, topic, result)); } } - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, JsonConverter.toJson(notificationMsg))); - } - - @Override - public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException { - JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, notificationMsg); - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, result)); - } - - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); - } - - @Override - public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, JsonConverter.toGatewayJson(deviceName, rpcRequest))); - } - - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); - } - - private MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, JsonElement json) { + protected MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, JsonElement json) { MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); @@ -202,16 +234,6 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { - String payload = validatePayload(sessionId, payloadData, false); - try { - return new JsonParser().parse(payload); - } catch (JsonSyntaxException ex) { - log.warn("Payload is in incorrect format: {}", payload); - throw new AdaptorException(ex); - } - } - private static String validatePayload(UUID sessionId, ByteBuf payloadData, boolean isEmptyPayloadAllowed) throws AdaptorException { String payload = payloadData.toString(UTF8); if (payload == null) { @@ -223,4 +245,8 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { return payload; } + private int getRequestId(String topicName, String topic) { + return Integer.parseInt(topicName.substring(topic.length())); + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java new file mode 100644 index 0000000000..f66daf2289 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -0,0 +1,193 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.adaptors; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; + +import java.util.Optional; + +@Component +@Slf4j +public class ProtoMqttAdaptor implements MqttTransportAdaptor { + + private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); + + @Override + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + try { + return ProtoConverter.convertToTelemetryProto(bytes); + } catch (InvalidProtocolBufferException | IllegalArgumentException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + try { + return ProtoConverter.validatePostAttributeMsg(bytes); + } catch (InvalidProtocolBufferException | IllegalArgumentException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + try { + return ProtoConverter.convertToClaimDeviceProto(ctx.getDeviceId(), bytes); + } catch (InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + String topicName = inbound.variableHeader().topicName(); + int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + try { + return ProtoConverter.convertToGetAttributeRequestMessage(bytes, requestId); + } catch (InvalidProtocolBufferException e) { + log.warn("Failed to decode get attributes request", e); + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + byte[] bytes = toBytes(mqttMsg.payload()); + try { + return TransportProtos.ToDeviceRpcResponseMsg.parseFrom(bytes); + } catch (RuntimeException | InvalidProtocolBufferException e) { + log.warn("Failed to decode Rpc response", e); + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + byte[] bytes = toBytes(mqttMsg.payload()); + String topicName = mqttMsg.variableHeader().topicName(); + try { + int requestId = getRequestId(topicName, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); + return ProtoConverter.convertToServerRpcRequest(bytes, requestId); + } catch (InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + if (!StringUtils.isEmpty(responseMsg.getError())) { + throw new AdaptorException(responseMsg.getError()); + } else { + int requestId = responseMsg.getRequestId(); + if (requestId >= 0) { + return Optional.of(createMqttPublishMsg(ctx, + MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId, + responseMsg.toByteArray())); + } + return Optional.empty(); + } + } + + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), rpcRequest.toByteArray())); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), rpcResponse.toByteArray())); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, notificationMsg.toByteArray())); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + if (!StringUtils.isEmpty(responseMsg.getError())) { + throw new AdaptorException(responseMsg.getError()); + } else { + TransportApiProtos.GatewayAttributeResponseMsg.Builder responseMsgBuilder = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); + responseMsgBuilder.setDeviceName(deviceName); + responseMsgBuilder.setResponseMsg(responseMsg); + byte[] payloadBytes = responseMsgBuilder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, payloadBytes)); + } + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder builder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setNotificationMsg(notificationMsg); + byte[] payloadBytes = builder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, payloadBytes)); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + TransportApiProtos.GatewayDeviceRpcRequestMsg.Builder builder = TransportApiProtos.GatewayDeviceRpcRequestMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setRpcRequestMsg(rpcRequest); + byte[] payloadBytes = builder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, payloadBytes)); + } + + public static byte[] toBytes(ByteBuf inbound) { + byte[] bytes = new byte[inbound.readableBytes()]; + int readerIndex = inbound.readerIndex(); + inbound.getBytes(readerIndex, bytes); + return bytes; + } + + private MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, byte[] payloadBytes) { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); + MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); + ByteBuf payload = ALLOCATOR.buffer(); + payload.writeBytes(payloadBytes); + return new MqttPublishMessage(mqttFixedHeader, header, payload); + } + + private int getRequestId(String topicName, String topic) { + return Integer.parseInt(topicName.substring(topic.length())); + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index d8732802db..ba701ba56c 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -18,6 +18,15 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.channel.ChannelHandlerContext; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.transport.mqtt.MqttTransportContext; +import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; import java.util.UUID; import java.util.concurrent.ConcurrentMap; @@ -31,10 +40,19 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { @Getter private ChannelHandlerContext channel; - private AtomicInteger msgIdSeq = new AtomicInteger(0); - public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap) { + @Getter + private MqttTransportContext context; + + private final AtomicInteger msgIdSeq = new AtomicInteger(0); + + private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); + private volatile MqttTopicFilter attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); + private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; + + public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap, MqttTransportContext context) { super(sessionId, mqttQoSMap); + this.context = context; } public void setChannel(ChannelHandlerContext channel) { @@ -44,4 +62,46 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public int nextMsgId() { return msgIdSeq.incrementAndGet(); } + + public boolean isDeviceTelemetryTopic(String topicName) { return telemetryTopicFilter.filter(topicName); } + + public boolean isDeviceAttributesTopic(String topicName) { + return attributesTopicFilter.filter(topicName); + } + + public MqttTransportAdaptor getPayloadAdaptor() { + return payloadType.equals(TransportPayloadType.JSON) ? context.getJsonMqttAdaptor() : context.getProtoMqttAdaptor(); + } + + public boolean isJsonPayloadType() { + return payloadType.equals(TransportPayloadType.JSON); + } + + @Override + public void setDeviceProfile(DeviceProfile deviceProfile) { + super.setDeviceProfile(deviceProfile); + updateTopicFilters(deviceProfile); + } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + super.onProfileUpdate(deviceProfile); + updateTopicFilters(deviceProfile); + } + + + private void updateTopicFilters(DeviceProfile deviceProfile) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) && + transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { + MqttDeviceProfileTransportConfiguration mqttConfig = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + payloadType = mqttConfig.getTransportPayloadType(); + telemetryTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceTelemetryTopic()); + attributesTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceAttributesTopic()); + } else { + telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); + attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); + } + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java index c137da0b82..da93405e63 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java @@ -16,9 +16,10 @@ package org.thingsboard.server.transport.mqtt.session; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.transport.SessionMsgListener; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import java.util.UUID; @@ -31,25 +32,28 @@ import java.util.concurrent.ConcurrentMap; public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext implements SessionMsgListener { private final GatewaySessionHandler parent; - private final SessionInfoProto sessionInfo; - public GatewayDeviceSessionCtx(GatewaySessionHandler parent, DeviceInfoProto deviceInfo, ConcurrentMap mqttQoSMap) { + public GatewayDeviceSessionCtx(GatewaySessionHandler parent, TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, ConcurrentMap mqttQoSMap) { super(UUID.randomUUID(), mqttQoSMap); this.parent = parent; - this.sessionInfo = SessionInfoProto.newBuilder() + setSessionInfo(SessionInfoProto.newBuilder() .setNodeId(parent.getNodeId()) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceIdMSB(deviceInfo.getDeviceIdMSB()) - .setDeviceIdLSB(deviceInfo.getDeviceIdLSB()) - .setTenantIdMSB(deviceInfo.getTenantIdMSB()) - .setTenantIdLSB(deviceInfo.getTenantIdLSB()) + .setDeviceIdMSB(deviceInfo.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceInfo.getDeviceId().getId().getLeastSignificantBits()) + .setTenantIdMSB(deviceInfo.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(deviceInfo.getTenantId().getId().getLeastSignificantBits()) .setDeviceName(deviceInfo.getDeviceName()) .setDeviceType(deviceInfo.getDeviceType()) .setGwSessionIdMSB(parent.getSessionId().getMostSignificantBits()) .setGwSessionIdLSB(parent.getSessionId().getLeastSignificantBits()) - .build(); + .setDeviceProfileIdMSB(deviceInfo.getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) + .build()); setDeviceInfo(deviceInfo); + setDeviceProfile(deviceProfile); } @Override @@ -62,14 +66,10 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple return parent.nextMsgId(); } - SessionInfoProto getSessionInfo() { - return sessionInfo; - } - @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { try { - parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), response).ifPresent(parent::writeAndFlush); + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), response).ifPresent(parent::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } @@ -78,26 +78,26 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple @Override public void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg notification) { try { - parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), notification).ifPresent(parent::writeAndFlush); + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), notification).ifPresent(parent::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } } - @Override - public void onRemoteSessionCloseCommand(TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { - parent.deregisterSession(getDeviceInfo().getDeviceName()); - } - @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg request) { try { - parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), request).ifPresent(parent::writeAndFlush); + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), request).ifPresent(parent::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } } + @Override + public void onRemoteSessionCloseCommand(TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { + parent.deregisterSession(getDeviceInfo().getDeviceName()); + } + @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { // This feature is not supported in the TB IoT Gateway yet. diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index fa81f584a0..36f78da63a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -25,30 +25,38 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ProtocolStringList; +import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.service.DefaultTransportService; +import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.transport.mqtt.MqttTransportContext; import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; import javax.annotation.Nullable; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -69,7 +77,7 @@ public class GatewaySessionHandler { private final MqttTransportContext context; private final TransportService transportService; - private final DeviceInfoProto gateway; + private final TransportDeviceInfo gateway; private final UUID sessionId; private final ConcurrentMap deviceCreationLockMap; private final ConcurrentMap devices; @@ -78,8 +86,8 @@ public class GatewaySessionHandler { private final ChannelHandlerContext channel; private final DeviceSessionCtx deviceSessionCtx; - public GatewaySessionHandler(MqttTransportContext context, DeviceSessionCtx deviceSessionCtx, UUID sessionId) { - this.context = context; + public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { + this.context = deviceSessionCtx.getContext(); this.transportService = context.getTransportService(); this.deviceSessionCtx = deviceSessionCtx; this.gateway = deviceSessionCtx.getDeviceInfo(); @@ -91,10 +99,108 @@ public class GatewaySessionHandler { this.channel = deviceSessionCtx.getChannel(); } - public void onDeviceConnect(MqttPublishMessage msg) throws AdaptorException { - JsonElement json = getJson(msg); - String deviceName = checkDeviceName(getDeviceName(json)); - String deviceType = getDeviceType(json); + public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceConnectJson(mqttMsg); + } else { + onDeviceConnectProto(mqttMsg); + } + } + + public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceDisconnectJson(mqttMsg); + } else { + onDeviceDisconnectProto(mqttMsg); + } + } + + public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceTelemetryJson(msgId, payload); + } else { + onDeviceTelemetryProto(msgId, payload); + } + } + + public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceClaimJson(msgId, payload); + } else { + onDeviceClaimProto(msgId, payload); + } + } + + public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceAttributesJson(msgId, payload); + } else { + onDeviceAttributesProto(msgId, payload); + } + } + + public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceAttributesRequestJson(mqttMsg); + } else { + onDeviceAttributesRequestProto(mqttMsg); + } + } + + public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceRpcResponseJson(msgId, payload); + } else { + onDeviceRpcResponseProto(msgId, payload); + } + } + + public void onGatewayDisconnect() { + devices.forEach(this::deregisterSession); + } + + public String getNodeId() { + return context.getNodeId(); + } + + public UUID getSessionId() { + return sessionId; + } + + public MqttTransportAdaptor getPayloadAdaptor() { + return deviceSessionCtx.getPayloadAdaptor(); + } + + void deregisterSession(String deviceName) { + GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); + if (deviceSessionCtx != null) { + deregisterSession(deviceName, deviceSessionCtx); + } else { + log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); + } + } + + void writeAndFlush(MqttMessage mqttMessage) { + channel.writeAndFlush(mqttMessage); + } + + int nextMsgId() { + return deviceSessionCtx.nextMsgId(); + } + + private boolean isJsonPayloadType() { + return deviceSessionCtx.isJsonPayloadType(); + } + + private void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback() { @Override @@ -140,12 +246,12 @@ public class GatewaySessionHandler { transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() .setDeviceName(deviceName) .setDeviceType(deviceType) - .setGatewayIdMSB(gateway.getDeviceIdMSB()) - .setGatewayIdLSB(gateway.getDeviceIdLSB()).build(), - new TransportServiceCallback() { + .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) + .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()).build(), + new TransportServiceCallback() { @Override - public void onSuccess(GetOrCreateDeviceFromGatewayResponseMsg msg) { - GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), mqttQoSMap); + public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { + GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap); if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); @@ -181,28 +287,50 @@ public class GatewaySessionHandler { return future; } - public void onDeviceDisconnect(MqttPublishMessage msg) throws AdaptorException { + private int getMsgId(MqttPublishMessage mqttMsg) { + return mqttMsg.variableHeader().packetId(); + } + + private void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { + JsonElement json = getJson(mqttMsg); + String deviceName = checkDeviceName(getDeviceName(json)); + String deviceType = getDeviceType(json); + processOnConnect(mqttMsg, deviceName, deviceType); + } + + private void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); + processOnConnect(mqttMsg, deviceName, deviceType); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { String deviceName = checkDeviceName(getDeviceName(getJson(msg))); - deregisterSession(deviceName); - ack(msg); + processOnDisconnect(msg, deviceName); } - void deregisterSession(String deviceName) { - GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); - if (deviceSessionCtx != null) { - deregisterSession(deviceName, deviceSessionCtx); - } else { - log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); + private void onDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + processOnDisconnect(mqttMsg, deviceName); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); } } - public void onGatewayDisconnect() { - devices.forEach(this::deregisterSession); + private void processOnDisconnect(MqttPublishMessage msg, String deviceName) { + deregisterSession(deviceName); + ack(msg); } - public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); + private void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry deviceEntry : jsonObj.entrySet()) { @@ -216,10 +344,9 @@ public class GatewaySessionHandler { } try { TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); } catch (Throwable e) { - UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB()); - log.warn("[{}][{}] Failed to convert telemetry: {}", gatewayId, deviceName, deviceEntry.getValue(), e); + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); } } @@ -234,9 +361,47 @@ public class GatewaySessionHandler { } } - public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); + private void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); + List deviceMsgList = telemetryMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(deviceMsgList)) { + deviceMsgList.forEach(telemetryMsg -> { + String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processPostTelemetryMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + } + + private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry deviceEntry : jsonObj.entrySet()) { @@ -251,10 +416,9 @@ public class GatewaySessionHandler { try { DeviceId deviceId = deviceCtx.getDeviceId(); TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); } catch (Throwable e) { - UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB()); - log.warn("[{}][{}] Failed to convert claim message: {}", gatewayId, deviceName, deviceEntry.getValue(), e); + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); } } @@ -269,9 +433,51 @@ public class GatewaySessionHandler { } } - public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); + private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); + List claimMsgList = claimMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(claimMsgList)) { + claimMsgList.forEach(claimDeviceMsg -> { + String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); + if (claimRequest == null) { + throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); + } + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processClaimDeviceMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + } + + private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry deviceEntry : jsonObj.entrySet()) { @@ -284,7 +490,7 @@ public class GatewaySessionHandler { throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); } TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); } @Override @@ -298,34 +504,49 @@ public class GatewaySessionHandler { } } - public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - Integer requestId = jsonObj.get("id").getAsInt(); - String data = jsonObj.get("data").toString(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); - } + private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); + List attributesMsgList = attributesMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(attributesMsgList)) { + attributesMsgList.forEach(attributesMsg -> { + String deviceName = checkDeviceName(attributesMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + if (kvListProto == null) { + throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); + } + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device teleemtry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); } } - public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException { + private void processPostAttributesMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + } + + private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); @@ -342,27 +563,47 @@ public class GatewaySessionHandler { keys.add(keyObj.getAsString()); } } - TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(requestId); + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(msg, deviceName, requestMsg); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } - if (clientScope) { - result.addAllClientAttributeNames(keys); - } else { - result.addAllSharedAttributeNames(keys); - } - TransportProtos.GetAttributeRequestMsg requestMsg = result.build(); - int msgId = msg.variableHeader().packetId(); + private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); + int requestId = gatewayAttributesRequestMsg.getId(); + boolean clientScope = gatewayAttributesRequestMsg.getClient(); + ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); + Set keys = new HashSet<>(keysList); + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); Futures.addCallback(checkDeviceConnected(deviceName), new FutureCallback() { @Override public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + Integer requestId = jsonObj.get("id").getAsInt(); + String data = jsonObj.get("data").toString(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); } @Override public void onFailure(Throwable t) { - ack(msg); - log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); } }, context.getExecutor()); } else { @@ -370,6 +611,64 @@ public class GatewaySessionHandler { } } + private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); + String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + Integer requestId = gatewayRpcResponseMsg.getId(); + String data = gatewayRpcResponseMsg.getData(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processRpcResponseMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); + } + + private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { + int msgId = getMsgId(mqttMsg); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + } + + @Override + public void onFailure(Throwable t) { + ack(mqttMsg); + log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + + private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + result.setRequestId(requestId); + + if (clientScope) { + result.addAllClientAttributeNames(keys); + } else { + result.addAllSharedAttributeNames(keys); + } + return result.build(); + } + private ListenableFuture checkDeviceConnected(String deviceName) { GatewayDeviceSessionCtx ctx = devices.get(deviceName); if (ctx == null) { @@ -388,11 +687,11 @@ public class GatewaySessionHandler { } } - private String getDeviceName(JsonElement json) throws AdaptorException { + private String getDeviceName(JsonElement json) { return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); } - private String getDeviceType(JsonElement json) throws AdaptorException { + private String getDeviceType(JsonElement json) { JsonElement type = json.getAsJsonObject().get("type"); return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); } @@ -401,18 +700,15 @@ public class GatewaySessionHandler { return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); } - private void ack(MqttPublishMessage msg) { - if (msg.variableHeader().packetId() > 0) { - writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().packetId())); - } - } - - void writeAndFlush(MqttMessage mqttMessage) { - channel.writeAndFlush(mqttMessage); + private byte[] getBytes(ByteBuf payload) { + return ProtoMqttAdaptor.toBytes(payload); } - public String getNodeId() { - return context.getNodeId(); + private void ack(MqttPublishMessage msg) { + int msgId = getMsgId(msg); + if (msgId > 0) { + writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msgId)); + } } private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) { @@ -433,25 +729,9 @@ public class GatewaySessionHandler { @Override public void onError(Throwable e) { - log.trace("[{}] Failed to publish msg: {}", sessionId, deviceName, msg, e); + log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); ctx.close(); } }; } - - public MqttTransportContext getContext() { - return context; - } - - MqttTransportAdaptor getAdaptor() { - return context.getAdaptor(); - } - - int nextMsgId() { - return deviceSessionCtx.nextMsgId(); - } - - public UUID getSessionId() { - return sessionId; - } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java index ea5291d7bf..76e8843afa 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java @@ -16,7 +16,14 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.handler.codec.mqtt.MqttQoS; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; import java.util.List; import java.util.Map; @@ -52,5 +59,4 @@ public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionCo return MqttQoS.AT_LEAST_ONCE; } } - } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/EqualsTopicFilter.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/EqualsTopicFilter.java new file mode 100644 index 0000000000..539c8f2b7e --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/EqualsTopicFilter.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import lombok.Data; + +@Data +public class EqualsTopicFilter implements MqttTopicFilter { + + private final String filter; + + @Override + public boolean filter(String topic) { + return filter.equals(topic); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilter.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilter.java new file mode 100644 index 0000000000..005deb5d44 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilter.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +public interface MqttTopicFilter { + + boolean filter(String topic); + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactory.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactory.java new file mode 100644 index 0000000000..4d5a9a7c2b --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactory.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.profile.MqttTopics; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +public class MqttTopicFilterFactory { + + private static final ConcurrentMap filters = new ConcurrentHashMap<>(); + private static final MqttTopicFilter DEFAULT_TELEMETRY_TOPIC_FILTER = toFilter(MqttTopics.DEVICE_TELEMETRY_TOPIC); + private static final MqttTopicFilter DEFAULT_ATTRIBUTES_TOPIC_FILTER = toFilter(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + + public static MqttTopicFilter toFilter(String topicFilter) { + if (topicFilter == null || topicFilter.isEmpty()) { + throw new IllegalArgumentException("Topic filter can't be empty!"); + } + return filters.computeIfAbsent(topicFilter, filter -> { + if (filter.contains("+") || filter.contains("#")) { + String regex = filter + .replace("\\", "\\\\") + .replace("+", "[^/]+") + .replace("/#", "($|/.*)"); + log.debug("Converting [{}] to [{}]", filter, regex); + return new RegexTopicFilter(regex); + } else { + return new EqualsTopicFilter(filter); + } + }); + } + + public static MqttTopicFilter getDefaultTelemetryFilter() { + return DEFAULT_TELEMETRY_TOPIC_FILTER; + } + + public static MqttTopicFilter getDefaultAttributesFilter() { + return DEFAULT_ATTRIBUTES_TOPIC_FILTER; + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/RegexTopicFilter.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/RegexTopicFilter.java new file mode 100644 index 0000000000..d5f50ae8c9 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/RegexTopicFilter.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import lombok.Data; + +import java.util.regex.Pattern; + +@Data +public class RegexTopicFilter implements MqttTopicFilter { + + private final Pattern regex; + + public RegexTopicFilter(String regex) { + this.regex = Pattern.compile(regex); + } + + @Override + public boolean filter(String topic) { + return regex.matcher(topic).matches(); + } +} diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java new file mode 100644 index 0000000000..0b854d51ef --- /dev/null +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.script.ScriptException; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class MqttTopicFilterFactoryTest { + + private static String TEST_STR_1 = "Sensor/Temperature/House/48"; + private static String TEST_STR_2 = "Sensor/Temperature"; + private static String TEST_STR_3 = "Sensor/Temperature2/House/48"; + + @Test + public void metadataCanBeUpdated() throws ScriptException { + MqttTopicFilter filter = MqttTopicFilterFactory.toFilter("Sensor/Temperature/House/+"); + assertTrue(filter.filter(TEST_STR_1)); + assertFalse(filter.filter(TEST_STR_2)); + + filter = MqttTopicFilterFactory.toFilter("Sensor/+/House/#"); + assertTrue(filter.filter(TEST_STR_1)); + assertFalse(filter.filter(TEST_STR_2)); + + filter = MqttTopicFilterFactory.toFilter("Sensor/#"); + assertTrue(filter.filter(TEST_STR_1)); + assertTrue(filter.filter(TEST_STR_2)); + assertTrue(filter.filter(TEST_STR_3)); + + filter = MqttTopicFilterFactory.toFilter("Sensor/Temperature/#"); + assertTrue(filter.filter(TEST_STR_1)); + assertTrue(filter.filter(TEST_STR_2)); + assertFalse(filter.filter(TEST_STR_3)); + } + +} diff --git a/common/transport/pom.xml b/common/transport/pom.xml index d104406726..fbdb10d081 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 352ac7f38a..168c991eb6 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport @@ -60,6 +60,10 @@ com.google.code.gson gson + + de.ruedigermoeller + fst + org.slf4j slf4j-api @@ -103,6 +107,19 @@ org.apache.commons commons-lang3 + + com.google.protobuf + protobuf-java + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 8517bc9390..ccd63ca430 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.transport; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; @@ -35,4 +36,8 @@ public interface SessionMsgListener { void onToDeviceRpcRequest(ToDeviceRpcRequestMsg toDeviceRequest); void onToServerRpcResponse(ToServerRpcResponseMsg toServerResponse); + + default void onProfileUpdate(DeviceProfile deviceProfile) { + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java new file mode 100644 index 0000000000..ee05e59010 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport; + +import com.google.protobuf.ByteString; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; + +import java.util.Optional; + +public interface TransportProfileCache { + + DeviceProfile getOrCreate(DeviceProfileId id, ByteString profileBody); + + DeviceProfile get(DeviceProfileId id); + + void put(DeviceProfile profile); + + DeviceProfile put(ByteString profileBody); + + void evict(DeviceProfileId id); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 2af55c9206..3fc8ed96d1 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -15,10 +15,14 @@ */ package org.thingsboard.server.common.transport; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; @@ -30,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; @@ -41,14 +45,21 @@ public interface TransportService { GetTenantRoutingInfoResponseMsg getRoutingInfo(GetTenantRoutingInfoRequestMsg msg); - void process(ValidateDeviceTokenRequestMsg msg, - TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg, + TransportServiceCallback callback); - void process(ValidateDeviceX509CertRequestMsg msg, - TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateBasicMqttCredRequestMsg msg, + TransportServiceCallback callback); + + void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, + TransportServiceCallback callback); void process(GetOrCreateDeviceFromGatewayRequestMsg msg, - TransportServiceCallback callback); + TransportServiceCallback callback); + + void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback callback); + + void onProfileUpdate(DeviceProfile deviceProfile); boolean checkLimits(SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index a43437bb9d..428dfb0912 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -26,7 +26,6 @@ import org.apache.commons.lang3.math.NumberUtils; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -53,6 +52,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -454,11 +454,20 @@ public class JsonConverter { } public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { - Map> result = new HashMap<>(); + return convertToTelemetry(jsonElement, systemTs, false); + } + + public static Map> convertToSortedTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { + return convertToTelemetry(jsonElement, systemTs, true); + } + + public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs, boolean sorted) throws JsonSyntaxException { + Map> result = sorted ? new TreeMap<>() : new HashMap<>(); convertToTelemetry(jsonElement, systemTs, result, null); return result; } + private static void parseObject(Map> result, long systemTs, JsonObject jo) { if (jo.has("ts") && jo.has("values")) { parseWithTs(result, jo); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java new file mode 100644 index 0000000000..b7d3d2d36c --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -0,0 +1,164 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.adaptor; + +import com.google.gson.JsonParser; +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Slf4j +public class ProtoConverter { + + public static final JsonParser JSON_PARSER = new JsonParser(); + + public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { + TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload); + TransportProtos.PostTelemetryMsg.Builder postTelemetryMsgBuilder = TransportProtos.PostTelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto tsKvListProto = validateTsKvListProto(protoPayload); + postTelemetryMsgBuilder.addTsKvList(tsKvListProto); + return postTelemetryMsgBuilder.build(); + } + + public static TransportProtos.PostTelemetryMsg validatePostTelemetryMsg(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { + TransportProtos.PostTelemetryMsg msg = TransportProtos.PostTelemetryMsg.parseFrom(payload); + TransportProtos.PostTelemetryMsg.Builder postTelemetryMsgBuilder = TransportProtos.PostTelemetryMsg.newBuilder(); + List tsKvListProtoList = msg.getTsKvListList(); + if (!CollectionUtils.isEmpty(tsKvListProtoList)) { + List tsKvListProtos = new ArrayList<>(); + tsKvListProtoList.forEach(tsKvListProto -> { + TransportProtos.TsKvListProto transportTsKvListProto = validateTsKvListProto(tsKvListProto); + tsKvListProtos.add(transportTsKvListProto); + }); + postTelemetryMsgBuilder.addAllTsKvList(tsKvListProtos); + return postTelemetryMsgBuilder.build(); + } else { + throw new IllegalArgumentException("TsKv list is empty!"); + } + } + + public static TransportProtos.PostAttributeMsg validatePostAttributeMsg(byte[] bytes) throws IllegalArgumentException, InvalidProtocolBufferException { + TransportProtos.PostAttributeMsg proto = TransportProtos.PostAttributeMsg.parseFrom(bytes); + List kvList = proto.getKvList(); + if (!CollectionUtils.isEmpty(kvList)) { + List keyValueProtos = validateKeyValueProtos(kvList); + TransportProtos.PostAttributeMsg.Builder result = TransportProtos.PostAttributeMsg.newBuilder(); + result.addAllKv(keyValueProtos); + return result.build(); + } else { + throw new IllegalArgumentException("KeyValue list is empty!"); + } + } + + public static TransportProtos.ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, byte[] bytes) throws InvalidProtocolBufferException { + TransportApiProtos.ClaimDevice proto = TransportApiProtos.ClaimDevice.parseFrom(bytes); + String secretKey = proto.getSecretKey() != null ? proto.getSecretKey() : DataConstants.DEFAULT_SECRET_KEY; + long durationMs = proto.getDurationMs(); + return buildClaimDeviceMsg(deviceId, secretKey, durationMs); + } + + public static TransportProtos.GetAttributeRequestMsg convertToGetAttributeRequestMessage(byte[] bytes, int requestId) throws InvalidProtocolBufferException, RuntimeException { + TransportApiProtos.AttributesRequest proto = TransportApiProtos.AttributesRequest.parseFrom(bytes); + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + result.setRequestId(requestId); + String clientKeys = proto.getClientKeys(); + String sharedKeys = proto.getSharedKeys(); + if (!StringUtils.isEmpty(clientKeys)) { + List clientKeysList = Arrays.asList(clientKeys.split(",")); + result.addAllClientAttributeNames(clientKeysList); + } + if (!StringUtils.isEmpty(sharedKeys)) { + List sharedKeysList = Arrays.asList(sharedKeys.split(",")); + result.addAllSharedAttributeNames(sharedKeysList); + } + return result.build(); + } + + public static TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(byte[] bytes, int requestId) throws InvalidProtocolBufferException { + TransportApiProtos.RpcRequest proto = TransportApiProtos.RpcRequest.parseFrom(bytes); + String method = proto.getMethod(); + String params = proto.getParams(); + return TransportProtos.ToServerRpcRequestMsg.newBuilder().setRequestId(requestId).setMethodName(method).setParams(params).build(); + } + + private static TransportProtos.ClaimDeviceMsg buildClaimDeviceMsg(DeviceId deviceId, String secretKey, long durationMs) { + TransportProtos.ClaimDeviceMsg.Builder result = TransportProtos.ClaimDeviceMsg.newBuilder(); + return result + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setSecretKey(secretKey) + .setDurationMs(durationMs) + .build(); + } + + private static TransportProtos.TsKvListProto validateTsKvListProto(TransportProtos.TsKvListProto tsKvListProto) { + TransportProtos.TsKvListProto.Builder tsKvListBuilder = TransportProtos.TsKvListProto.newBuilder(); + long ts = tsKvListProto.getTs(); + if (ts == 0) { + ts = System.currentTimeMillis(); + } + tsKvListBuilder.setTs(ts); + List kvList = tsKvListProto.getKvList(); + if (!CollectionUtils.isEmpty(kvList)) { + List keyValueListProtos = validateKeyValueProtos(kvList); + tsKvListBuilder.addAllKv(keyValueListProtos); + return tsKvListBuilder.build(); + } else { + throw new IllegalArgumentException("KeyValue list is empty!"); + } + } + + + private static List validateKeyValueProtos(List kvList) { + kvList.forEach(keyValueProto -> { + String key = keyValueProto.getKey(); + if (StringUtils.isEmpty(key)) { + throw new IllegalArgumentException("Invalid key value: " + key + "!"); + } + TransportProtos.KeyValueType type = keyValueProto.getType(); + switch (type) { + case BOOLEAN_V: + case LONG_V: + case DOUBLE_V: + break; + case STRING_V: + if (StringUtils.isEmpty(keyValueProto.getStringV())) { + throw new IllegalArgumentException("Value is empty for key: " + key + "!"); + } + break; + case JSON_V: + try { + JSON_PARSER.parse(keyValueProto.getJsonV()); + } catch (Exception e) { + throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!"); + } + break; + case UNRECOGNIZED: + throw new IllegalArgumentException("Unsupported keyValueType: " + type + "!"); + } + }); + return kvList; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/DeviceProfileAware.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/DeviceProfileAware.java new file mode 100644 index 0000000000..f4e96a375b --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/DeviceProfileAware.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import org.thingsboard.server.common.data.DeviceProfile; + +public interface DeviceProfileAware { + + DeviceProfile getDeviceProfile(); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/GetOrCreateDeviceFromGatewayResponse.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/GetOrCreateDeviceFromGatewayResponse.java new file mode 100644 index 0000000000..985866c962 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/GetOrCreateDeviceFromGatewayResponse.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfile; + +@Data +@Builder +public class GetOrCreateDeviceFromGatewayResponse implements DeviceProfileAware { + + private TransportDeviceInfo deviceInfo; + private DeviceProfile deviceProfile; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java new file mode 100644 index 0000000000..39bec0890f --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; + +public class SessionInfoCreator { + + public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) { + return TransportProtos.SessionInfoProto.newBuilder() + .setNodeId(context.getNodeId()) + .setSessionIdMSB(sessionId.getMostSignificantBits()) + .setSessionIdLSB(sessionId.getLeastSignificantBits()) + .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(msg.getDeviceInfo().getDeviceId().getId().getLeastSignificantBits()) + .setTenantIdMSB(msg.getDeviceInfo().getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getDeviceInfo().getTenantId().getId().getLeastSignificantBits()) + .setDeviceName(msg.getDeviceInfo().getDeviceName()) + .setDeviceType(msg.getDeviceInfo().getDeviceType()) + .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileId().getId().getLeastSignificantBits()) + .build(); + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java new file mode 100644 index 0000000000..9aa3336f7d --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class TransportDeviceInfo { + + private TenantId tenantId; + private DeviceProfileId deviceProfileId; + private DeviceId deviceId; + private String deviceName; + private String deviceType; + private String additionalInfo; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceCredentialsResponse.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceCredentialsResponse.java new file mode 100644 index 0000000000..1ce33d7c94 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceCredentialsResponse.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfile; + +@Data +@Builder +public class ValidateDeviceCredentialsResponse implements DeviceProfileAware { + + private final TransportDeviceInfo deviceInfo; + private final DeviceProfile deviceProfile; + private final String credentials; + + public boolean hasDeviceInfo() { + return deviceInfo != null; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java new file mode 100644 index 0000000000..4d955de70c --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.service; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.transport.TransportProfileCache; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +@Component +@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'") +public class DefaultTransportProfileCache implements TransportProfileCache { + + private final ConcurrentMap deviceProfiles = new ConcurrentHashMap<>(); + + private final DataDecodingEncodingService dataDecodingEncodingService; + + public DefaultTransportProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { + this.dataDecodingEncodingService = dataDecodingEncodingService; + } + + @Override + public DeviceProfile getOrCreate(DeviceProfileId id, ByteString profileBody) { + DeviceProfile profile = deviceProfiles.get(id); + if (profile == null) { + Optional deviceProfile = dataDecodingEncodingService.decode(profileBody.toByteArray()); + if (deviceProfile.isPresent()) { + profile = deviceProfile.get(); + deviceProfiles.put(id, profile); + } + } + return profile; + } + + @Override + public DeviceProfile get(DeviceProfileId id) { + return deviceProfiles.get(id); + } + + @Override + public void put(DeviceProfile profile) { + deviceProfiles.put(profile.getId(), profile); + } + + @Override + public DeviceProfile put(ByteString profileBody) { + Optional deviceProfile = dataDecodingEncodingService.decode(profileBody.toByteArray()); + if (deviceProfile.isPresent()) { + put(deviceProfile.get()); + return deviceProfile.get(); + } else { + return null; + } + } + + @Override + public void evict(DeviceProfileId id) { + deviceProfiles.remove(id); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 438c3b9ff6..46c9bc2d01 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -15,27 +15,41 @@ */ package org.thingsboard.server.common.transport.service; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.SessionMsgListener; +import org.thingsboard.server.common.transport.TransportProfileCache; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -61,9 +75,11 @@ import org.thingsboard.server.common.stats.StatsType; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -75,6 +91,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; /** * Created by ashvayka on 17.10.18. @@ -105,6 +122,7 @@ public class DefaultTransportService implements TransportService { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final StatsFactory statsFactory; + private final TransportProfileCache transportProfileCache; protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; @@ -120,19 +138,25 @@ public class DefaultTransportService implements TransportService { private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); - //TODO: Implement cleanup of this maps. + //TODO 3.2: @ybondarenko Implement cleanup of this maps. private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); private ExecutorService mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); private volatile boolean stopped = false; - public DefaultTransportService(TbServiceInfoProvider serviceInfoProvider, TbTransportQueueFactory queueProvider, TbQueueProducerProvider producerProvider, PartitionService partitionService, StatsFactory statsFactory) { + public DefaultTransportService(TbServiceInfoProvider serviceInfoProvider, + TbTransportQueueFactory queueProvider, + TbQueueProducerProvider producerProvider, + PartitionService partitionService, + StatsFactory statsFactory, + TransportProfileCache transportProfileCache) { this.serviceInfoProvider = serviceInfoProvider; this.queueProvider = queueProvider; this.producerProvider = producerProvider; this.partitionService = partitionService; this.statsFactory = statsFactory; + this.transportProfileCache = transportProfileCache; } @PostConstruct @@ -228,27 +252,84 @@ public class DefaultTransportService implements TransportService { } @Override - public void process(TransportProtos.ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg, + TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getValidateTokenResponseMsg()), callback::onError, transportCallbackExecutor); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); } @Override - public void process(TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateBasicMqttCredRequestMsg msg, + TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getValidateTokenResponseMsg()), callback::onError, transportCallbackExecutor); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateBasicMqttCredRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); } @Override - public void process(TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getGetOrCreateDeviceResponseMsg()), callback::onError, transportCallbackExecutor); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); + } + + private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg protoMsg, + TransportServiceCallback callback) { + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateCredResponseMsg(); + ValidateDeviceCredentialsResponse.ValidateDeviceCredentialsResponseBuilder result = ValidateDeviceCredentialsResponse.builder(); + if (msg.hasDeviceInfo()) { + result.credentials(msg.getCredentialsBody()); + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (profileBody != null && !profileBody.isEmpty()) { + DeviceProfile profile = transportProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody); + if (transportType != DeviceTransportType.DEFAULT + && profile != null && profile.getTransportType() != DeviceTransportType.DEFAULT && profile.getTransportType() != transportType) { + log.debug("[{}] Device profile [{}] has different transport type: {}, expected: {}", tdi.getDeviceId(), tdi.getDeviceProfileId(), profile.getTransportType(), transportType); + throw new IllegalStateException("Device profile has different transport type: " + profile.getTransportType() + ". Expected: " + transportType); + } + result.deviceProfile(profile); + } + } + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } + + @Override + public void process(TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(requestMsg).build()); + log.trace("Processing msg: {}", requestMsg); + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg msg = tmp.getValue().getGetOrCreateDeviceResponseMsg(); + GetOrCreateDeviceFromGatewayResponse.GetOrCreateDeviceFromGatewayResponseBuilder result = GetOrCreateDeviceFromGatewayResponse.builder(); + if (msg.hasDeviceInfo()) { + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (profileBody != null && !profileBody.isEmpty()) { + result.deviceProfile(transportProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + } + } + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } + + private TransportDeviceInfo getTransportDeviceInfo(TransportProtos.DeviceInfoProto di) { + TransportDeviceInfo tdi = new TransportDeviceInfo(); + tdi.setTenantId(new TenantId(new UUID(di.getTenantIdMSB(), di.getTenantIdLSB()))); + tdi.setDeviceId(new DeviceId(new UUID(di.getDeviceIdMSB(), di.getDeviceIdLSB()))); + tdi.setDeviceProfileId(new DeviceProfileId(new UUID(di.getDeviceProfileIdMSB(), di.getDeviceProfileIdLSB()))); + tdi.setAdditionalInfo(di.getAdditionalInfo()); + tdi.setDeviceName(di.getDeviceName()); + tdi.setDeviceType(di.getDeviceType()); + return tdi; } @Override @@ -282,7 +363,9 @@ public class DefaultTransportService implements TransportService { metaData.putValue("deviceType", sessionInfo.getDeviceType()); metaData.putValue("ts", tsKv.getTs() + ""); JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, metaData, gson.toJson(json)); + RuleChainId ruleChainId = resolveRuleChainId(sessionInfo); + TbMsg tbMsg = TbMsg.newMsg(ServiceQueue.MAIN, SessionMsgType.POST_TELEMETRY_REQUEST.name(), + deviceId, metaData, gson.toJson(json), ruleChainId, null); sendToRuleEngine(tenantId, tbMsg, packCallback); } } @@ -298,7 +381,10 @@ public class DefaultTransportService implements TransportService { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("deviceName", sessionInfo.getDeviceName()); metaData.putValue("deviceType", sessionInfo.getDeviceType()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), deviceId, metaData, gson.toJson(json)); + metaData.putValue("notifyDevice", "false"); + RuleChainId ruleChainId = resolveRuleChainId(sessionInfo); + TbMsg tbMsg = TbMsg.newMsg(ServiceQueue.MAIN, SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), + deviceId, metaData, gson.toJson(json), ruleChainId, null); sendToRuleEngine(tenantId, tbMsg, new TransportTbQueueCallback(callback)); } } @@ -380,9 +466,10 @@ public class DefaultTransportService implements TransportService { metaData.putValue("requestId", Integer.toString(msg.getRequestId())); metaData.putValue("serviceId", serviceInfoProvider.getServiceId()); metaData.putValue("sessionId", sessionId.toString()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, metaData, TbMsgDataType.JSON, gson.toJson(json)); + RuleChainId ruleChainId = resolveRuleChainId(sessionInfo); + TbMsg tbMsg = TbMsg.newMsg(ServiceQueue.MAIN, SessionMsgType.TO_SERVER_RPC_REQUEST.name(), + deviceId, metaData, gson.toJson(json), ruleChainId, null); sendToRuleEngine(tenantId, tbMsg, new TransportTbQueueCallback(callback)); - String requestId = sessionId + "-" + msg.getRequestId(); toServerRpcPendingMap.put(requestId, new RpcRequestMetadata(sessionId, msg.getRequestId())); schedulerExecutor.schedule(() -> processTimeout(requestId), clientSideRpcTimeout, TimeUnit.MILLISECONDS); @@ -538,11 +625,67 @@ public class DefaultTransportService implements TransportService { deregisterSession(md.getSessionInfo()); } } else { - //TODO: should we notify the device actor about missed session? - log.debug("[{}] Missing session.", sessionId); + if (toSessionMsg.hasDeviceProfileUpdateMsg()) { + DeviceProfile deviceProfile = transportProfileCache.put(toSessionMsg.getDeviceProfileUpdateMsg().getData()); + if (deviceProfile != null) { + onProfileUpdate(deviceProfile); + } + } else if (toSessionMsg.hasDeviceProfileDeleteMsg()) { + transportProfileCache.evict(new DeviceProfileId(new UUID( + toSessionMsg.getDeviceProfileDeleteMsg().getProfileIdMSB(), + toSessionMsg.getDeviceProfileDeleteMsg().getProfileIdLSB() + ))); + } else { + //TODO: should we notify the device actor about missed session? + log.debug("[{}] Missing session.", sessionId); + } } } + @Override + public void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback callback) { + DeviceProfile deviceProfile = transportProfileCache.get(deviceProfileId); + if (deviceProfile != null) { + callback.onSuccess(deviceProfile); + } else { + log.trace("Processing device profile request: [{}]", deviceProfileId); + TransportProtos.GetDeviceProfileRequestMsg msg = TransportProtos.GetDeviceProfileRequestMsg.newBuilder() + .setProfileIdMSB(deviceProfileId.getId().getMostSignificantBits()) + .setProfileIdLSB(deviceProfileId.getId().getLeastSignificantBits()) + .build(); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setGetDeviceProfileRequestMsg(msg).build()); + AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), + response -> { + ByteString devProfileBody = response.getValue().getGetDeviceProfileResponseMsg().getData(); + if (devProfileBody != null && !devProfileBody.isEmpty()) { + DeviceProfile profile = transportProfileCache.put(devProfileBody); + if (profile != null) { + callback.onSuccess(profile); + } else { + log.warn("Failed to decode device profile: {}", devProfileBody); + callback.onError(new IllegalArgumentException("Failed to decode device profile!")); + } + } else { + log.warn("Failed to find device profile: [{}]", deviceProfileId); + callback.onError(new IllegalArgumentException("Failed to find device profile!")); + } + }, callback::onError, transportCallbackExecutor); + } + } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + long deviceProfileIdMSB = deviceProfile.getId().getId().getMostSignificantBits(); + long deviceProfileIdLSB = deviceProfile.getId().getId().getLeastSignificantBits(); + sessions.forEach((id, md) -> { + if (md.getSessionInfo().getDeviceProfileIdMSB() == deviceProfileIdMSB + && md.getSessionInfo().getDeviceProfileIdLSB() == deviceProfileIdLSB) { + transportCallbackExecutor.submit(() -> md.getListener().onProfileUpdate(deviceProfile)); + } + }); + } + protected UUID toSessionId(TransportProtos.SessionInfoProto sessionInfo) { return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); } @@ -593,6 +736,19 @@ public class DefaultTransportService implements TransportService { ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), wrappedCallback); } + private RuleChainId resolveRuleChainId(TransportProtos.SessionInfoProto sessionInfo) { + DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + DeviceProfile deviceProfile = transportProfileCache.get(deviceProfileId); + RuleChainId ruleChainId; + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", deviceProfileId); + ruleChainId = null; + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + } + return ruleChainId; + } + private class TransportTbQueueCallback implements TbQueueCallback { private final TransportServiceCallback callback; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java index c312b3a4d9..2f7f2d69e6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java @@ -17,8 +17,12 @@ package org.thingsboard.server.common.transport.session; import lombok.Data; import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.msg.session.SessionContext; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import java.util.UUID; @@ -34,17 +38,31 @@ public abstract class DeviceAwareSessionContext implements SessionContext { @Getter private volatile DeviceId deviceId; @Getter - private volatile DeviceInfoProto deviceInfo; + protected volatile TransportDeviceInfo deviceInfo; + @Getter + @Setter + protected volatile DeviceProfile deviceProfile; + @Getter + @Setter + private volatile TransportProtos.SessionInfoProto sessionInfo; + private volatile boolean connected; public DeviceId getDeviceId() { return deviceId; } - public void setDeviceInfo(DeviceInfoProto deviceInfo) { + public void setDeviceInfo(TransportDeviceInfo deviceInfo) { this.deviceInfo = deviceInfo; this.connected = true; - this.deviceId = new DeviceId(new UUID(deviceInfo.getDeviceIdMSB(), deviceInfo.getDeviceIdLSB())); + this.deviceId = deviceInfo.getDeviceId(); + } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + this.deviceProfile = deviceProfile; + this.deviceInfo.setDeviceType(deviceProfile.getName()); + this.sessionInfo = TransportProtos.SessionInfoProto.newBuilder().mergeFrom(sessionInfo).setDeviceType(deviceProfile.getName()).build(); } public boolean isConnected() { diff --git a/application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java similarity index 84% rename from application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java index 4a781b8673..1b10cb5dc3 100644 --- a/application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.encoding; +package org.thingsboard.server.common.transport.util; import org.thingsboard.server.common.msg.TbActorMsg; @@ -21,9 +21,9 @@ import java.util.Optional; public interface DataDecodingEncodingService { - Optional decode(byte[] byteArray); + Optional decode(byte[] byteArray); - byte[] encode(TbActorMsg msq); + byte[] encode(T msq); } diff --git a/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithFSTService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java similarity index 82% rename from application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithFSTService.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java index 8d89059488..eee9dacfe4 100644 --- a/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithFSTService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.encoding; +package org.thingsboard.server.common.transport.util; import lombok.extern.slf4j.Slf4j; import org.nustaq.serialization.FSTConfiguration; import org.springframework.stereotype.Service; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import java.util.Optional; @@ -29,11 +30,10 @@ public class ProtoWithFSTService implements DataDecodingEncodingService { private final FSTConfiguration config = FSTConfiguration.createDefaultConfiguration(); @Override - public Optional decode(byte[] byteArray) { + public Optional decode(byte[] byteArray) { try { - TbActorMsg msg = (TbActorMsg) config.asObject(byteArray); + T msg = (T) config.asObject(byteArray); return Optional.of(msg); - } catch (IllegalArgumentException e) { log.error("Error during deserialization message, [{}]", e.getMessage()); return Optional.empty(); @@ -41,7 +41,7 @@ public class ProtoWithFSTService implements DataDecodingEncodingService { } @Override - public byte[] encode(TbActorMsg msq) { + public byte[] encode(T msq) { return config.asByteArray(msq); } diff --git a/common/transport/transport-api/src/main/proto/transport.proto b/common/transport/transport-api/src/main/proto/transport.proto new file mode 100644 index 0000000000..b536c22198 --- /dev/null +++ b/common/transport/transport-api/src/main/proto/transport.proto @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto3"; +package transportapi; + +option java_package = "org.thingsboard.server.gen.transport"; +option java_outer_classname = "TransportApiProtos"; + +import "queue.proto"; + +message ClaimDevice { + string secretKey = 1; + int64 durationMs = 2; +} + +message AttributesRequest { + string clientKeys = 1; + string sharedKeys = 2; +} + +message RpcRequest { + string method = 1; + string params = 2; +} + +message DisconnectMsg { + string deviceName = 1; +} + +message ConnectMsg { + string deviceName = 1; + string deviceType = 2; +} + +message TelemetryMsg { + string deviceName = 1; + transport.PostTelemetryMsg msg = 3; +} + +message AttributesMsg { + string deviceName = 1; + transport.PostAttributeMsg msg = 2; +} + +message ClaimDeviceMsg { + string deviceName = 1; + ClaimDevice claimRequest = 2; +} + +message GatewayTelemetryMsg { + repeated TelemetryMsg msg = 1; +} + +message GatewayClaimMsg { + repeated ClaimDeviceMsg msg = 1; +} + +message GatewayAttributesMsg { + repeated AttributesMsg msg = 1; +} + +message GatewayRpcResponseMsg { + string deviceName = 1; + int32 id = 2; + string data = 3; +} + +message GatewayAttributeResponseMsg { + string deviceName = 1; + transport.GetAttributeResponseMsg responseMsg = 2; +} + +message GatewayAttributeUpdateNotificationMsg { + string deviceName = 1; + transport.AttributeUpdateNotificationMsg notificationMsg = 2; +} + +message GatewayDeviceRpcRequestMsg { + string deviceName = 1; + transport.ToDeviceRpcRequestMsg rpcRequestMsg = 2; +} + +message GatewayAttributesRequestMsg { + int32 id = 1; + string deviceName = 2; + bool client = 3; + repeated string keys = 4; +} diff --git a/common/util/pom.xml b/common/util/pom.xml index 25dbf9d35e..076eff152a 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index a2f973029e..ec4960eb5f 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 259ff89b0b..26290fcae9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -43,6 +43,10 @@ public abstract class DaoUtil { return new PageData(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); } + public static PageData pageToPageData(Page page) { + return new PageData(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); + } + public static Pageable toPageable(PageLink pageLink) { return toPageable(pageLink, Collections.emptyMap()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index ac247667bc..ff1763f662 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -21,18 +21,20 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -75,8 +77,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } private DeviceCredentials saveOrUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { - if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { - formatCertData(deviceCredentials); + if(deviceCredentials.getCredentialsType() == null){ + throw new DataValidationException("Device credentials type should be specified"); + } + switch (deviceCredentials.getCredentialsType()) { + case X509_CERTIFICATE: + formatCertData(deviceCredentials); + break; + case MQTT_BASIC: + formatSimpleMqttCredentials(deviceCredentials); + break; } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); @@ -93,6 +103,32 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } } + private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { + BasicMqttCredentials mqttCredentials; + try { + mqttCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); + if (mqttCredentials == null) { + throw new IllegalArgumentException(); + } + } catch (IllegalArgumentException e) { + throw new DataValidationException("Invalid credentials body for simple mqtt credentials!"); + } + if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) { + throw new DataValidationException("Both mqtt client id and user name are empty!"); + } + if (StringUtils.isEmpty(mqttCredentials.getClientId())) { + deviceCredentials.setCredentialsId(mqttCredentials.getUserName()); + } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) { + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(mqttCredentials.getClientId())); + } else { + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName())); + } + if (!StringUtils.isEmpty(mqttCredentials.getPassword())) { + mqttCredentials.setPassword(mqttCredentials.getPassword()); + } + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); + } + private void formatCertData(DeviceCredentials deviceCredentials) { String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); String sha3Hash = EncryptionUtil.getSha3Hash(cert); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index ae87564e81..bbc1735c9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -89,6 +89,16 @@ public interface DeviceDao extends Dao { */ PageData findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + /** + * Find device infos by tenantId, deviceProfileId and page link. + * + * @param tenantId the tenantId + * @param deviceProfileId the deviceProfileId + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink); + /** * Find devices by tenantId and devices Ids. * @@ -140,6 +150,16 @@ public interface DeviceDao extends Dao { */ PageData findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + /** + * Find device infos by tenantId, customerId, deviceProfileId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param deviceProfileId the deviceProfileId + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink); /** * Find devices by tenantId, customerId and devices Ids. @@ -183,4 +203,16 @@ public interface DeviceDao extends Dao { */ ListenableFuture findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id); + Long countDevicesByDeviceProfileId(TenantId tenantId, UUID deviceProfileId); + + /** + * Find devices by tenantId, profileId and page link. + * + * @param tenantId the tenantId + * @param profileId the profileId + * @param pageLink the page link + * @return the list of device objects + */ + PageData findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java new file mode 100644 index 0000000000..267aff358e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface DeviceProfileDao extends Dao { + + DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, UUID deviceProfileId); + + DeviceProfile save(TenantId tenantId, DeviceProfile deviceProfile); + + PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink); + + PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink); + + DeviceProfile findDefaultDeviceProfile(TenantId tenantId); + + DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId); + + DeviceProfile findByName(TenantId tenantId, String profileName); +} 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 new file mode 100644 index 0000000000..b40ad102eb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -0,0 +1,346 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +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.tenant.TenantDao; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.thingsboard.server.common.data.CacheConstants.DEVICE_PROFILE_CACHE; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class DeviceProfileServiceImpl extends AbstractEntityService implements DeviceProfileService { + + private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; + private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName "; + + @Autowired + private DeviceProfileDao deviceProfileDao; + + @Autowired + private DeviceDao deviceDao; + + @Autowired + private DeviceService deviceService; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private CacheManager cacheManager; + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#deviceProfileId.id}") + @Override + public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + return deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + } + + @Override + public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) { + log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName); + Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName); + return deviceProfileDao.findByName(tenantId, profileName); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'info', #deviceProfileId.id}") + @Override + public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + return deviceProfileDao.findDeviceProfileInfoById(tenantId, deviceProfileId.getId()); + } + + @Override + public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { + log.trace("Executing saveDeviceProfile [{}]", deviceProfile); + deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); + DeviceProfile oldDeviceProfile = null; + if (deviceProfile.getId() != null) { + oldDeviceProfile = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId()); + } + DeviceProfile savedDeviceProfile; + try { + savedDeviceProfile = deviceProfileDao.save(deviceProfile.getTenantId(), deviceProfile); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) { + throw new DataValidationException("Device profile with such name already exists!"); + } else { + throw t; + } + } + Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); + cache.evict(Collections.singletonList(savedDeviceProfile.getId().getId())); + cache.evict(Arrays.asList("info", savedDeviceProfile.getId().getId())); + cache.evict(Arrays.asList(deviceProfile.getTenantId().getId(), deviceProfile.getName())); + if (savedDeviceProfile.isDefault()) { + cache.evict(Arrays.asList("default", savedDeviceProfile.getTenantId().getId())); + cache.evict(Arrays.asList("default", "info", savedDeviceProfile.getTenantId().getId())); + } + if (oldDeviceProfile != null && !oldDeviceProfile.getName().equals(deviceProfile.getName())) { + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = deviceDao.findDevicesByTenantIdAndProfileId(deviceProfile.getTenantId().getId(), deviceProfile.getUuidId(), pageLink); + for (Device device : pageData.getData()) { + device.setType(deviceProfile.getName()); + deviceService.saveDevice(device); + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + } + return savedDeviceProfile; + } + + @Override + public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + if (deviceProfile != null && deviceProfile.isDefault()) { + throw new DataValidationException("Deletion of Default Device Profile is prohibited!"); + } + this.removeDeviceProfile(tenantId, deviceProfile); + } + + private void removeDeviceProfile(TenantId tenantId, DeviceProfile deviceProfile) { + DeviceProfileId deviceProfileId = deviceProfile.getId(); + try { + deviceProfileDao.removeById(tenantId, deviceProfileId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) { + throw new DataValidationException("The device profile referenced by the devices cannot be deleted!"); + } else { + throw t; + } + } + deleteEntityRelations(tenantId, deviceProfileId); + Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); + cache.evict(Collections.singletonList(deviceProfileId.getId())); + cache.evict(Arrays.asList("info", deviceProfileId.getId())); + cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName())); + } + + @Override + public PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findDeviceProfiles tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return deviceProfileDao.findDeviceProfiles(tenantId, pageLink); + } + + @Override + public PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#tenantId.id, #name}") + @Override + public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String name) { + log.trace("Executing findOrCreateDefaultDeviceProfile"); + DeviceProfile deviceProfile = findDeviceProfileByName(tenantId, name); + if (deviceProfile == null) { + deviceProfile = this.doCreateDefaultDeviceProfile(tenantId, name, name.equals("default")); + } + return deviceProfile; + } + + @Override + public DeviceProfile createDefaultDeviceProfile(TenantId tenantId) { + log.trace("Executing createDefaultDeviceProfile tenantId [{}]", tenantId); + return doCreateDefaultDeviceProfile(tenantId, "default", true); + } + + private DeviceProfile doCreateDefaultDeviceProfile(TenantId tenantId, String profileName, boolean defaultProfile) { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setDefault(defaultProfile); + deviceProfile.setName(profileName); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setDescription("Default device profile"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfile.setProfileData(deviceProfileData); + return saveDeviceProfile(deviceProfile); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', #tenantId.id}") + @Override + public DeviceProfile findDefaultDeviceProfile(TenantId tenantId) { + log.trace("Executing findDefaultDeviceProfile tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return deviceProfileDao.findDefaultDeviceProfile(tenantId); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', 'info', #tenantId.id}") + @Override + public DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId) { + log.trace("Executing findDefaultDeviceProfileInfo tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return deviceProfileDao.findDefaultDeviceProfileInfo(tenantId); + } + + @Override + public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + if (!deviceProfile.isDefault()) { + Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); + deviceProfile.setDefault(true); + DeviceProfile previousDefaultDeviceProfile = findDefaultDeviceProfile(tenantId); + boolean changed = false; + if (previousDefaultDeviceProfile == null) { + deviceProfileDao.save(tenantId, deviceProfile); + changed = true; + } else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) { + previousDefaultDeviceProfile.setDefault(false); + deviceProfileDao.save(tenantId, previousDefaultDeviceProfile); + deviceProfileDao.save(tenantId, deviceProfile); + cache.evict(Collections.singletonList(previousDefaultDeviceProfile.getId().getId())); + cache.evict(Arrays.asList("info", previousDefaultDeviceProfile.getId().getId())); + cache.evict(Arrays.asList(tenantId.getId(), previousDefaultDeviceProfile.getName())); + changed = true; + } + if (changed) { + cache.evict(Collections.singletonList(deviceProfile.getId().getId())); + cache.evict(Arrays.asList("info", deviceProfile.getId().getId())); + cache.evict(Arrays.asList("default", tenantId.getId())); + cache.evict(Arrays.asList("default", "info", tenantId.getId())); + cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName())); + } + return changed; + } + return false; + } + + @Override + public void deleteDeviceProfilesByTenantId(TenantId tenantId) { + log.trace("Executing deleteDeviceProfilesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantDeviceProfilesRemover.removeEntities(tenantId, tenantId); + } + + private DataValidator deviceProfileValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) { + if (StringUtils.isEmpty(deviceProfile.getName())) { + throw new DataValidationException("Device profile name should be specified!"); + } + if (deviceProfile.getType() == null) { + throw new DataValidationException("Device profile type should be specified!"); + } + if (deviceProfile.getTransportType() == null) { + throw new DataValidationException("Device profile transport type should be specified!"); + } + if (deviceProfile.getTenantId() == null) { + throw new DataValidationException("Device profile should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(deviceProfile.getTenantId(), deviceProfile.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Device profile is referencing to non-existent tenant!"); + } + } + if (deviceProfile.isDefault()) { + DeviceProfile defaultDeviceProfile = findDefaultDeviceProfile(tenantId); + if (defaultDeviceProfile != null && !defaultDeviceProfile.getId().equals(deviceProfile.getId())) { + throw new DataValidationException("Another default device profile is present in scope of current tenant!"); + } + } + } + + @Override + protected void validateUpdate(TenantId tenantId, DeviceProfile deviceProfile) { + DeviceProfile old = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing device profile!"); + } + boolean profileTypeChanged = !old.getType().equals(deviceProfile.getType()); + boolean transportTypeChanged = !old.getTransportType().equals(deviceProfile.getTransportType()); + if (profileTypeChanged || transportTypeChanged) { + Long profileDeviceCount = deviceDao.countDevicesByDeviceProfileId(deviceProfile.getTenantId(), deviceProfile.getId().getId()); + if (profileDeviceCount > 0) { + String message = null; + if (profileTypeChanged) { + message = "Can't change device profile type because devices referenced it!"; + } else if (transportTypeChanged) { + message = "Can't change device profile transport type because devices referenced it!"; + } + throw new DataValidationException(message); + } + } + } + }; + + private PaginatedRemover tenantDeviceProfilesRemover = + new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return deviceProfileDao.findDeviceProfiles(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, DeviceProfile entity) { + removeDeviceProfile(tenantId, entity); + } + }; + +} 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 219656badd..ad66ed47fd 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 @@ -34,13 +34,20 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -80,6 +87,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; public class DeviceServiceImpl extends AbstractEntityService implements DeviceService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; @@ -95,6 +103,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceCredentialsService deviceCredentialsService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired private EntityViewService entityViewService; @@ -159,6 +170,23 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe deviceValidator.validate(device, Device::getTenantId); Device savedDevice; try { + DeviceProfile deviceProfile; + if (device.getDeviceProfileId() == null) { + if (!StringUtils.isEmpty(device.getType())) { + deviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(device.getTenantId(), device.getType()); + } else { + deviceProfile = this.deviceProfileService.findDefaultDeviceProfile(device.getTenantId()); + } + device.setDeviceProfileId(new DeviceProfileId(deviceProfile.getId().getId())); + } else { + deviceProfile = this.deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile == null) { + throw new DataValidationException("Device is referencing non existing device profile!"); + } + } + device.setType(deviceProfile.getName()); + device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); + savedDevice = deviceDao.save(device.getTenantId(), device); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); @@ -178,6 +206,33 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return savedDevice; } + private DeviceData syncDeviceData(DeviceProfile deviceProfile, DeviceData deviceData) { + if (deviceData == null) { + deviceData = new DeviceData(); + } + if (deviceData.getConfiguration() == null || !deviceProfile.getType().equals(deviceData.getConfiguration().getType())) { + switch (deviceProfile.getType()) { + case DEFAULT: + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + break; + } + } + if (deviceData.getTransportConfiguration() == null || !deviceProfile.getTransportType().equals(deviceData.getTransportConfiguration().getType())) { + switch (deviceProfile.getTransportType()) { + case DEFAULT: + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + break; + case MQTT: + deviceData.setTransportConfiguration(new MqttDeviceTransportConfiguration()); + break; + case LWM2M: + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + break; + } + } + return deviceData; + } + @Override public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId) { Device device = findDeviceById(tenantId, deviceId); @@ -257,6 +312,15 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink); } + @Override + public PageData findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndDeviceProfileId, tenantId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, deviceProfileId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId.getId(), deviceProfileId.getId(), pageLink); + } + @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds) { log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds); @@ -311,6 +375,16 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } + @Override + public PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId, tenantId [{}], customerId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, customerId, deviceProfileId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId.getId(), customerId.getId(), deviceProfileId.getId(), pageLink); + } + @Override public ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds) { log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds); @@ -401,13 +475,14 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Override protected void validateUpdate(TenantId tenantId, Device device) { + Device old = deviceDao.findById(device.getTenantId(), device.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing device!"); + } } @Override protected void validateDataImpl(TenantId tenantId, Device device) { - if (StringUtils.isEmpty(device.getType())) { - throw new DataValidationException("Device type should be specified!"); - } if (StringUtils.isEmpty(device.getName())) { throw new DataValidationException("Device name should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index ef5fcf0fa2..e4bab842cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.event; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -35,6 +37,8 @@ import java.util.Optional; @Slf4j public class BaseEventService implements EventService { + private static final int MAX_DEBUG_EVENT_SYMBOLS = 4 * 1024; + @Autowired public EventDao eventDao; @@ -47,6 +51,7 @@ public class BaseEventService implements EventService { @Override public ListenableFuture saveAsync(Event event) { eventValidator.validate(event, Event::getTenantId); + checkAndTruncateDebugEvent(event); return eventDao.saveAsync(event); } @@ -56,9 +61,21 @@ public class BaseEventService implements EventService { if (StringUtils.isEmpty(event.getUid())) { throw new DataValidationException("Event uid should be specified!."); } + checkAndTruncateDebugEvent(event); return eventDao.saveIfNotExists(event); } + private void checkAndTruncateDebugEvent(Event event) { + if (event.getType().startsWith("DEBUG") && event.getBody() != null && event.getBody().has("data")) { + String dataStr = event.getBody().get("data").asText(); + int length = dataStr.length(); + if (length > MAX_DEBUG_EVENT_SYMBOLS) { + ((ObjectNode) event.getBody()).put("data", dataStr.substring(0, MAX_DEBUG_EVENT_SYMBOLS) + "...[truncated " + (length - MAX_DEBUG_EVENT_SYMBOLS) + " symbols]"); + log.trace("[{}] Event was truncated: {}", event.getId(), dataStr); + } + } + } + @Override public Optional findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) { if (tenantId == null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 08ae361d50..fb8a0194b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -114,11 +114,21 @@ public class ModelConstants { public static final String TENANT_TITLE_PROPERTY = TITLE_PROPERTY; public static final String TENANT_REGION_PROPERTY = "region"; public static final String TENANT_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; - public static final String TENANT_ISOLATED_TB_CORE = "isolated_tb_core"; - public static final String TENANT_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; + public static final String TENANT_TENANT_PROFILE_ID_PROPERTY = "tenant_profile_id"; public static final String TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "tenant_by_region_and_search_text"; + /** + * Tenant profile constants. + */ + public static final String TENANT_PROFILE_COLUMN_FAMILY_NAME = "tenant_profile"; + public static final String TENANT_PROFILE_NAME_PROPERTY = "name"; + public static final String TENANT_PROFILE_PROFILE_DATA_PROPERTY = "profile_data"; + public static final String TENANT_PROFILE_DESCRIPTION_PROPERTY = "description"; + public static final String TENANT_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; + public static final String TENANT_PROFILE_ISOLATED_TB_CORE = "isolated_tb_core"; + public static final String TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; + /** * Cassandra customer constants. */ @@ -141,6 +151,9 @@ public class ModelConstants { public static final String DEVICE_TYPE_PROPERTY = "type"; public static final String DEVICE_LABEL_PROPERTY = "label"; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String DEVICE_DEVICE_PROFILE_ID_PROPERTY = "device_profile_id"; + public static final String DEVICE_DEVICE_DATA_PROPERTY = "device_data"; + public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text"; public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text"; @@ -148,6 +161,19 @@ public class ModelConstants { public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant"; + /** + * Device profile constants. + */ + public static final String DEVICE_PROFILE_COLUMN_FAMILY_NAME = "device_profile"; + public static final String DEVICE_PROFILE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String DEVICE_PROFILE_NAME_PROPERTY = "name"; + public static final String DEVICE_PROFILE_TYPE_PROPERTY = "type"; + public static final String DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY = "transport_type"; + public static final String DEVICE_PROFILE_PROFILE_DATA_PROPERTY = "profile_data"; + public static final String DEVICE_PROFILE_DESCRIPTION_PROPERTY = "description"; + public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; + public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; + /** * Cassandra entityView constants. */ @@ -354,6 +380,15 @@ public class ModelConstants { public static final String RULE_NODE_NAME_PROPERTY = "name"; public static final String RULE_NODE_CONFIGURATION_PROPERTY = "configuration"; + /** + * Rule node state constants. + */ + public static final String RULE_NODE_STATE_TABLE_NAME = "rule_node_state"; + public static final String RULE_NODE_STATE_NODE_ID_PROPERTY = "rule_node_id"; + public static final String RULE_NODE_STATE_ENTITY_TYPE_PROPERTY = "entity_type"; + public static final String RULE_NODE_STATE_ENTITY_ID_PROPERTY = "entity_id"; + public static final String RULE_NODE_STATE_DATA_PROPERTY = "state_data"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java index 7dc7414e87..4f8c0f9e45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java @@ -16,17 +16,23 @@ package org.thingsboard.server.dao.model.sql; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -35,7 +41,10 @@ import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) -@TypeDef(name = "json", typeClass = JsonStringType.class) +@TypeDefs({ + @TypeDef(name = "json", typeClass = JsonStringType.class), + @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +}) @MappedSuperclass public abstract class AbstractDeviceEntity extends BaseSqlEntity implements SearchTextEntity { @@ -61,6 +70,13 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; + @Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_ID_PROPERTY, columnDefinition = "uuid") + private UUID deviceProfileId; + + @Type(type = "jsonb") + @Column(name = ModelConstants.DEVICE_DEVICE_DATA_PROPERTY, columnDefinition = "jsonb") + private JsonNode deviceData; + public AbstractDeviceEntity() { super(); } @@ -76,6 +92,10 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti if (device.getCustomerId() != null) { this.customerId = device.getCustomerId().getId(); } + if (device.getDeviceProfileId() != null) { + this.deviceProfileId = device.getDeviceProfileId().getId(); + } + this.deviceData = JacksonUtil.convertValue(device.getDeviceData(), ObjectNode.class); this.name = device.getName(); this.type = device.getType(); this.label = device.getLabel(); @@ -87,6 +107,8 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti this.setCreatedTime(deviceEntity.getCreatedTime()); this.tenantId = deviceEntity.getTenantId(); this.customerId = deviceEntity.getCustomerId(); + this.deviceProfileId = deviceEntity.getDeviceProfileId(); + this.deviceData = deviceEntity.getDeviceData(); this.type = deviceEntity.getType(); this.name = deviceEntity.getName(); this.label = deviceEntity.getLabel(); @@ -113,6 +135,10 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti if (customerId != null) { device.setCustomerId(new CustomerId(customerId)); } + if (deviceProfileId != null) { + device.setDeviceProfileId(new DeviceProfileId(deviceProfileId)); + } + device.setDeviceData(JacksonUtil.convertValue(deviceData, DeviceData.class)); device.setName(name); device.setType(type); device.setLabel(label); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java new file mode 100644 index 0000000000..7cd5c6778e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java @@ -0,0 +1,161 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractTenantEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.TENANT_TITLE_PROPERTY) + private String title; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.TENANT_REGION_PROPERTY) + private String region; + + @Column(name = ModelConstants.COUNTRY_PROPERTY) + private String country; + + @Column(name = ModelConstants.STATE_PROPERTY) + private String state; + + @Column(name = ModelConstants.CITY_PROPERTY) + private String city; + + @Column(name = ModelConstants.ADDRESS_PROPERTY) + private String address; + + @Column(name = ModelConstants.ADDRESS2_PROPERTY) + private String address2; + + @Column(name = ModelConstants.ZIP_PROPERTY) + private String zip; + + @Column(name = ModelConstants.PHONE_PROPERTY) + private String phone; + + @Column(name = ModelConstants.EMAIL_PROPERTY) + private String email; + + @Type(type = "json") + @Column(name = ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + @Column(name = ModelConstants.TENANT_TENANT_PROFILE_ID_PROPERTY, columnDefinition = "uuid") + private UUID tenantProfileId; + + public AbstractTenantEntity() { + super(); + } + + public AbstractTenantEntity(Tenant tenant) { + if (tenant.getId() != null) { + this.setUuid(tenant.getId().getId()); + } + this.setCreatedTime(tenant.getCreatedTime()); + this.title = tenant.getTitle(); + this.region = tenant.getRegion(); + this.country = tenant.getCountry(); + this.state = tenant.getState(); + this.city = tenant.getCity(); + this.address = tenant.getAddress(); + this.address2 = tenant.getAddress2(); + this.zip = tenant.getZip(); + this.phone = tenant.getPhone(); + this.email = tenant.getEmail(); + this.additionalInfo = tenant.getAdditionalInfo(); + if (tenant.getTenantProfileId() != null) { + this.tenantProfileId = tenant.getTenantProfileId().getId(); + } + } + + public AbstractTenantEntity(TenantEntity tenantEntity) { + this.setId(tenantEntity.getId()); + this.setCreatedTime(tenantEntity.getCreatedTime()); + this.title = tenantEntity.getTitle(); + this.region = tenantEntity.getRegion(); + this.country = tenantEntity.getCountry(); + this.state = tenantEntity.getState(); + this.city = tenantEntity.getCity(); + this.address = tenantEntity.getAddress(); + this.address2 = tenantEntity.getAddress2(); + this.zip = tenantEntity.getZip(); + this.phone = tenantEntity.getPhone(); + this.email = tenantEntity.getEmail(); + this.additionalInfo = tenantEntity.getAdditionalInfo(); + this.tenantProfileId = tenantEntity.getTenantProfileId(); + } + + @Override + public String getSearchTextSource() { + return title; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + protected Tenant toTenant() { + Tenant tenant = new Tenant(new TenantId(this.getUuid())); + tenant.setCreatedTime(createdTime); + tenant.setTitle(title); + tenant.setRegion(region); + tenant.setCountry(country); + tenant.setState(state); + tenant.setCity(city); + tenant.setAddress(address); + tenant.setAddress2(address2); + tenant.setZip(zip); + tenant.setPhone(phone); + tenant.setEmail(email); + tenant.setAdditionalInfo(additionalInfo); + if (tenantProfileId != null) { + tenant.setTenantProfileId(new TenantProfileId(tenantProfileId)); + } + return tenant; + } + + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java index 0c91225b74..15c2c00b36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java @@ -18,8 +18,10 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Entity; @@ -28,7 +30,10 @@ import javax.persistence.Table; @Data @EqualsAndHashCode(callSuper = true) @Entity -@TypeDef(name = "json", typeClass = JsonStringType.class) +@TypeDefs({ + @TypeDef(name = "json", typeClass = JsonStringType.class), + @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +}) @Table(name = ModelConstants.DEVICE_COLUMN_FAMILY_NAME) public final class DeviceEntity extends AbstractDeviceEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java index 83914a6a9b..b3c42945fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java @@ -30,10 +30,12 @@ public class DeviceInfoEntity extends AbstractDeviceEntity { public static final Map deviceInfoColumnMap = new HashMap<>(); static { deviceInfoColumnMap.put("customerTitle", "c.title"); + deviceInfoColumnMap.put("deviceProfileName", "p.name"); } private String customerTitle; private boolean customerIsPublic; + private String deviceProfileName; public DeviceInfoEntity() { super(); @@ -41,7 +43,8 @@ public class DeviceInfoEntity extends AbstractDeviceEntity { public DeviceInfoEntity(DeviceEntity deviceEntity, String customerTitle, - Object customerAdditionalInfo) { + Object customerAdditionalInfo, + String deviceProfileName) { super(deviceEntity); this.customerTitle = customerTitle; if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { @@ -49,10 +52,11 @@ public class DeviceInfoEntity extends AbstractDeviceEntity { } else { this.customerIsPublic = false; } + this.deviceProfileName = deviceProfileName; } @Override public DeviceInfo toData() { - return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic); + return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic, deviceProfileName); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java new file mode 100644 index 0000000000..27e77d4eca --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -0,0 +1,136 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +@Table(name = ModelConstants.DEVICE_PROFILE_COLUMN_FAMILY_NAME) +public final class DeviceProfileEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.DEVICE_PROFILE_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.DEVICE_PROFILE_NAME_PROPERTY) + private String name; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEVICE_PROFILE_TYPE_PROPERTY) + private DeviceProfileType type; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY) + private DeviceTransportType transportType; + + @Column(name = ModelConstants.DEVICE_PROFILE_DESCRIPTION_PROPERTY) + private String description; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.DEVICE_PROFILE_IS_DEFAULT_PROPERTY) + private boolean isDefault; + + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID defaultRuleChainId; + + @Type(type = "jsonb") + @Column(name = ModelConstants.DEVICE_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") + private JsonNode profileData; + + public DeviceProfileEntity() { + super(); + } + + public DeviceProfileEntity(DeviceProfile deviceProfile) { + if (deviceProfile.getId() != null) { + this.setUuid(deviceProfile.getId().getId()); + } + if (deviceProfile.getTenantId() != null) { + this.tenantId = deviceProfile.getTenantId().getId(); + } + this.setCreatedTime(deviceProfile.getCreatedTime()); + this.name = deviceProfile.getName(); + this.type = deviceProfile.getType(); + this.transportType = deviceProfile.getTransportType(); + this.description = deviceProfile.getDescription(); + this.isDefault = deviceProfile.isDefault(); + this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class); + if (deviceProfile.getDefaultRuleChainId() != null) { + this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId().getId(); + } + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + @Override + public DeviceProfile toData() { + DeviceProfile deviceProfile = new DeviceProfile(new DeviceProfileId(this.getUuid())); + deviceProfile.setCreatedTime(createdTime); + if (tenantId != null) { + deviceProfile.setTenantId(new TenantId(tenantId)); + } + deviceProfile.setName(name); + deviceProfile.setType(type); + deviceProfile.setTransportType(transportType); + deviceProfile.setDescription(description); + deviceProfile.setDefault(isDefault); + deviceProfile.setProfileData(JacksonUtil.convertValue(profileData, DeviceProfileData.class)); + if (defaultRuleChainId != null) { + deviceProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId)); + } + return deviceProfile; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java new file mode 100644 index 0000000000..a416034fab --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.RuleNodeStateId; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.RULE_NODE_STATE_TABLE_NAME) +public class RuleNodeStateEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.RULE_NODE_STATE_NODE_ID_PROPERTY) + private UUID ruleNodeId; + + @Column(name = ModelConstants.RULE_NODE_STATE_ENTITY_TYPE_PROPERTY) + private String entityType; + + @Column(name = ModelConstants.RULE_NODE_STATE_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Column(name = ModelConstants.RULE_NODE_STATE_DATA_PROPERTY) + private String stateData; + + public RuleNodeStateEntity() { + } + + public RuleNodeStateEntity(RuleNodeState ruleNodeState) { + if (ruleNodeState.getId() != null) { + this.setUuid(ruleNodeState.getUuidId()); + } + this.setCreatedTime(ruleNodeState.getCreatedTime()); + this.ruleNodeId = DaoUtil.getId(ruleNodeState.getRuleNodeId()); + this.entityId = ruleNodeState.getEntityId().getId(); + this.entityType = ruleNodeState.getEntityId().getEntityType().name(); + this.stateData = ruleNodeState.getStateData(); + } + + @Override + public RuleNodeState toData() { + RuleNodeState ruleNode = new RuleNodeState(new RuleNodeStateId(this.getUuid())); + ruleNode.setCreatedTime(createdTime); + ruleNode.setRuleNodeId(new RuleNodeId(ruleNodeId)); + ruleNode.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); + ruleNode.setStateData(stateData); + return ruleNode; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java index 7cb48081bb..2d78763f71 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java @@ -15,19 +15,13 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; -import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; @@ -36,108 +30,18 @@ import javax.persistence.Table; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.TENANT_COLUMN_FAMILY_NAME) -public final class TenantEntity extends BaseSqlEntity implements SearchTextEntity { - - @Column(name = ModelConstants.TENANT_TITLE_PROPERTY) - private String title; - - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = ModelConstants.TENANT_REGION_PROPERTY) - private String region; - - @Column(name = ModelConstants.COUNTRY_PROPERTY) - private String country; - - @Column(name = ModelConstants.STATE_PROPERTY) - private String state; - - @Column(name = ModelConstants.CITY_PROPERTY) - private String city; - - @Column(name = ModelConstants.ADDRESS_PROPERTY) - private String address; - - @Column(name = ModelConstants.ADDRESS2_PROPERTY) - private String address2; - - @Column(name = ModelConstants.ZIP_PROPERTY) - private String zip; - - @Column(name = ModelConstants.PHONE_PROPERTY) - private String phone; - - @Column(name = ModelConstants.EMAIL_PROPERTY) - private String email; - - @Column(name = ModelConstants.TENANT_ISOLATED_TB_CORE) - private boolean isolatedTbCore; - - @Column(name = ModelConstants.TENANT_ISOLATED_TB_RULE_ENGINE) - private boolean isolatedTbRuleEngine; - - @Type(type = "json") - @Column(name = ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; +public final class TenantEntity extends AbstractTenantEntity { public TenantEntity() { super(); } public TenantEntity(Tenant tenant) { - if (tenant.getId() != null) { - this.setUuid(tenant.getId().getId()); - } - this.setCreatedTime(tenant.getCreatedTime()); - this.title = tenant.getTitle(); - this.region = tenant.getRegion(); - this.country = tenant.getCountry(); - this.state = tenant.getState(); - this.city = tenant.getCity(); - this.address = tenant.getAddress(); - this.address2 = tenant.getAddress2(); - this.zip = tenant.getZip(); - this.phone = tenant.getPhone(); - this.email = tenant.getEmail(); - this.additionalInfo = tenant.getAdditionalInfo(); - this.isolatedTbCore = tenant.isIsolatedTbCore(); - this.isolatedTbRuleEngine = tenant.isIsolatedTbRuleEngine(); - } - - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; + super(tenant); } @Override public Tenant toData() { - Tenant tenant = new Tenant(new TenantId(this.getUuid())); - tenant.setCreatedTime(createdTime); - tenant.setTitle(title); - tenant.setRegion(region); - tenant.setCountry(country); - tenant.setState(state); - tenant.setCity(city); - tenant.setAddress(address); - tenant.setAddress2(address2); - tenant.setZip(zip); - tenant.setPhone(phone); - tenant.setEmail(email); - tenant.setAdditionalInfo(additionalInfo); - tenant.setIsolatedTbCore(isolatedTbCore); - tenant.setIsolatedTbRuleEngine(isolatedTbRuleEngine); - return tenant; + return super.toTenant(); } - - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantInfoEntity.java new file mode 100644 index 0000000000..02e7ce9ebc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantInfoEntity.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.TenantInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantInfoEntity extends AbstractTenantEntity { + + public static final Map tenantInfoColumnMap = new HashMap<>(); + static { + tenantInfoColumnMap.put("tenantProfileName", "p.name"); + } + + private String tenantProfileName; + + public TenantInfoEntity() { + super(); + } + + public TenantInfoEntity(TenantEntity tenantEntity, String tenantProfileName) { + super(tenantEntity); + this.tenantProfileName = tenantProfileName; + } + + @Override + public TenantInfo toData() { + return new TenantInfo(super.toTenant(), this.tenantProfileName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java new file mode 100644 index 0000000000..7c69f58935 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.TenantProfileData; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +@Table(name = ModelConstants.TENANT_PROFILE_COLUMN_FAMILY_NAME) +public final class TenantProfileEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.TENANT_PROFILE_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.TENANT_PROFILE_DESCRIPTION_PROPERTY) + private String description; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.TENANT_PROFILE_IS_DEFAULT_PROPERTY) + private boolean isDefault; + + @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_CORE) + private boolean isolatedTbCore; + + @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE) + private boolean isolatedTbRuleEngine; + + @Type(type = "jsonb") + @Column(name = ModelConstants.TENANT_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") + private JsonNode profileData; + + public TenantProfileEntity() { + super(); + } + + public TenantProfileEntity(TenantProfile tenantProfile) { + if (tenantProfile.getId() != null) { + this.setUuid(tenantProfile.getId().getId()); + } + this.setCreatedTime(tenantProfile.getCreatedTime()); + this.name = tenantProfile.getName(); + this.description = tenantProfile.getDescription(); + this.isDefault = tenantProfile.isDefault(); + this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); + this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); + this.profileData = JacksonUtil.convertValue(tenantProfile.getProfileData(), ObjectNode.class); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + @Override + public TenantProfile toData() { + TenantProfile tenantProfile = new TenantProfile(new TenantProfileId(this.getUuid())); + tenantProfile.setCreatedTime(createdTime); + tenantProfile.setName(name); + tenantProfile.setDescription(description); + tenantProfile.setDefault(isDefault); + tenantProfile.setIsolatedTbCore(isolatedTbCore); + tenantProfile.setIsolatedTbRuleEngine(isolatedTbRuleEngine); + tenantProfile.setProfileData(JacksonUtil.convertValue(profileData, TenantProfileData.class)); + return tenantProfile; + } + +} 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 f3ac74596f..9c5fb32da9 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 @@ -15,11 +15,16 @@ */ package org.thingsboard.server.dao.rule; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; @@ -29,11 +34,14 @@ import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -45,9 +53,14 @@ import org.thingsboard.server.dao.tenant.TenantDao; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.DataConstants.TENANT; /** * Created by igor on 3/12/18. @@ -56,6 +69,7 @@ import java.util.concurrent.ExecutionException; @Slf4j public class BaseRuleChainService extends AbstractEntityService implements RuleChainService { + private static final int DEFAULT_PAGE_SIZE = 1000; @Autowired private RuleChainDao ruleChainDao; @@ -357,13 +371,157 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC tenantRuleChainsRemover.removeEntities(tenantId, tenantId); } + @Override + public RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) { + Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request."); + Validator.validatePageLink(pageLink); + PageData ruleChainData = ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink); + List ruleChains = ruleChainData.getData(); + List metadata = ruleChains.stream().map(rc -> loadRuleChainMetaData(tenantId, rc.getId())).collect(Collectors.toList()); + RuleChainData rcData = new RuleChainData(); + rcData.setRuleChains(ruleChains); + rcData.setMetadata(metadata); + setRandomRuleChainIds(rcData); + resetRuleNodeIds(metadata); + return rcData; + } + + @Override + public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) { + List importResults = new ArrayList<>(); + setRandomRuleChainIds(ruleChainData); + resetRuleNodeIds(ruleChainData.getMetadata()); + resetRuleChainMetadataTenantIds(tenantId, ruleChainData.getMetadata()); + if (overwrite) { + List persistentRuleChains = findAllTenantRuleChains(tenantId); + for (RuleChain ruleChain : ruleChainData.getRuleChains()) { + ComponentLifecycleEvent lifecycleEvent; + Optional persistentRuleChainOpt = persistentRuleChains.stream().filter(rc -> rc.getName().equals(ruleChain.getName())).findFirst(); + if (persistentRuleChainOpt.isPresent()) { + setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), ruleChain.getId(), persistentRuleChainOpt.get().getId()); + ruleChain.setRoot(persistentRuleChainOpt.get().isRoot()); + lifecycleEvent = ComponentLifecycleEvent.UPDATED; + } else { + ruleChain.setRoot(false); + lifecycleEvent = ComponentLifecycleEvent.CREATED; + } + ruleChain.setTenantId(tenantId); + ruleChainDao.save(tenantId, ruleChain); + importResults.add(new RuleChainImportResult(tenantId, ruleChain.getId(), lifecycleEvent)); + } + } else { + if (!CollectionUtils.isEmpty(ruleChainData.getRuleChains())) { + ruleChainData.getRuleChains().forEach(rc -> { + rc.setTenantId(tenantId); + rc.setRoot(false); + RuleChain savedRc = ruleChainDao.save(tenantId, rc); + importResults.add(new RuleChainImportResult(tenantId, savedRc.getId(), ComponentLifecycleEvent.CREATED)); + }); + } + } + if (!CollectionUtils.isEmpty(ruleChainData.getMetadata())) { + ruleChainData.getMetadata().forEach(md -> saveRuleChainMetaData(tenantId, md)); + } + return importResults; + } + + private void resetRuleChainMetadataTenantIds(TenantId tenantId, List metaData) { + for (RuleChainMetaData md : metaData) { + for (RuleNode node : md.getNodes()) { + JsonNode nodeConfiguration = node.getConfiguration(); + searchTenantIdRecursive(tenantId, nodeConfiguration); + } + } + } + + private void searchTenantIdRecursive(TenantId tenantId, JsonNode node) { + Iterator iter = node.fieldNames(); + boolean isTenantId = false; + while (iter.hasNext()) { + String field = iter.next(); + if ("entityType".equals(field) && TENANT.equals(node.get(field).asText())) { + isTenantId = true; + break; + } + } + if (isTenantId) { + ObjectNode objNode = (ObjectNode) node; + objNode.put("id", tenantId.getId().toString()); + } else { + Iterator childIter = node.iterator(); + while (childIter.hasNext()) { + searchTenantIdRecursive(tenantId, childIter.next()); + } + } + } + + private void setRandomRuleChainIds(RuleChainData ruleChainData) { + for (RuleChain ruleChain : ruleChainData.getRuleChains()) { + RuleChainId oldRuleChainId = ruleChain.getId(); + RuleChainId newRuleChainId = new RuleChainId(Uuids.timeBased()); + setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), oldRuleChainId, newRuleChainId); + ruleChain.setTenantId(null); + } + } + + private void resetRuleNodeIds(List metaData) { + for (RuleChainMetaData md : metaData) { + for (RuleNode node : md.getNodes()) { + node.setId(null); + node.setRuleChainId(null); + } + } + } + + private List findAllTenantRuleChains(TenantId tenantId) { + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + return findAllTenantRuleChainsRecursive(tenantId, new ArrayList<>(), pageLink); + } + + private List findAllTenantRuleChainsRecursive(TenantId tenantId, List accumulator, PageLink pageLink) { + PageData persistentRuleChainData = findTenantRuleChains(tenantId, pageLink); + List ruleChains = persistentRuleChainData.getData(); + if (!CollectionUtils.isEmpty(ruleChains)) { + accumulator.addAll(ruleChains); + } + if (persistentRuleChainData.hasNext()) { + return findAllTenantRuleChainsRecursive(tenantId, accumulator, pageLink.nextPageLink()); + } + return accumulator; + } + + private void setNewRuleChainId(RuleChain ruleChain, List metadata, RuleChainId oldRuleChainId, RuleChainId newRuleChainId) { + ruleChain.setId(newRuleChainId); + for (RuleChainMetaData metaData : metadata) { + if (metaData.getRuleChainId().equals(oldRuleChainId)) { + metaData.setRuleChainId(newRuleChainId); + } + if (!CollectionUtils.isEmpty(metaData.getRuleChainConnections())) { + for (RuleChainConnectionInfo rcConnInfo : metaData.getRuleChainConnections()) { + if (rcConnInfo.getTargetRuleChainId().equals(oldRuleChainId)) { + rcConnInfo.setTargetRuleChainId(newRuleChainId); + } + } + } + } + } + private void checkRuleNodesAndDelete(TenantId tenantId, RuleChainId ruleChainId) { + try{ + ruleChainDao.removeById(tenantId, ruleChainId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_device_profile")) { + throw new DataValidationException("The rule chain referenced by the device profiles cannot be deleted!"); + } else { + throw t; + } + } List nodeRelations = getRuleChainToNodeRelations(tenantId, ruleChainId); for (EntityRelation relation : nodeRelations) { deleteRuleNode(tenantId, relation.getTo()); } deleteEntityRelations(tenantId, ruleChainId); - ruleChainDao.removeById(tenantId, ruleChainId.getId()); } private List getRuleChainToNodeRelations(TenantId tenantId, RuleChainId ruleChainId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java new file mode 100644 index 0000000000..a0b83f0333 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; + +@Service +@Slf4j +public class BaseRuleNodeStateService extends AbstractEntityService implements RuleNodeStateService { + + @Autowired + private RuleNodeStateDao ruleNodeStateDao; + + @Override + public PageData findByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId, PageLink pageLink) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (ruleNodeId == null) { + throw new DataValidationException("RuleNode id should be specified!."); + } + return ruleNodeStateDao.findByRuleNodeId(ruleNodeId.getId(), pageLink); + } + + @Override + public RuleNodeState findByRuleNodeIdAndEntityId(TenantId tenantId, RuleNodeId ruleNodeId, EntityId entityId) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (ruleNodeId == null) { + throw new DataValidationException("RuleNode id should be specified!."); + } + if (entityId == null) { + throw new DataValidationException("Entity id should be specified!."); + } + return ruleNodeStateDao.findByRuleNodeIdAndEntityId(ruleNodeId.getId(), entityId.getId()); + } + + @Override + public RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + return saveOrUpdate(tenantId, ruleNodeState, false); + } + + public RuleNodeState saveOrUpdate(TenantId tenantId, RuleNodeState ruleNodeState, boolean update) { + try { + if (update) { + RuleNodeState old = ruleNodeStateDao.findByRuleNodeIdAndEntityId(ruleNodeState.getRuleNodeId().getId(), ruleNodeState.getEntityId().getId()); + if (old != null && !old.getId().equals(ruleNodeState.getId())) { + ruleNodeState.setId(old.getId()); + ruleNodeState.setCreatedTime(old.getCreatedTime()); + } + } + return ruleNodeStateDao.save(tenantId, ruleNodeState); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("rule_node_state_unq_key")) { + if (!update) { + return saveOrUpdate(tenantId, ruleNodeState, true); + } else { + throw new DataValidationException("Rule node state for such rule node id and entity id already exists!"); + } + } else { + throw t; + } + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java new file mode 100644 index 0000000000..b12c448aa5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +/** + * Created by igor on 3/12/18. + */ +public interface RuleNodeStateDao extends Dao { + + PageData findByRuleNodeId(UUID ruleNodeId, PageLink pageLink); + + RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index 22fda66759..6a3a66a320 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -22,6 +22,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.stats.MessagesStats; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; @@ -30,6 +31,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j public class TbSqlBlockingQueue implements TbSqlQueue { @@ -46,7 +48,7 @@ public class TbSqlBlockingQueue implements TbSqlQueue { } @Override - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, int index) { + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int index) { executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase())); executor.submit(() -> { String logName = params.getLogName(); @@ -65,7 +67,11 @@ public class TbSqlBlockingQueue implements TbSqlQueue { queue.drainTo(entities, batchSize - 1); boolean fullPack = entities.size() == batchSize; log.debug("[{}] Going to save {} entities", logName, entities.size()); - saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList())); + Stream entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity); + saveFunction.accept( + (params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream) + .collect(Collectors.toList()) + ); entities.forEach(v -> v.getFuture().set(null)); stats.incrementSuccessful(entities.size()); if (!fullPack) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java index a63461787e..6100405711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java @@ -31,4 +31,5 @@ public class TbSqlBlockingQueueParams { private final long maxDelay; private final long statsPrintIntervalMs; private final String statsNamePrefix; + private final boolean batchSortEnabled; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java index d9596a6efd..884d4c7a3d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; +import java.util.Comparator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -36,12 +37,20 @@ public class TbSqlBlockingQueueWrapper { private final int maxThreads; private final StatsFactory statsFactory; - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction) { + /** + * Starts TbSqlBlockingQueues. + * + * @param logExecutor executor that will be printing logs and statistics + * @param saveFunction function to save entities in database + * @param batchUpdateComparator comparator to sort entities by primary key to avoid deadlocks in cluster mode + * NOTE: you must use all of primary key parts in your comparator + */ + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator) { for (int i = 0; i < maxThreads; i++) { MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i); TbSqlBlockingQueue queue = new TbSqlBlockingQueue<>(params, stats); queues.add(queue); - queue.init(logExecutor, saveFunction, i); + queue.init(logExecutor, saveFunction, batchUpdateComparator, i); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java index c3955a811c..135e636ea5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java @@ -17,12 +17,13 @@ package org.thingsboard.server.dao.sql; import com.google.common.util.concurrent.ListenableFuture; +import java.util.Comparator; import java.util.List; import java.util.function.Consumer; public interface TbSqlQueue { - void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, int queueIndex); + void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int queueIndex); void destroy(); 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 2803dec8dd..69616155d2 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 @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -71,6 +72,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Value("${sql.attributes.batch_threads:4}") private int batchThreads; + @Value("${sql.batch_sort:false}") + private boolean batchSortEnabled; + private TbSqlBlockingQueueWrapper queue; @PostConstruct @@ -81,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl .maxDelay(maxDelay) .statsPrintIntervalMs(statsPrintIntervalMs) .statsNamePrefix("attributes") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getId().getEntityId().hashCode(); queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, batchThreads, statsFactory); - queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v)); + queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v), + Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getEntityType().name()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()) + ); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java new file mode 100644 index 0000000000..8116b711d5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.device; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; + +import java.util.UUID; + +public interface DeviceProfileRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d " + + "WHERE d.id = :deviceProfileId") + DeviceProfileInfo findDeviceProfileInfoById(@Param("deviceProfileId") UUID deviceProfileId); + + @Query("SELECT d FROM DeviceProfileEntity d WHERE " + + "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceProfiles(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d WHERE " + + "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT d FROM DeviceProfileEntity d " + + "WHERE d.tenantId = :tenantId AND d.isDefault = true") + DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d " + + "WHERE d.tenantId = :tenantId AND d.isDefault = true") + DeviceProfileInfo findDefaultDeviceProfileInfo(@Param("tenantId") UUID tenantId); + + DeviceProfileEntity findByTenantIdAndName(UUID id, String profileName); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 828a9e28f0..02f62d3358 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -31,9 +31,10 @@ import java.util.UUID; */ public interface DeviceRepository extends PagingAndSortingRepository { - @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + "FROM DeviceEntity d " + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + "WHERE d.id = :deviceId") DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId); @@ -45,9 +46,18 @@ public interface DeviceRepository extends PagingAndSortingRepository findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, + @Param("profileId") UUID profileId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + "FROM DeviceEntity d " + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + "WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") @@ -66,9 +76,10 @@ public interface DeviceRepository extends PagingAndSortingRepository findDeviceInfosByTenantId(@Param("tenantId") UUID tenantId, @@ -83,9 +94,10 @@ public interface DeviceRepository extends PagingAndSortingRepository findDeviceInfosByTenantIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, + @Param("deviceProfileId") UUID deviceProfileId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.type = :type " + @@ -104,9 +128,10 @@ public interface DeviceRepository extends PagingAndSortingRepository findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("deviceProfileId") UUID deviceProfileId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId") List findTenantDeviceTypes(@Param("tenantId") UUID tenantId); @@ -128,4 +167,6 @@ public interface DeviceRepository extends PagingAndSortingRepository DaoUtil.toPageable(pageLink))); } + @Override + public PageData findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findByTenantIdAndProfileId( + tenantId, + profileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + @Override public PageData findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { return DaoUtil.toPageData( @@ -146,6 +156,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); } + @Override + public PageData findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndDeviceProfileId( + tenantId, + deviceProfileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); + } + @Override public PageData findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { return DaoUtil.toPageData( @@ -168,6 +188,17 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); } + @Override + public PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId( + tenantId, + customerId, + deviceProfileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); + } + @Override public ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId) { return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId))); @@ -183,6 +214,11 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao return service.submit(() -> DaoUtil.getData(deviceRepository.findByTenantIdAndId(tenantId.getId(), id))); } + @Override + public Long countDevicesByDeviceProfileId(TenantId tenantId, UUID deviceProfileId) { + return deviceRepository.countByDeviceProfileId(deviceProfileId); + } + private List convertTenantDeviceTypesToDto(UUID tenantId, List types) { List list = Collections.emptyList(); if (types != null && !types.isEmpty()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java new file mode 100644 index 0000000000..9399d27304 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.device; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Objects; +import java.util.UUID; + +@Component +public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao implements DeviceProfileDao { + + @Autowired + private DeviceProfileRepository deviceProfileRepository; + + @Override + protected Class getEntityClass() { + return DeviceProfileEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return deviceProfileRepository; + } + + @Override + public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, UUID deviceProfileId) { + return deviceProfileRepository.findDeviceProfileInfoById(deviceProfileId); + } + + @Override + public PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceProfileRepository.findDeviceProfiles( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData( + deviceProfileRepository.findDeviceProfileInfos( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public DeviceProfile findDefaultDeviceProfile(TenantId tenantId) { + return DaoUtil.getData(deviceProfileRepository.findByDefaultTrueAndTenantId(tenantId.getId())); + } + + @Override + public DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId) { + return deviceProfileRepository.findDefaultDeviceProfileInfo(tenantId.getId()); + } + + @Override + public DeviceProfile findByName(TenantId tenantId, String profileName) { + return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 3ad734deb1..e3aa614bfe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -77,7 +77,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { alarmFieldColumnMap.put("originator", "originator_name"); } - private static final String SELECT_ORIGINATOR_NAME = " CASE" + + private static final String SELECT_ORIGINATOR_NAME = " COALESCE(CASE" + " WHEN a.originator_type = " + EntityType.TENANT.ordinal() + " THEN (select title from tenant where id = a.originator_id)" + " WHEN a.originator_type = " + EntityType.CUSTOMER.ordinal() + @@ -92,7 +92,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " THEN (select name from device where id = a.originator_id)" + " WHEN a.originator_type = " + EntityType.ENTITY_VIEW.ordinal() + " THEN (select name from entity_view where id = a.originator_id)" + - " END as originator_name"; + " END, 'Deleted') as originator_name"; private static final String FIELDS_SELECTION = "select a.id as id," + " a.created_time as created_time," + 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 95649e007e..232a534e88 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 @@ -225,9 +225,9 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " INNER JOIN related_entities re ON" + " r.$in_id = re.$out_id and r.$in_type = re.$out_type and" + " relation_type_group = 'COMMON' %s)" + - " SELECT re.$out_id entity_id, re.$out_type entity_type, re.lvl lvl" + + " SELECT re.$out_id entity_id, re.$out_type entity_type, max(re.lvl) lvl" + " from related_entities re" + - " %s ) entity"; + " %s GROUP BY entity_id, entity_type) entity"; private static final String HIERARCHICAL_TO_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "to").replace("$out", "from"); private static final String HIERARCHICAL_FROM_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "from").replace("$out", "to"); @@ -457,8 +457,6 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { private String entitySearchQuery(QueryContext ctx, EntitySearchQueryFilter entityFilter, EntityType entityType, List types) { EntityId rootId = entityFilter.getRootEntity(); - //TODO: fetch last level only. - //TODO: fetch distinct records. String lvlFilter = getLvlFilter(entityFilter.getMaxLevel()); String selectFields = "SELECT tenant_id, customer_id, id, created_time, type, name, additional_info " + (entityType.equals(EntityType.ENTITY_VIEW) ? "" : ", label ") @@ -469,8 +467,21 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { ctx.addStringParameter("where_relation_type", entityFilter.getRelationType()); whereFilter += " re.relation_type = :where_relation_type AND"; } + String toOrFrom = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from"); whereFilter += " re." + (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from") + "_type = :where_entity_type"; - + if (entityFilter.isFetchLastLevelOnly()) { + String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); + StringBuilder notExistsPart = new StringBuilder(); + notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr where ") + .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") + .append(" and ") + .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); + if (!StringUtils.isEmpty(entityFilter.getRelationType())) { + notExistsPart.append(" and nr.relation_type = :where_relation_type"); + } + notExistsPart.append(")"); + whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; + } from = String.format(from, lvlFilter, whereFilter); String query = "( " + selectFields + from + ")"; if (types != null && !types.isEmpty()) { @@ -500,16 +511,15 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { ctx.addStringParameter("relation_root_type", rootId.getEntityType().name()); StringBuilder whereFilter = new StringBuilder(); - ; + boolean noConditions = true; + boolean single = entityFilter.getFilters() != null && entityFilter.getFilters().size() == 1; if (entityFilter.getFilters() != null && !entityFilter.getFilters().isEmpty()) { - boolean single = entityFilter.getFilters().size() == 1; int entityTypeFilterIdx = 0; for (EntityTypeFilter etf : entityFilter.getFilters()) { String etfCondition = buildEtfCondition(ctx, etf, entityFilter.getDirection(), entityTypeFilterIdx++); if (!etfCondition.isEmpty()) { if (noConditions) { - whereFilter.append(" WHERE "); noConditions = false; } else { whereFilter.append(" OR "); @@ -525,12 +535,33 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } } if (noConditions) { - whereFilter.append(" WHERE re.") + whereFilter.append(" re.") .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())); } - from = String.format(from, lvlFilter, whereFilter); + + if (!noConditions && !single) { + whereFilter = new StringBuilder().append("(").append(whereFilter).append(")"); + } + + if (entityFilter.isFetchLastLevelOnly()) { + String toOrFrom = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from"); + String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); + + StringBuilder notExistsPart = new StringBuilder(); + notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE "); + notExistsPart.append(whereFilter.toString()); + notExistsPart + .append(" and ") + .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") + .append(" and ") + .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); + + notExistsPart.append(")"); + whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); + } + from = String.format(from, lvlFilter, " WHERE " + whereFilter); return "( " + selectFields + from + ")"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index e11e7859cc..525483f0a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -54,24 +54,24 @@ public class EntityKeyMapping { private static final Map entityFieldColumnMap = new HashMap<>(); private static final Map> aliases = new HashMap<>(); - private static final String CREATED_TIME = "createdTime"; - private static final String ENTITY_TYPE = "entityType"; - private static final String NAME = "name"; - private static final String TYPE = "type"; - private static final String LABEL = "label"; - private static final String FIRST_NAME = "firstName"; - private static final String LAST_NAME = "lastName"; - private static final String EMAIL = "email"; - private static final String TITLE = "title"; - private static final String REGION = "region"; - private static final String COUNTRY = "country"; - private static final String STATE = "state"; - private static final String CITY = "city"; - private static final String ADDRESS = "address"; - private static final String ADDRESS_2 = "address2"; - private static final String ZIP = "zip"; - private static final String PHONE = "phone"; - private static final String ADDITIONAL_INFO = "additionalInfo"; + public static final String CREATED_TIME = "createdTime"; + public static final String ENTITY_TYPE = "entityType"; + public static final String NAME = "name"; + public static final String TYPE = "type"; + public static final String LABEL = "label"; + public static final String FIRST_NAME = "firstName"; + public static final String LAST_NAME = "lastName"; + public static final String EMAIL = "email"; + public static final String TITLE = "title"; + public static final String REGION = "region"; + public static final String COUNTRY = "country"; + public static final String STATE = "state"; + public static final String CITY = "city"; + public static final String ADDRESS = "address"; + public static final String ADDRESS_2 = "address2"; + public static final String ZIP = "zip"; + public static final String PHONE = "phone"; + public static final String ADDITIONAL_INFO = "additionalInfo"; public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); public static final List widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java new file mode 100644 index 0000000000..51e487b456 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.rule; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.RuleNodeStateEntity; +import org.thingsboard.server.dao.rule.RuleNodeStateDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.UUID; + +@Slf4j +@Component +public class JpaRuleNodeStateDao extends JpaAbstractDao implements RuleNodeStateDao { + + @Autowired + private RuleNodeStateRepository ruleNodeStateRepository; + + @Override + protected Class getEntityClass() { + return RuleNodeStateEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return ruleNodeStateRepository; + } + + @Override + public PageData findByRuleNodeId(UUID ruleNodeId, PageLink pageLink) { + return DaoUtil.toPageData(ruleNodeStateRepository.findByRuleNodeId(ruleNodeId, DaoUtil.toPageable(pageLink))); + } + + @Override + public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) { + return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java new file mode 100644 index 0000000000..403dcd1377 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.rule; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.model.sql.RuleNodeStateEntity; + +import java.util.UUID; + +public interface RuleNodeStateRepository extends PagingAndSortingRepository { + + @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId") + Page findByRuleNodeId(@Param("ruleNodeId") UUID ruleNodeId, Pageable pageable); + + @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId and e.entityId = :entityId") + RuleNodeStateEntity findByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index 6f22af118c..7352f8d0a5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -19,11 +19,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TenantEntity; +import org.thingsboard.server.dao.model.sql.TenantInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.tenant.TenantDao; @@ -50,6 +52,11 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao return tenantRepository; } + @Override + public TenantInfo findTenantInfoById(TenantId tenantId, UUID id) { + return DaoUtil.getData(tenantRepository.findTenantInfoById(id)); + } + @Override public PageData findTenantsByRegion(TenantId tenantId, String region, PageLink pageLink) { return DaoUtil.toPageData(tenantRepository @@ -58,4 +65,13 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); } + + @Override + public PageData findTenantInfosByRegion(TenantId tenantId, String region, PageLink pageLink) { + return DaoUtil.toPageData(tenantRepository + .findTenantInfoByRegionNextPage( + region, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, TenantInfoEntity.tenantInfoColumnMap))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java new file mode 100644 index 0000000000..f80f4d9f2c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.tenant; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.TenantProfileEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.tenant.TenantProfileDao; + +import java.util.Objects; +import java.util.UUID; + +@Component +public class JpaTenantProfileDao extends JpaAbstractSearchTextDao implements TenantProfileDao { + + @Autowired + private TenantProfileRepository tenantProfileRepository; + + @Override + protected Class getEntityClass() { + return TenantProfileEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return tenantProfileRepository; + } + + @Override + public EntityInfo findTenantProfileInfoById(TenantId tenantId, UUID tenantProfileId) { + return tenantProfileRepository.findTenantProfileInfoById(tenantProfileId); + } + + @Override + public PageData findTenantProfiles(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + tenantProfileRepository.findTenantProfiles( + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData( + tenantProfileRepository.findTenantProfileInfos( + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public TenantProfile findDefaultTenantProfile(TenantId tenantId) { + return DaoUtil.getData(tenantProfileRepository.findByDefaultTrue()); + } + + @Override + public EntityInfo findDefaultTenantProfileInfo(TenantId tenantId) { + return tenantProfileRepository.findDefaultTenantProfileInfo(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java new file mode 100644 index 0000000000..9c0687ab64 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.tenant; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.dao.model.sql.TenantProfileEntity; + +import java.util.UUID; + +public interface TenantProfileRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + + "FROM TenantProfileEntity t " + + "WHERE t.id = :tenantProfileId") + EntityInfo findTenantProfileInfoById(@Param("tenantProfileId") UUID tenantProfileId); + + @Query("SELECT t FROM TenantProfileEntity t WHERE " + + "LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantProfiles(@Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + + "FROM TenantProfileEntity t " + + "WHERE LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantProfileInfos(@Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT t FROM TenantProfileEntity t " + + "WHERE t.isDefault = true") + TenantProfileEntity findByDefaultTrue(); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + + "FROM TenantProfileEntity t " + + "WHERE t.isDefault = true") + EntityInfo findDefaultTenantProfileInfo(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 012920e187..e1ff50eb2a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.TenantEntity; +import org.thingsboard.server.dao.model.sql.TenantInfoEntity; import java.util.UUID; @@ -29,9 +30,24 @@ import java.util.UUID; */ public interface TenantRepository extends PagingAndSortingRepository { + @Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " + + "FROM TenantEntity t " + + "LEFT JOIN TenantProfileEntity p on p.id = t.tenantProfileId " + + "WHERE t.id = :tenantId") + TenantInfoEntity findTenantInfoById(@Param("tenantId") UUID tenantId); + @Query("SELECT t FROM TenantEntity t WHERE t.region = :region " + "AND LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") Page findByRegionNextPage(@Param("region") String region, @Param("textSearch") String textSearch, Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " + + "FROM TenantEntity t " + + "LEFT JOIN TenantProfileEntity p on p.id = t.tenantProfileId " + + "WHERE t.region = :region " + + "AND LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantInfoByRegionNextPage(@Param("region") String region, + @Param("textSearch") String textSearch, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 079bf1b660..3cee730ee3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.thingsboard.server.common.data.id.EntityId; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -40,9 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -68,11 +68,16 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq .maxDelay(tsMaxDelay) .statsPrintIntervalMs(tsStatsPrintIntervalMs) .statsNamePrefix("ts") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, tsBatchThreads, statsFactory); - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparing(AbstractTsKvEntity::getKey) + .thenComparing(AbstractTsKvEntity::getTs) + ); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java index 9ab8aa3dfc..8dcefeca61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -53,6 +53,9 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries @Value("${sql.timescale.batch_threads:4}") protected int timescaleBatchThreads; + @Value("${sql.batch_sort:false}") + protected boolean batchSortEnabled; + protected ListenableFuture> processFindAllAsync(TenantId tenantId, EntityId entityId, List queries) { List>> futures = queries .stream() diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index ee7c02d558..f6e5e8aa40 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; @@ -50,12 +51,10 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @Component @@ -90,6 +89,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Value("${sql.ts_latest.batch_threads:4}") private int tsLatestBatchThreads; + @Value("${sql.batch_sort:false}") + protected boolean batchSortEnabled; + @Autowired protected ScheduledLogExecutorComponent logExecutor; @@ -104,6 +106,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme .maxDelay(tsLatestMaxDelay) .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) .statsNamePrefix("ts.latest") + .batchSortEnabled(false) .build(); java.util.function.Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); @@ -113,14 +116,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme Map trueLatest = new HashMap<>(); v.forEach(ts -> { TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); - TsKvLatestEntity old = trueLatest.get(key); - if (old == null || old.getTs() < ts.getTs()) { - trueLatest.put(key, ts); - } + trueLatest.merge(key, ts, (oldTs, newTs) -> oldTs.getTs() < newTs.getTs() ? newTs : oldTs); }); List latestEntities = new ArrayList<>(trueLatest.values()); + if (batchSortEnabled) { + latestEntities.sort(Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparingInt(AbstractTsKvEntity::getKey)); + } insertLatestTsRepository.saveOrUpdate(latestEntities); - }); + }, (l, r) -> 0); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index 71647f88f9..6d5923c508 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -43,11 +44,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -78,12 +75,17 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements .maxDelay(tsMaxDelay) .statsPrintIntervalMs(tsStatsPrintIntervalMs) .statsNamePrefix("ts.timescale") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, timescaleBatchThreads, statsFactory); - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparing(AbstractTsKvEntity::getKey) + .thenComparing(AbstractTsKvEntity::getTs) + ); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java index d850694135..18fd3ff73e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java @@ -16,13 +16,18 @@ package org.thingsboard.server.dao.tenant; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import java.util.UUID; + public interface TenantDao extends Dao { + TenantInfo findTenantInfoById(TenantId tenantId, UUID id); + /** * Save or update tenant object * @@ -39,5 +44,7 @@ public interface TenantDao extends Dao { * @return the list of tenant objects */ PageData findTenantsByRegion(TenantId tenantId, String region, PageLink pageLink); + + PageData findTenantInfosByRegion(TenantId tenantId, String region, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileDao.java new file mode 100644 index 0000000000..1c9e50431f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileDao.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface TenantProfileDao extends Dao { + + EntityInfo findTenantProfileInfoById(TenantId tenantId, UUID tenantProfileId); + + TenantProfile save(TenantId tenantId, TenantProfile tenantProfile); + + PageData findTenantProfiles(TenantId tenantId, PageLink pageLink); + + PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink); + + TenantProfile findDefaultTenantProfile(TenantId tenantId); + + EntityInfo findDefaultTenantProfileInfo(TenantId tenantId); +} 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 new file mode 100644 index 0000000000..939e2b7426 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -0,0 +1,253 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.TenantProfileData; +import org.thingsboard.server.common.data.id.TenantId; +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.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +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.util.mapping.JacksonUtil; + +import java.util.Arrays; +import java.util.Collections; + +import static org.thingsboard.server.common.data.CacheConstants.TENANT_PROFILE_CACHE; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class TenantProfileServiceImpl extends AbstractEntityService implements TenantProfileService { + + private static final String INCORRECT_TENANT_PROFILE_ID = "Incorrect tenantProfileId "; + + @Autowired + private TenantProfileDao tenantProfileDao; + + @Autowired + private CacheManager cacheManager; + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{#tenantProfileId.id}") + @Override + public TenantProfile findTenantProfileById(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing findTenantProfileById [{}]", tenantProfileId); + Validator.validateId(tenantProfileId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + return tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + } + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'info', #tenantProfileId.id}") + @Override + public EntityInfo findTenantProfileInfoById(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing findTenantProfileInfoById [{}]", tenantProfileId); + Validator.validateId(tenantProfileId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + return tenantProfileDao.findTenantProfileInfoById(tenantId, tenantProfileId.getId()); + } + + @Override + public TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile) { + log.trace("Executing saveTenantProfile [{}]", tenantProfile); + tenantProfileValidator.validate(tenantProfile, (tenantProfile1) -> TenantId.SYS_TENANT_ID); + TenantProfile savedTenantProfile; + try { + savedTenantProfile = tenantProfileDao.save(tenantId, tenantProfile); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("tenant_profile_name_unq_key")) { + throw new DataValidationException("Tenant profile with such name already exists!"); + } else { + throw t; + } + } + Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); + cache.evict(Collections.singletonList(savedTenantProfile.getId().getId())); + cache.evict(Arrays.asList("info", savedTenantProfile.getId().getId())); + if (savedTenantProfile.isDefault()) { + cache.evict(Collections.singletonList("default")); + cache.evict(Arrays.asList("default", "info")); + } + return savedTenantProfile; + } + + @Override + public void deleteTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing deleteTenantProfile [{}]", tenantProfileId); + validateId(tenantId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + if (tenantProfile != null && tenantProfile.isDefault()) { + throw new DataValidationException("Deletion of Default Tenant Profile is prohibited!"); + } + this.removeTenantProfile(tenantId, tenantProfileId); + } + + private void removeTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { + try { + tenantProfileDao.removeById(tenantId, tenantProfileId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_tenant_profile")) { + throw new DataValidationException("The tenant profile referenced by the tenants cannot be deleted!"); + } else { + throw t; + } + } + deleteEntityRelations(tenantId, tenantProfileId); + Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); + cache.evict(Collections.singletonList(tenantProfileId.getId())); + cache.evict(Arrays.asList("info", tenantProfileId.getId())); + } + + @Override + public PageData findTenantProfiles(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findTenantProfiles pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return tenantProfileDao.findTenantProfiles(tenantId, pageLink); + } + + @Override + public PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findTenantProfileInfos pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return tenantProfileDao.findTenantProfileInfos(tenantId, pageLink); + } + + @Override + public TenantProfile findOrCreateDefaultTenantProfile(TenantId tenantId) { + log.trace("Executing findOrCreateDefaultTenantProfile"); + TenantProfile defaultTenantProfile = findDefaultTenantProfile(tenantId); + if (defaultTenantProfile == null) { + defaultTenantProfile = new TenantProfile(); + defaultTenantProfile.setDefault(true); + defaultTenantProfile.setName("Default"); + defaultTenantProfile.setProfileData(new TenantProfileData()); + defaultTenantProfile.setDescription("Default tenant profile"); + defaultTenantProfile.setIsolatedTbCore(false); + defaultTenantProfile.setIsolatedTbRuleEngine(false); + defaultTenantProfile = saveTenantProfile(tenantId, defaultTenantProfile); + } + return defaultTenantProfile; + } + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'default'}") + @Override + public TenantProfile findDefaultTenantProfile(TenantId tenantId) { + log.trace("Executing findDefaultTenantProfile"); + return tenantProfileDao.findDefaultTenantProfile(tenantId); + } + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'default', 'info'}") + @Override + public EntityInfo findDefaultTenantProfileInfo(TenantId tenantId) { + log.trace("Executing findDefaultTenantProfileInfo"); + return tenantProfileDao.findDefaultTenantProfileInfo(tenantId); + } + + @Override + public boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing setDefaultTenantProfile [{}]", tenantProfileId); + validateId(tenantId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + if (!tenantProfile.isDefault()) { + Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); + tenantProfile.setDefault(true); + TenantProfile previousDefaultTenantProfile = findDefaultTenantProfile(tenantId); + boolean changed = false; + if (previousDefaultTenantProfile == null) { + tenantProfileDao.save(tenantId, tenantProfile); + changed = true; + } else if (!previousDefaultTenantProfile.getId().equals(tenantProfile.getId())) { + previousDefaultTenantProfile.setDefault(false); + tenantProfileDao.save(tenantId, previousDefaultTenantProfile); + tenantProfileDao.save(tenantId, tenantProfile); + cache.evict(Collections.singletonList(previousDefaultTenantProfile.getId().getId())); + cache.evict(Arrays.asList("info", previousDefaultTenantProfile.getId().getId())); + changed = true; + } + if (changed) { + cache.evict(Collections.singletonList(tenantProfile.getId().getId())); + cache.evict(Arrays.asList("info", tenantProfile.getId().getId())); + cache.evict(Collections.singletonList("default")); + cache.evict(Arrays.asList("default", "info")); + } + return changed; + } + return false; + } + + @Override + public void deleteTenantProfiles(TenantId tenantId) { + log.trace("Executing deleteTenantProfiles"); + tenantProfilesRemover.removeEntities(tenantId, null); + } + + private DataValidator tenantProfileValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId tenantId, TenantProfile tenantProfile) { + if (StringUtils.isEmpty(tenantProfile.getName())) { + throw new DataValidationException("Tenant profile name should be specified!"); + } + if (tenantProfile.isDefault()) { + TenantProfile defaultTenantProfile = findDefaultTenantProfile(tenantId); + if (defaultTenantProfile != null && !defaultTenantProfile.getId().equals(tenantProfile.getId())) { + throw new DataValidationException("Another default tenant profile is present!"); + } + } + } + + @Override + protected void validateUpdate(TenantId tenantId, TenantProfile tenantProfile) { + TenantProfile old = tenantProfileDao.findById(TenantId.SYS_TENANT_ID, tenantProfile.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing tenant profile!"); + } else if (old.isIsolatedTbRuleEngine() != tenantProfile.isIsolatedTbRuleEngine()) { + throw new DataValidationException("Can't update isolatedTbRuleEngine property!"); + } else if (old.isIsolatedTbCore() != tenantProfile.isIsolatedTbCore()) { + throw new DataValidationException("Can't update isolatedTbCore property!"); + } + } + }; + + private PaginatedRemover tenantProfilesRemover = + new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { + return tenantProfileDao.findTenantProfiles(tenantId, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, TenantProfile entity) { + removeTenantProfile(tenantId, entity.getId()); + } + }; + +} 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 b63e4f7846..52761184fd 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 @@ -21,6 +21,8 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -28,6 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink; 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.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; @@ -51,6 +54,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private TenantDao tenantDao; + @Autowired + private TenantProfileService tenantProfileService; + @Autowired private UserService userService; @@ -63,6 +69,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private DeviceService deviceService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired private EntityViewService entityViewService; @@ -82,6 +91,13 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe return tenantDao.findById(tenantId, tenantId.getId()); } + @Override + public TenantInfo findTenantInfoById(TenantId tenantId) { + log.trace("Executing findTenantInfoById [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return tenantDao.findTenantInfoById(tenantId, tenantId.getId()); + } + @Override public ListenableFuture findTenantByIdAsync(TenantId callerId, TenantId tenantId) { log.trace("Executing TenantIdAsync [{}]", tenantId); @@ -93,8 +109,16 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe public Tenant saveTenant(Tenant tenant) { log.trace("Executing saveTenant [{}]", tenant); tenant.setRegion(DEFAULT_TENANT_REGION); + if (tenant.getTenantProfileId() == null) { + TenantProfile tenantProfile = this.tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID); + tenant.setTenantProfileId(tenantProfile.getId()); + } tenantValidator.validate(tenant, Tenant::getId); - return tenantDao.save(tenant.getId(), tenant); + Tenant savedTenant = tenantDao.save(tenant.getId(), tenant); + if (tenant.getId() == null) { + deviceProfileService.createDefaultDeviceProfile(savedTenant.getId()); + } + return savedTenant; } @Override @@ -107,6 +131,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe entityViewService.deleteEntityViewsByTenantId(tenantId); assetService.deleteAssetsByTenantId(tenantId); deviceService.deleteDevicesByTenantId(tenantId); + deviceProfileService.deleteDeviceProfilesByTenantId(tenantId); userService.deleteTenantAdmins(tenantId); ruleChainService.deleteRuleChainsByTenantId(tenantId); tenantDao.removeById(tenantId, tenantId.getId()); @@ -120,6 +145,13 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe return tenantDao.findTenantsByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); } + @Override + public PageData findTenantInfos(PageLink pageLink) { + log.trace("Executing findTenantInfos pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return tenantDao.findTenantInfosByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); + } + @Override public void deleteTenants() { log.trace("Executing deleteTenants"); @@ -143,10 +175,6 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe Tenant old = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId()); if (old == null) { throw new DataValidationException("Can't update non existing tenant!"); - } else if (old.isIsolatedTbRuleEngine() != tenant.isIsolatedTbRuleEngine()) { - throw new DataValidationException("Can't update isolatedTbRuleEngine property!"); - } else if (old.isIsolatedTbCore() != tenant.isIsolatedTbCore()) { - throw new DataValidationException("Can't update isolatedTbCore property!"); } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java index d654e85d5c..907a17157b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.util.mapping; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; @@ -30,28 +31,28 @@ public class JacksonUtil { public static T convertValue(Object fromValue, Class toValueType) { try { - return OBJECT_MAPPER.convertValue(fromValue, toValueType); + return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueType) : null; } catch (IllegalArgumentException e) { throw new IllegalArgumentException("The given object value: " - + fromValue + " cannot be converted to " + toValueType); + + fromValue + " cannot be converted to " + toValueType, e); } } public static T fromString(String string, Class clazz) { try { - return OBJECT_MAPPER.readValue(string, clazz); + return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; } catch (IOException e) { throw new IllegalArgumentException("The given string value: " - + string + " cannot be transformed to Json object"); + + string + " cannot be transformed to Json object", e); } } public static String toString(Object value) { try { - return OBJECT_MAPPER.writeValueAsString(value); + return value != null ? OBJECT_MAPPER.writeValueAsString(value) : null; } catch (JsonProcessingException e) { throw new IllegalArgumentException("The given Json object value: " - + value + " cannot be transformed to a String"); + + value + " cannot be transformed to a String", e); } } @@ -65,8 +66,16 @@ public class JacksonUtil { throw new IllegalArgumentException(e); } } + + public static ObjectNode newObjectNode(){ + return OBJECT_MAPPER.createObjectNode(); + } public static T clone(T value) { return fromString(toString(value), (Class) value.getClass()); } + + public static JsonNode valueToTree(T value) { + return OBJECT_MAPPER.valueToTree(value); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinarySqlTypeDescriptor.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinarySqlTypeDescriptor.java new file mode 100644 index 0000000000..05d0315101 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinarySqlTypeDescriptor.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util.mapping; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.BasicBinder; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +public class JsonBinarySqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { + + public static final JsonBinarySqlTypeDescriptor INSTANCE = new JsonBinarySqlTypeDescriptor(); + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { + return new BasicBinder(javaTypeDescriptor, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setObject(index, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { + st.setObject(name, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); + } + }; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinaryType.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinaryType.java new file mode 100644 index 0000000000..2eea7e7dc7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinaryType.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util.mapping; + +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +public class JsonBinaryType extends AbstractSingleColumnStandardBasicType implements DynamicParameterizedType { + + public JsonBinaryType() { + super( + JsonBinarySqlTypeDescriptor.INSTANCE, + new JsonTypeDescriptor() + ); + } + + public String getName() { + return "jsonb"; + } + + @Override + protected boolean registerUnderJavaType() { + return true; + } + + @Override + public void setParameterValues(Properties parameters) { + ((JsonTypeDescriptor) getJavaTypeDescriptor()) + .setParameterValues(parameters); + } +} diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 664925db76..99fc915d24 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -14,7 +14,6 @@ -- limitations under the License. -- - CREATE TABLE IF NOT EXISTS admin_settings ( id uuid NOT NULL CONSTRAINT admin_settings_pkey PRIMARY KEY, created_time bigint NOT NULL, @@ -122,17 +121,72 @@ CREATE TABLE IF NOT EXISTS dashboard ( title varchar(255) ); +CREATE TABLE IF NOT EXISTS rule_chain ( + id uuid NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + configuration varchar(10000000), + name varchar(255), + first_rule_node_id uuid, + root boolean, + debug_mode boolean, + search_text varchar(255), + tenant_id uuid +); + +CREATE TABLE IF NOT EXISTS rule_node ( + id uuid NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_chain_id uuid, + additional_info varchar, + configuration varchar(10000000), + type varchar(255), + name varchar(255), + debug_mode boolean, + search_text varchar(255) +); + +CREATE TABLE IF NOT EXISTS rule_node_state ( + id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_node_id uuid NOT NULL, + entity_type varchar(32) NOT NULL, + entity_id uuid NOT NULL, + state_data varchar(16384) NOT NULL, + CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id), + CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS device_profile ( + id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + type varchar(255), + transport_type varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) +); + CREATE TABLE IF NOT EXISTS device ( id uuid NOT NULL CONSTRAINT device_pkey PRIMARY KEY, created_time bigint NOT NULL, additional_info varchar, customer_id uuid, + device_profile_id uuid NOT NULL, + device_data jsonb, type varchar(255), name varchar(255), label varchar(255), search_text varchar(255), tenant_id uuid, - CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name) + CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id) ); CREATE TABLE IF NOT EXISTS device_credentials ( @@ -183,10 +237,24 @@ CREATE TABLE IF NOT EXISTS tb_user ( tenant_id uuid ); +CREATE TABLE IF NOT EXISTS tenant_profile ( + id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + isolated_tb_core boolean, + isolated_tb_rule_engine boolean, + CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) +); + CREATE TABLE IF NOT EXISTS tenant ( id uuid NOT NULL CONSTRAINT tenant_pkey PRIMARY KEY, created_time bigint NOT NULL, additional_info varchar, + tenant_profile_id uuid NOT NULL, address varchar, address2 varchar, city varchar(255), @@ -198,8 +266,7 @@ CREATE TABLE IF NOT EXISTS tenant ( state varchar(255), title varchar(255), zip varchar(255), - isolated_tb_core boolean, - isolated_tb_rule_engine boolean + CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id) ); CREATE TABLE IF NOT EXISTS user_credentials ( @@ -231,31 +298,6 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( title varchar(255) ); -CREATE TABLE IF NOT EXISTS rule_chain ( - id uuid NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, - created_time bigint NOT NULL, - additional_info varchar, - configuration varchar(10000000), - name varchar(255), - first_rule_node_id uuid, - root boolean, - debug_mode boolean, - search_text varchar(255), - tenant_id uuid -); - -CREATE TABLE IF NOT EXISTS rule_node ( - id uuid NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, - created_time bigint NOT NULL, - rule_chain_id uuid, - additional_info varchar, - configuration varchar(10000000), - type varchar(255), - name varchar(255), - debug_mode boolean, - search_text varchar(255) -); - CREATE TABLE IF NOT EXISTS entity_view ( id uuid NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, created_time bigint NOT NULL, diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 2d3b2c4d75..97134a8e19 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -40,4 +40,4 @@ CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, cu CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); -CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index ad8f97f053..a4ff653aa6 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -25,7 +25,7 @@ CREATE OR REPLACE PROCEDURE insert_tb_schema_settings() $$ BEGIN IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN - INSERT INTO tb_schema_settings (schema_version) VALUES (3001000); + INSERT INTO tb_schema_settings (schema_version) VALUES (3002000); END IF; END; $$; @@ -139,17 +139,72 @@ CREATE TABLE IF NOT EXISTS dashboard ( title varchar(255) ); +CREATE TABLE IF NOT EXISTS rule_chain ( + id uuid NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + configuration varchar(10000000), + name varchar(255), + first_rule_node_id uuid, + root boolean, + debug_mode boolean, + search_text varchar(255), + tenant_id uuid +); + +CREATE TABLE IF NOT EXISTS rule_node ( + id uuid NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_chain_id uuid, + additional_info varchar, + configuration varchar(10000000), + type varchar(255), + name varchar(255), + debug_mode boolean, + search_text varchar(255) +); + +CREATE TABLE IF NOT EXISTS rule_node_state ( + id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_node_id uuid NOT NULL, + entity_type varchar(32) NOT NULL, + entity_id uuid NOT NULL, + state_data varchar(16384) NOT NULL, + CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id), + CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS device_profile ( + id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + type varchar(255), + transport_type varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) +); + CREATE TABLE IF NOT EXISTS device ( id uuid NOT NULL CONSTRAINT device_pkey PRIMARY KEY, created_time bigint NOT NULL, additional_info varchar, customer_id uuid, + device_profile_id uuid NOT NULL, + device_data jsonb, type varchar(255), name varchar(255), label varchar(255), search_text varchar(255), tenant_id uuid, - CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name) + CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id) ); CREATE TABLE IF NOT EXISTS device_credentials ( @@ -207,10 +262,24 @@ CREATE TABLE IF NOT EXISTS tb_user ( tenant_id uuid ); +CREATE TABLE IF NOT EXISTS tenant_profile ( + id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + isolated_tb_core boolean, + isolated_tb_rule_engine boolean, + CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) +); + CREATE TABLE IF NOT EXISTS tenant ( id uuid NOT NULL CONSTRAINT tenant_pkey PRIMARY KEY, created_time bigint NOT NULL, additional_info varchar, + tenant_profile_id uuid NOT NULL, address varchar, address2 varchar, city varchar(255), @@ -222,8 +291,7 @@ CREATE TABLE IF NOT EXISTS tenant ( state varchar(255), title varchar(255), zip varchar(255), - isolated_tb_core boolean, - isolated_tb_rule_engine boolean + CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id) ); CREATE TABLE IF NOT EXISTS user_credentials ( @@ -255,31 +323,6 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( title varchar(255) ); -CREATE TABLE IF NOT EXISTS rule_chain ( - id uuid NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, - created_time bigint NOT NULL, - additional_info varchar, - configuration varchar(10000000), - name varchar(255), - first_rule_node_id uuid, - root boolean, - debug_mode boolean, - search_text varchar(255), - tenant_id uuid -); - -CREATE TABLE IF NOT EXISTS rule_node ( - id uuid NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, - created_time bigint NOT NULL, - rule_chain_id uuid, - additional_info varchar, - configuration varchar(10000000), - type varchar(255), - name varchar(255), - debug_mode boolean, - search_text varchar(255) -); - CREATE TABLE IF NOT EXISTS entity_view ( id uuid NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, created_time bigint NOT NULL, diff --git a/dao/src/main/resources/sql/schema-ts-psql.sql b/dao/src/main/resources/sql/schema-ts-psql.sql index ef6c51aa0a..48f74b17da 100644 --- a/dao/src/main/resources/sql/schema-ts-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-psql.sql @@ -84,6 +84,7 @@ BEGIN AND tablename like 'ts_kv_' || '%' AND tablename != 'ts_kv_latest' AND tablename != 'ts_kv_dictionary' + AND tablename != 'ts_kv_indefinite' LOOP IF partition != partition_by_max_ttl_date THEN IF partition_year IS NOT NULL THEN @@ -187,8 +188,8 @@ $$ DECLARE tenant_cursor CURSOR FOR select tenant.id as tenant_id from tenant; - tenant_id_record varchar; - customer_id_record varchar; + tenant_id_record uuid; + customer_id_record uuid; tenant_ttl bigint; customer_ttl bigint; deleted_for_entities bigint; diff --git a/dao/src/main/resources/sql/schema-types-hsql.sql b/dao/src/main/resources/sql/schema-types-hsql.sql new file mode 100644 index 0000000000..74095c4ed9 --- /dev/null +++ b/dao/src/main/resources/sql/schema-types-hsql.sql @@ -0,0 +1,20 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +DROP TYPE json IF EXISTS; +CREATE TYPE json AS varchar; +DROP TYPE jsonb IF EXISTS; +CREATE TYPE jsonb AS other; diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java new file mode 100644 index 0000000000..a5e6122537 --- /dev/null +++ b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java @@ -0,0 +1,364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.io.sstable; + +import java.io.File; +import java.io.IOError; +import java.io.IOException; +import java.util.*; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.CharMatcher; +import com.google.common.base.Objects; + +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer; +import org.apache.cassandra.io.sstable.metadata.LegacyMetadataSerializer; +import org.apache.cassandra.io.sstable.metadata.MetadataSerializer; +import org.apache.cassandra.utils.Pair; + +import static org.apache.cassandra.io.sstable.Component.separator; + +/** + * A SSTable is described by the keyspace and column family it contains data + * for, a generation (where higher generations contain more recent data) and + * an alphabetic version string. + * + * A descriptor can be marked as temporary, which influences generated filenames. + */ +public class Descriptor +{ + public static String TMP_EXT = ".tmp"; + + /** canonicalized path to the directory where SSTable resides */ + public final File directory; + /** version has the following format: [a-z]+ */ + public final Version version; + public final String ksname; + public final String cfname; + public final int generation; + public final SSTableFormat.Type formatType; + /** digest component - might be {@code null} for old, legacy sstables */ + public final Component digestComponent; + private final int hashCode; + + /** + * A descriptor that assumes CURRENT_VERSION. + */ + @VisibleForTesting + public Descriptor(File directory, String ksname, String cfname, int generation) + { + this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current(), null); + } + + /** + * Constructor for sstable writers only. + */ + public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType) + { + this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType())); + } + + @VisibleForTesting + public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType) + { + this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType())); + } + + public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType, Component digestComponent) + { + assert version != null && directory != null && ksname != null && cfname != null && formatType.info.getLatestVersion().getClass().equals(version.getClass()); + this.version = version; + try + { + this.directory = directory.getCanonicalFile(); + } + catch (IOException e) + { + throw new IOError(e); + } + this.ksname = ksname; + this.cfname = cfname; + this.generation = generation; + this.formatType = formatType; + this.digestComponent = digestComponent; + + hashCode = Objects.hashCode(version, this.directory, generation, ksname, cfname, formatType); + } + + public Descriptor withGeneration(int newGeneration) + { + return new Descriptor(version, directory, ksname, cfname, newGeneration, formatType, digestComponent); + } + + public Descriptor withFormatType(SSTableFormat.Type newType) + { + return new Descriptor(newType.info.getLatestVersion(), directory, ksname, cfname, generation, newType, digestComponent); + } + + public Descriptor withDigestComponent(Component newDigestComponent) + { + return new Descriptor(version, directory, ksname, cfname, generation, formatType, newDigestComponent); + } + + public String tmpFilenameFor(Component component) + { + return filenameFor(component) + TMP_EXT; + } + + public String filenameFor(Component component) + { + return baseFilename() + separator + component.name(); + } + + public String baseFilename() + { + StringBuilder buff = new StringBuilder(); + buff.append(directory).append(File.separatorChar); + appendFileName(buff); + return buff.toString(); + } + + private void appendFileName(StringBuilder buff) + { + if (!version.hasNewFileName()) + { + buff.append(ksname).append(separator); + buff.append(cfname).append(separator); + } + buff.append(version).append(separator); + buff.append(generation); + if (formatType != SSTableFormat.Type.LEGACY) + buff.append(separator).append(formatType.name); + } + + public String relativeFilenameFor(Component component) + { + final StringBuilder buff = new StringBuilder(); + appendFileName(buff); + buff.append(separator).append(component.name()); + return buff.toString(); + } + + public SSTableFormat getFormat() + { + return formatType.info; + } + + /** Return any temporary files found in the directory */ + public List getTemporaryFiles() + { + List ret = new ArrayList<>(); + File[] tmpFiles = directory.listFiles((dir, name) -> + name.endsWith(Descriptor.TMP_EXT)); + + for (File tmpFile : tmpFiles) + ret.add(tmpFile); + + return ret; + } + + /** + * Files obsoleted by CASSANDRA-7066 : temporary files and compactions_in_progress. We support + * versions 2.1 (ka) and 2.2 (la). + * Temporary files have tmp- or tmplink- at the beginning for 2.2 sstables or after ks-cf- for 2.1 sstables + */ + + private final static String LEGACY_COMP_IN_PROG_REGEX_STR = "^compactions_in_progress(\\-[\\d,a-f]{32})?$"; + private final static Pattern LEGACY_COMP_IN_PROG_REGEX = Pattern.compile(LEGACY_COMP_IN_PROG_REGEX_STR); + private final static String LEGACY_TMP_REGEX_STR = "^((.*)\\-(.*)\\-)?tmp(link)?\\-((?:l|k).)\\-(\\d)*\\-(.*)$"; + private final static Pattern LEGACY_TMP_REGEX = Pattern.compile(LEGACY_TMP_REGEX_STR); + + public static boolean isLegacyFile(File file) + { + if (file.isDirectory()) + return file.getParentFile() != null && + file.getParentFile().getName().equalsIgnoreCase("system") && + LEGACY_COMP_IN_PROG_REGEX.matcher(file.getName()).matches(); + else + return LEGACY_TMP_REGEX.matcher(file.getName()).matches(); + } + + public static boolean isValidFile(String fileName) + { + return fileName.endsWith(".db") && !LEGACY_TMP_REGEX.matcher(fileName).matches(); + } + + /** + * @see #fromFilename(File directory, String name) + * @param filename The SSTable filename + * @return Descriptor of the SSTable initialized from filename + */ + public static Descriptor fromFilename(String filename) + { + return fromFilename(filename, false); + } + + public static Descriptor fromFilename(String filename, SSTableFormat.Type formatType) + { + return fromFilename(filename).withFormatType(formatType); + } + + public static Descriptor fromFilename(String filename, boolean skipComponent) + { + File file = new File(filename).getAbsoluteFile(); + return fromFilename(file.getParentFile(), file.getName(), skipComponent).left; + } + + public static Pair fromFilename(File directory, String name) + { + return fromFilename(directory, name, false); + } + + /** + * Filename of the form is vary by version: + * + *
    + *
  • <ksname>-<cfname>-(tmp-)?<version>-<gen>-<component> for cassandra 2.0 and before
  • + *
  • (<tmp marker>-)?<version>-<gen>-<component> for cassandra 3.0 and later
  • + *
+ * + * If this is for SSTable of secondary index, directory should ends with index name for 2.1+. + * + * @param directory The directory of the SSTable files + * @param name The name of the SSTable file + * @param skipComponent true if the name param should not be parsed for a component tag + * + * @return A Descriptor for the SSTable, and the Component remainder. + */ + public static Pair fromFilename(File directory, String name, boolean skipComponent) + { + File parentDirectory = directory != null ? directory : new File("."); + + // tokenize the filename + StringTokenizer st = new StringTokenizer(name, String.valueOf(separator)); + String nexttok; + + // read tokens backwards to determine version + Deque tokenStack = new ArrayDeque<>(); + while (st.hasMoreTokens()) + { + tokenStack.push(st.nextToken()); + } + + // component suffix + String component = skipComponent ? null : tokenStack.pop(); + + nexttok = tokenStack.pop(); + // generation OR format type + SSTableFormat.Type fmt = SSTableFormat.Type.LEGACY; + if (!CharMatcher.digit().matchesAllOf(nexttok)) + { + fmt = SSTableFormat.Type.validate(nexttok); + nexttok = tokenStack.pop(); + } + + // generation + int generation = Integer.parseInt(nexttok); + + // version + nexttok = tokenStack.pop(); + + if (!Version.validate(nexttok)) + throw new UnsupportedOperationException("SSTable " + name + " is too old to open. Upgrade to 2.0 first, and run upgradesstables"); + + Version version = fmt.info.getVersion(nexttok); + + // ks/cf names + String ksname, cfname; + if (version.hasNewFileName()) + { + // for 2.1+ read ks and cf names from directory + File cfDirectory = parentDirectory; + // check if this is secondary index + String indexName = ""; + if (cfDirectory.getName().startsWith(Directories.SECONDARY_INDEX_NAME_SEPARATOR)) + { + indexName = cfDirectory.getName(); + cfDirectory = cfDirectory.getParentFile(); + } + if (cfDirectory.getName().equals(Directories.BACKUPS_SUBDIR)) + { + cfDirectory = cfDirectory.getParentFile(); + } + else if (cfDirectory.getParentFile().getName().equals(Directories.SNAPSHOT_SUBDIR)) + { + cfDirectory = cfDirectory.getParentFile().getParentFile(); + } + cfname = cfDirectory.getName().split("-")[0] + indexName; + ksname = cfDirectory.getParentFile().getName(); + } + else + { + cfname = tokenStack.pop(); + ksname = tokenStack.pop(); + } + assert tokenStack.isEmpty() : "Invalid file name " + name + " in " + directory; + + return Pair.create(new Descriptor(version, parentDirectory, ksname, cfname, generation, fmt, + // _assume_ version from version + Component.digestFor(version.uncompressedChecksumType())), + component); + } + + public IMetadataSerializer getMetadataSerializer() + { + if (version.hasNewStatsFile()) + return new MetadataSerializer(); + else + return new LegacyMetadataSerializer(); + } + + /** + * @return true if the current Cassandra version can read the given sstable version + */ + public boolean isCompatible() + { + return version.isCompatible(); + } + + @Override + public String toString() + { + return baseFilename(); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Descriptor)) + return false; + Descriptor that = (Descriptor)o; + return that.directory.equals(this.directory) + && that.generation == this.generation + && that.ksname.equals(this.ksname) + && that.cfname.equals(this.cfname) + && that.formatType == this.formatType; + } + + @Override + public int hashCode() + { + return hashCode; + } +} diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java new file mode 100644 index 0000000000..af6af442a3 --- /dev/null +++ b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.io.sstable.format; + +import com.google.common.base.CharMatcher; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.db.RowIndexEntry; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.io.sstable.format.big.BigFormat; + +/** + * Provides the accessors to data on disk. + */ +public interface SSTableFormat +{ + static boolean enableSSTableDevelopmentTestMode = Boolean.getBoolean("cassandra.test.sstableformatdevelopment"); + + + Version getLatestVersion(); + Version getVersion(String version); + + SSTableWriter.Factory getWriterFactory(); + SSTableReader.Factory getReaderFactory(); + + RowIndexEntry.IndexSerializer getIndexSerializer(CFMetaData cfm, Version version, SerializationHeader header); + + public static enum Type + { + //Used internally to refer to files with no + //format flag in the filename + LEGACY("big", BigFormat.instance), + + //The original sstable format + BIG("big", BigFormat.instance); + + public final SSTableFormat info; + public final String name; + + public static Type current() + { + return BIG; + } + + private Type(String name, SSTableFormat info) + { + //Since format comes right after generation + //we disallow formats with numeric names + // We have removed this check for compatibility with the embedded cassandra used for tests. + assert !CharMatcher.digit().matchesAllOf(name); + + this.name = name; + this.info = info; + } + + public static Type validate(String name) + { + for (Type valid : Type.values()) + { + //This is used internally for old sstables + if (valid == LEGACY) + continue; + + if (valid.name.equalsIgnoreCase(name)) + return valid; + } + + throw new IllegalArgumentException("No Type constant " + name); + } + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index 1f467209e5..1faa036f93 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -31,7 +31,7 @@ public class NoSqlDaoServiceTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql", "sql/system-test.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql", "sql/system-test.sql"), "sql/hsql/drop-all-tables.sql", "nosql-test.properties" ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java index a6ef3935b0..06f1e70a20 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java @@ -30,7 +30,7 @@ public class SqlDaoServiceTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql" + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql" , "sql/system-data.sql" , "sql/system-test.sql" ), diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index f0c6a3bed5..cf6f574180 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -28,11 +28,20 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.audit.AuditLogLevelFilter; @@ -41,6 +50,7 @@ import org.thingsboard.server.dao.component.ComponentDescriptorService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; @@ -48,6 +58,7 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; @@ -125,7 +136,13 @@ public abstract class AbstractServiceTest { @Autowired private ComponentDescriptorService componentDescriptorService; - class IdComparator> implements Comparator { + @Autowired + protected TenantProfileService tenantProfileService; + + @Autowired + protected DeviceProfileService deviceProfileService; + + class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { return o1.getId().getId().compareTo(o2.getId().getId()); @@ -178,4 +195,22 @@ public abstract class AbstractServiceTest { return new AuditLogLevelFilter(mask); } + protected DeviceProfile createDeviceProfile(TenantId tenantId, String name) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setName(name); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setDescription(name + " Test"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + return deviceProfile; + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java new file mode 100644 index 0000000000..dfaa46e7c1 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java @@ -0,0 +1,291 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class BaseDeviceProfileServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator deviceProfileInfoIdComparator = new IdComparator<>(); + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveDeviceProfile() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Assert.assertNotNull(savedDeviceProfile); + Assert.assertNotNull(savedDeviceProfile.getId()); + Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); + Assert.assertEquals(deviceProfile.getName(), savedDeviceProfile.getName()); + Assert.assertEquals(deviceProfile.getDescription(), savedDeviceProfile.getDescription()); + Assert.assertEquals(deviceProfile.getProfileData(), savedDeviceProfile.getProfileData()); + Assert.assertEquals(deviceProfile.isDefault(), savedDeviceProfile.isDefault()); + Assert.assertEquals(deviceProfile.getDefaultRuleChainId(), savedDeviceProfile.getDefaultRuleChainId()); + savedDeviceProfile.setName("New device profile"); + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); + } + + @Test + public void testFindDeviceProfileById() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertNotNull(foundDeviceProfile); + Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); + } + + @Test + public void testFindDeviceProfileInfoById() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + DeviceProfileInfo foundDeviceProfileInfo = deviceProfileService.findDeviceProfileInfoById(tenantId, savedDeviceProfile.getId()); + Assert.assertNotNull(foundDeviceProfileInfo); + Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfileInfo.getName()); + Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType()); + } + + @Test + public void testFindDefaultDeviceProfile() { + DeviceProfile foundDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + Assert.assertNotNull(foundDefaultDeviceProfile); + Assert.assertNotNull(foundDefaultDeviceProfile.getId()); + Assert.assertNotNull(foundDefaultDeviceProfile.getName()); + } + + @Test + public void testFindDefaultDeviceProfileInfo() { + DeviceProfileInfo foundDefaultDeviceProfileInfo = deviceProfileService.findDefaultDeviceProfileInfo(tenantId); + Assert.assertNotNull(foundDefaultDeviceProfileInfo); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getId()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getName()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getType()); + } + + @Test + public void testSetDefaultDeviceProfile() { + DeviceProfile deviceProfile1 = this.createDeviceProfile(tenantId,"Device Profile 1"); + DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile 2"); + + DeviceProfile savedDeviceProfile1 = deviceProfileService.saveDeviceProfile(deviceProfile1); + DeviceProfile savedDeviceProfile2 = deviceProfileService.saveDeviceProfile(deviceProfile2); + + boolean result = deviceProfileService.setDefaultDeviceProfile(tenantId, savedDeviceProfile1.getId()); + Assert.assertTrue(result); + DeviceProfile defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + Assert.assertNotNull(defaultDeviceProfile); + Assert.assertEquals(savedDeviceProfile1.getId(), defaultDeviceProfile.getId()); + result = deviceProfileService.setDefaultDeviceProfile(tenantId, savedDeviceProfile2.getId()); + Assert.assertTrue(result); + defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + Assert.assertNotNull(defaultDeviceProfile); + Assert.assertEquals(savedDeviceProfile2.getId(), defaultDeviceProfile.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveDeviceProfileWithEmptyName() { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfileService.saveDeviceProfile(deviceProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveDeviceProfileWithSameName() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + deviceProfileService.saveDeviceProfile(deviceProfile); + DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile"); + deviceProfileService.saveDeviceProfile(deviceProfile2); + } + + @Ignore + @Test(expected = DataValidationException.class) + public void testChangeDeviceProfileTypeWithExistingDevices() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + deviceService.saveDevice(device); + //TODO: once we have more profile types, we should test that we can not change profile type in runtime and uncomment the @Ignore. +// savedDeviceProfile.setType(DeviceProfileType.LWM2M); + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + } + + @Test(expected = DataValidationException.class) + public void testChangeDeviceProfileTransportTypeWithExistingDevices() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + deviceService.saveDevice(device); + savedDeviceProfile.setTransportType(DeviceTransportType.MQTT); + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + } + + @Test(expected = DataValidationException.class) + public void testDeleteDeviceProfileWithExistingDevice() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + deviceService.saveDevice(device); + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + } + + @Test + public void testDeleteDeviceProfile() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertNull(foundDeviceProfile); + } + + @Test + public void testFindDeviceProfiles() { + + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + deviceProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"+i); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(deviceProfile)); + } + + List loadedDeviceProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + loadedDeviceProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfiles, idComparator); + + Assert.assertEquals(deviceProfiles, loadedDeviceProfiles); + + for (DeviceProfile deviceProfile : loadedDeviceProfiles) { + if (!deviceProfile.isDefault()) { + deviceProfileService.deleteDeviceProfile(tenantId, deviceProfile.getId()); + } + } + + pageLink = new PageLink(17); + pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindDeviceProfileInfos() { + + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData deviceProfilePageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + Assert.assertFalse(deviceProfilePageData.hasNext()); + Assert.assertEquals(1, deviceProfilePageData.getTotalElements()); + deviceProfiles.addAll(deviceProfilePageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"+i); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(deviceProfile)); + } + + List loadedDeviceProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink); + loadedDeviceProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); + + List deviceProfileInfos = deviceProfiles.stream() + .map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), + deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + + Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); + + for (DeviceProfile deviceProfile : deviceProfiles) { + if (!deviceProfile.isDefault()) { + deviceProfileService.deleteDeviceProfile(tenantId, deviceProfile.getId()); + } + } + + pageLink = new PageLink(17); + pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index 149e78a341..389ddbf638 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -127,7 +127,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } } - + @Test(expected = DataValidationException.class) public void testAssignDeviceToCustomerFromDifferentTenant() { Device device = new Device(); @@ -270,7 +270,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); device.setType("default"); - devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false)); + devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); } String title2 = "Device title 2"; List devicesTitle2 = new ArrayList<>(); @@ -282,7 +282,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); device.setType("default"); - devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false)); + devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); } List loadedDevicesTitle1 = new ArrayList<>(); @@ -435,7 +435,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setName("Device"+i); device.setType("default"); device = deviceService.saveDevice(device); - devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic())); + devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default")); } List loadedDevices = new ArrayList<>(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java new file mode 100644 index 0000000000..a9917126df --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java @@ -0,0 +1,273 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.TenantProfileData; +import org.thingsboard.server.common.data.id.TenantId; +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.exception.DataValidationException; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class BaseTenantProfileServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator tenantProfileInfoIdComparator = new IdComparator<>(); + + @After + public void after() { + tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveTenantProfile() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Assert.assertNotNull(savedTenantProfile); + Assert.assertNotNull(savedTenantProfile.getId()); + Assert.assertTrue(savedTenantProfile.getCreatedTime() > 0); + Assert.assertEquals(tenantProfile.getName(), savedTenantProfile.getName()); + Assert.assertEquals(tenantProfile.getDescription(), savedTenantProfile.getDescription()); + Assert.assertEquals(tenantProfile.getProfileData(), savedTenantProfile.getProfileData()); + Assert.assertEquals(tenantProfile.isDefault(), savedTenantProfile.isDefault()); + Assert.assertEquals(tenantProfile.isIsolatedTbCore(), savedTenantProfile.isIsolatedTbCore()); + Assert.assertEquals(tenantProfile.isIsolatedTbRuleEngine(), savedTenantProfile.isIsolatedTbRuleEngine()); + + savedTenantProfile.setName("New tenant profile"); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + TenantProfile foundTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName()); + } + + @Test + public void testFindTenantProfileById() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + TenantProfile foundTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertNotNull(foundTenantProfile); + Assert.assertEquals(savedTenantProfile, foundTenantProfile); + } + + @Test + public void testFindTenantProfileInfoById() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + EntityInfo foundTenantProfileInfo = tenantProfileService.findTenantProfileInfoById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertNotNull(foundTenantProfileInfo); + Assert.assertEquals(savedTenantProfile.getId(), foundTenantProfileInfo.getId()); + Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName()); + } + + @Test + public void testFindDefaultTenantProfile() { + TenantProfile tenantProfile = this.createTenantProfile("Default Tenant Profile"); + tenantProfile.setDefault(true); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + TenantProfile foundDefaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(foundDefaultTenantProfile); + Assert.assertEquals(savedTenantProfile, foundDefaultTenantProfile); + } + + @Test + public void testFindDefaultTenantProfileInfo() { + TenantProfile tenantProfile = this.createTenantProfile("Default Tenant Profile"); + tenantProfile.setDefault(true); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + EntityInfo foundDefaultTenantProfileInfo = tenantProfileService.findDefaultTenantProfileInfo(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(foundDefaultTenantProfileInfo); + Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfileInfo.getId()); + Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfileInfo.getName()); + } + + @Test + public void testSetDefaultTenantProfile() { + TenantProfile tenantProfile1 = this.createTenantProfile("Tenant Profile 1"); + TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile 2"); + + TenantProfile savedTenantProfile1 = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile1); + TenantProfile savedTenantProfile2 = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + + boolean result = tenantProfileService.setDefaultTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile1.getId()); + Assert.assertTrue(result); + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(defaultTenantProfile); + Assert.assertEquals(savedTenantProfile1.getId(), defaultTenantProfile.getId()); + result = tenantProfileService.setDefaultTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile2.getId()); + Assert.assertTrue(result); + defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(defaultTenantProfile); + Assert.assertEquals(savedTenantProfile2.getId(), defaultTenantProfile.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveTenantProfileWithEmptyName() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveTenantProfileWithSameName() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile"); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + } + + @Test(expected = DataValidationException.class) + public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + savedTenantProfile.setIsolatedTbRuleEngine(true); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveSameTenantProfileWithDifferentIsolatedTbCore() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + savedTenantProfile.setIsolatedTbCore(true); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + } + + @Test(expected = DataValidationException.class) + public void testDeleteTenantProfileWithExistingTenant() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + tenant = tenantService.saveTenant(tenant); + try { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + } finally { + tenantService.deleteTenant(tenant.getId()); + } + } + + @Test + public void testDeleteTenantProfile() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + TenantProfile foundTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertNull(foundTenantProfile); + } + + @Test + public void testFindTenantProfiles() { + + List tenantProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + tenantProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i); + tenantProfiles.add(tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile)); + } + + List loadedTenantProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink); + loadedTenantProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfiles, idComparator); + + Assert.assertEquals(tenantProfiles, loadedTenantProfiles); + + for (TenantProfile tenantProfile : loadedTenantProfiles) { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile.getId()); + } + + pageLink = new PageLink(17); + pageData = tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + } + + @Test + public void testFindTenantProfileInfos() { + + List tenantProfiles = new ArrayList<>(); + + for (int i=0;i<28;i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i); + tenantProfiles.add(tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile)); + } + + List loadedTenantProfileInfos = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData; + do { + pageData = tenantProfileService.findTenantProfileInfos(TenantId.SYS_TENANT_ID, pageLink); + loadedTenantProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfileInfos, tenantProfileInfoIdComparator); + + List tenantProfileInfos = tenantProfiles.stream().map(tenantProfile -> new EntityInfo(tenantProfile.getId(), + tenantProfile.getName())).collect(Collectors.toList()); + + Assert.assertEquals(tenantProfileInfos, loadedTenantProfileInfos); + + for (EntityInfo tenantProfile : loadedTenantProfileInfos) { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, new TenantProfileId(tenantProfile.getId().getId())); + } + + pageLink = new PageLink(17); + pageData = tenantProfileService.findTenantProfileInfos(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + } + + private TenantProfile createTenantProfile(String name) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName(name); + tenantProfile.setDescription(name + " Test"); + tenantProfile.setProfileData(new TenantProfileData()); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbCore(false); + tenantProfile.setIsolatedTbRuleEngine(false); + return tenantProfile; + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java index 0c1cd3ffe5..e71788258b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -26,6 +27,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public abstract class BaseTenantServiceTest extends AbstractServiceTest { @@ -59,6 +61,17 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertEquals(savedTenant, foundTenant); tenantService.deleteTenant(savedTenant.getId()); } + + @Test + public void testFindTenantInfoById() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + TenantInfo foundTenant = tenantService.findTenantInfoById(savedTenant.getId()); + Assert.assertNotNull(foundTenant); + Assert.assertEquals(new TenantInfo(savedTenant, "Default"), foundTenant); + tenantService.deleteTenant(savedTenant.getId()); + } @Test(expected = DataValidationException.class) public void testSaveTenantWithEmptyTitle() { @@ -116,9 +129,7 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertEquals(tenants, loadedTenants); for (Tenant tenant : loadedTenants) { - if (!tenant.getTitle().equals("Tenant")) { - tenantService.deleteTenant(tenant.getId()); - } + tenantService.deleteTenant(tenant.getId()); } pageLink = new PageLink(17); @@ -200,4 +211,46 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testFindTenantInfos() { + + List tenants = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = tenantService.findTenantInfos(pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + tenants.addAll(pageData.getData()); + + for (int i=0;i<156;i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("Tenant"+i); + tenants.add(new TenantInfo(tenantService.saveTenant(tenant), "Default")); + } + + List loadedTenants = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = tenantService.findTenantInfos(pageLink); + loadedTenants.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenants, idComparator); + Collections.sort(loadedTenants, idComparator); + + Assert.assertEquals(tenants, loadedTenants); + + for (TenantInfo tenant : loadedTenants) { + tenantService.deleteTenant(tenant.getId()); + } + + pageLink = new PageLink(17); + pageData = tenantService.findTenantInfos(pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceProfileServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceProfileServiceSqlTest.java new file mode 100644 index 0000000000..3acf858929 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceProfileServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseDeviceProfileServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class DeviceProfileServiceSqlTest extends BaseDeviceProfileServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantProfileServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantProfileServiceSqlTest.java new file mode 100644 index 0000000000..6caf3242be --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantProfileServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseTenantProfileServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TenantProfileServiceSqlTest extends BaseTenantProfileServiceTest { +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index caec9e0e66..36d73a96ca 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -30,6 +30,12 @@ caffeine.specs.entityViews.maxSize=100000 caffeine.specs.claimDevices.timeToLiveInMinutes=1440 caffeine.specs.claimDevices.maxSize=100000 +caffeine.specs.tenantProfiles.timeToLiveInMinutes=1440 +caffeine.specs.tenantProfiles.maxSize=100000 + +caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 +caffeine.specs.deviceProfiles.maxSize=100000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties index 4cb4cd662a..43a78abac4 100644 --- a/dao/src/test/resources/cassandra-test.properties +++ b/dao/src/test/resources/cassandra-test.properties @@ -6,7 +6,13 @@ cassandra.url=127.0.0.1:9142 cassandra.local_datacenter=datacenter1 -cassandra.ssl=false +cassandra.ssl.enabled=false +cassandra.ssl.hostname_validation=false +cassandra.ssl.trust_store= +cassandra.ssl.trust_store_password= +cassandra.ssl.key_store= +cassandra.ssl.key_store_password= +cassandra.ssl.cipher_suites= cassandra.jmx=false diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index 123c0506cc..058d7ac056 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -13,7 +13,7 @@ spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect spring.datasource.username=sa spring.datasource.password= -spring.datasource.url=jdbc:hsqldb:file:/tmp/testDb;sql.enforce_size=false +spring.datasource.url=jdbc:hsqldb:file:target/tmp/testDb;sql.enforce_size=false spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver spring.datasource.hikari.maximumPoolSize = 50 diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 8480a754e9..c8cc908125 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -18,7 +18,10 @@ DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; +DROP TABLE IF EXISTS entity_view; +DROP TABLE IF EXISTS device_profile; +DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; -DROP TABLE IF EXISTS entity_view; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index b2e4a27963..899a66f510 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -18,7 +18,10 @@ DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; +DROP TABLE IF EXISTS entity_view; +DROP TABLE IF EXISTS device_profile; +DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; -DROP TABLE IF EXISTS entity_view; -DROP TABLE IF EXISTS tb_schema_settings; \ No newline at end of file +DROP TABLE IF EXISTS tb_schema_settings; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index b2e4a27963..4270a2a192 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -18,7 +18,10 @@ DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; +DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; -DROP TABLE IF EXISTS tb_schema_settings; \ No newline at end of file +DROP TABLE IF EXISTS device_profile; +DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS tb_schema_settings; diff --git a/docker/queue-kafka.env b/docker/queue-kafka.env index 63107942fb..0207d64ef5 100644 --- a/docker/queue-kafka.env +++ b/docker/queue-kafka.env @@ -1,2 +1,3 @@ TB_QUEUE_TYPE=kafka TB_KAFKA_SERVERS=kafka:9092 +TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100 diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 21811582a9..74948b0e84 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index 788de04cb1..71ec297204 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -177,6 +177,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); + // Subscribe for attribute update event + mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get(); + ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index fac1ac7e8a..e6ec6f5b36 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -25,7 +25,7 @@ kafka: # Kafka Bootstrap Servers servers: "localhost:9092" replication_factor: "1" - topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600" + topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100" use_confluent_cloud: false confluent: sasl: diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index b0f3af4fe3..1f498922cd 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.1.1", + "version": "3.2.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.js", "bin": "server.js", @@ -19,7 +19,7 @@ "azure-sb": "^0.11.1", "config": "^3.3.1", "js-yaml": "^3.14.0", - "kafkajs": "^1.12.0", + "kafkajs": "^1.14.0", "long": "^4.0.0", "uuid-parse": "^1.1.0", "uuid-random": "^1.3.2", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 0f9c6dac86..ea286c58ba 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/queue/kafkaTemplate.js b/msa/js-executor/queue/kafkaTemplate.js index 0420173188..33dd0d8c20 100644 --- a/msa/js-executor/queue/kafkaTemplate.js +++ b/msa/js-executor/queue/kafkaTemplate.js @@ -27,20 +27,10 @@ let kafkaAdmin; let consumer; let producer; -const topics = []; const configEntries = []; function KafkaProducer() { this.send = async (responseTopic, scriptId, rawResponse, headers) => { - - if (!topics.includes(responseTopic)) { - let createResponseTopicResult = await createTopic(responseTopic); - topics.push(responseTopic); - if (createResponseTopicResult) { - logger.info('Created new topic: %s', requestTopic); - } - } - return producer.send( { topic: responseTopic, @@ -88,10 +78,24 @@ function KafkaProducer() { kafkaAdmin = kafkaClient.admin(); await kafkaAdmin.connect(); - let createRequestTopicResult = await createTopic(requestTopic); + let partitions = 1; - if (createRequestTopicResult) { - logger.info('Created new topic: %s', requestTopic); + for (let i = 0; i < configEntries.length; i++) { + let param = configEntries[i]; + if (param.name === 'partitions') { + partitions = param.value; + configEntries.splice(i, 1); + break; + } + } + + let topics = await kafkaAdmin.listTopics(); + + if (!topics.includes(requestTopic)) { + let createRequestTopicResult = await createTopic(requestTopic, partitions); + if (createRequestTopicResult) { + logger.info('Created new topic: %s', requestTopic); + } } consumer = kafkaClient.consumer({groupId: 'js-executor-group'}); @@ -121,10 +125,11 @@ function KafkaProducer() { } })(); -function createTopic(topic) { +function createTopic(topic, partitions) { return kafkaAdmin.createTopics({ topics: [{ topic: topic, + numPartitions: partitions, replicationFactor: replicationFactor, configEntries: configEntries }] diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index 3a65bce117..a2e65d950d 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -1665,12 +1665,10 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -kafkajs@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.12.0.tgz#50ad336baee95f3324af8ae8df6fadc96e07c613" - integrity sha512-Izkd9iFRgeeKaHEgVpGQH08ygzCbHSxTbnu8W3G3uiNaVjGibUTmTwjv1Qf2M8NORXcPfzwVyg6bBlVj4SKr9g== - dependencies: - long "^4.0.0" +kafkajs@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.14.0.tgz#3d998a77bfde54dc502e8e88690eedf0b21a1ed6" + integrity sha512-W+WCekiooY5rJP3Me5N3gWcQ8O6uG6lw0vv9t+sI+WqXKjKwj2+CWIXJy241x+ITE+1M1D19ABSiL2J8lKja5A== keyv@^3.0.0: version "3.1.0" diff --git a/msa/pom.xml b/msa/pom.xml index bdcc73e441..7419141995 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 282bbf6f97..2d42d8f6dd 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/README.md b/msa/tb/README.md index cffca5ceb7..488ead2d85 100644 --- a/msa/tb/README.md +++ b/msa/tb/README.md @@ -37,7 +37,7 @@ Where: > **NOTE**: **Windows** users should use docker managed volume instead of host's dir. Create docker volume (for ex. `mytb-data`) before executing `docker run` command: > ``` -> $ docker create volume mytb-data +> $ docker volume create mytb-data > ``` > After you can execute docker run command using `mytb-data` volume instead of `~/.mytb-data`. > In order to get access to necessary resources from external IP/Host on **Windows** machine, please execute the following commands: diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index b1d373c231..f41a3afd1e 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 7b3a9d8824..0b3ab420e8 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index cad26dc670..5621fd6bdc 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index ebb25456b7..24f8350706 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index eccfcfb1bf..e000241608 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index a39acf8e0e..d839f97a33 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.1.1", + "version": "3.2.0", "description": "ThingsBoard Web UI Microservice", "main": "server.js", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 7fb6a99896..f9ef249ce1 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 1aa0fc49cf..58524a1a08 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard netty-mqtt - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 4fb089b93b..cc5bbce51b 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT pom Thingsboard @@ -728,6 +728,7 @@ ui/** src/browserslist **/*.raw + **/apache/cassandra/io/** JAVADOC_STYLE diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 1b989fb7b0..a7b97610d1 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard rest-client diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 89b39d2b89..d2ba4d43b4 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -45,7 +45,6 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -56,6 +55,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -73,6 +73,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -890,7 +891,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } - public PageData getCustomerDashboards(CustomerId customerId, TimePageLink pageLink) { + public PageData getCustomerDashboards(CustomerId customerId, PageLink pageLink) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); addPageLinkToParam(params, pageLink); @@ -1629,22 +1630,42 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return RestJsonConverter.toTimeseries(timeseries); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink) { return getTimeseries(entityId, keys, interval, agg, pageLink, true); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink, boolean useStrictDataTypes) { + SortOrder sortOrder = pageLink.getSortOrder(); + return getTimeseries(entityId, keys, interval, agg, sortOrder != null ? sortOrder.getDirection() : null, pageLink.getStartTime(), pageLink.getEndTime(), 100, useStrictDataTypes); + } + + public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("keys", listToString(keys)); params.put("interval", interval == null ? "0" : interval.toString()); params.put("agg", agg == null ? "NONE" : agg.name()); + params.put("limit", limit != null ? limit.toString() : "100"); + params.put("orderBy", sortOrder != null ? sortOrder.name() : "DESC"); params.put("useStrictDataTypes", Boolean.toString(useStrictDataTypes)); - addPageLinkToParam(params, pageLink); + + StringBuilder urlBuilder = new StringBuilder(baseURL); + urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}"); + + if (startTime != null) { + urlBuilder.append("&startTs={startTs}"); + params.put("startTs", String.valueOf(startTime)); + } + if (endTime != null) { + urlBuilder.append("&endTs={endTs}"); + params.put("endTs", String.valueOf(endTime)); + } Map> timeseries = restTemplate.exchange( - baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&" + getUrlParamsTs(pageLink), + urlBuilder.toString(), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @@ -1996,23 +2017,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } private String getTimeUrlParams(TimePageLink pageLink) { - return this.getUrlParams(pageLink); - } - private String getUrlParams(TimePageLink pageLink) { - return getUrlParams(pageLink, "startTime", "endTime"); - } - - private String getUrlParamsTs(TimePageLink pageLink) { - return getUrlParams(pageLink, "startTs", "endTs"); - } - - private String getUrlParams(TimePageLink pageLink, String startTime, String endTime) { String urlParams = "limit={limit}&ascOrder={ascOrder}"; if (pageLink.getStartTime() != null) { - urlParams += "&" + startTime + "={startTime}"; + urlParams += "&startTime={startTime}"; } if (pageLink.getEndTime() != null) { - urlParams += "&" + endTime + "={endTime}"; + urlParams += "&endTime={endTime}"; } return urlParams; } diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 8c3014ac80..197c4e500d 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 011d1a8dbc..1a4c0b8506 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java new file mode 100644 index 0000000000..c398131143 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.api; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +/** + * Created by ashvayka on 02.04.18. + */ +public interface RuleEngineDeviceProfileCache { + + DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId); + + DeviceProfile get(TenantId tenantId, DeviceId deviceId); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index 07cc897d19..ffef48d590 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.api; import com.google.common.util.concurrent.FutureCallback; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -24,7 +23,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.Collection; import java.util.List; -import java.util.Set; /** * Created by ashvayka on 02.04.18. @@ -37,6 +35,8 @@ public interface RuleEngineTelemetryService { void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback); + void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback); + void saveLatestAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback); void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value, FutureCallback callback); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 8894ba222d..fa3c1f67a1 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -25,9 +25,11 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; @@ -183,6 +185,8 @@ public interface TbContext { EntityViewService getEntityViewService(); + RuleEngineDeviceProfileCache getDeviceProfileCache(); + ListeningExecutor getJsExecutor(); ListeningExecutor getMailExecutor(); @@ -212,4 +216,9 @@ public interface TbContext { @Deprecated RedisTemplate getRedisTemplate(); + PageData findRuleNodeStates(PageLink pageLink); + + RuleNodeState findRuleNodeStateForEntity(EntityId entityId); + + RuleNodeState saveRuleNodeState(RuleNodeState state); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 9df49dcd78..c4d2b66d15 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java index b3344105bb..103da4706a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java @@ -25,9 +25,10 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import static org.thingsboard.common.util.DonAsynchron.withCallback; @@ -37,10 +38,6 @@ public abstract class TbAbstractAlarmNode ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } - protected abstract ListenableFuture processAlarm(TbContext ctx, TbMsg msg); + protected abstract ListenableFuture processAlarm(TbContext ctx, TbMsg msg); protected ListenableFuture buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) { try { @@ -91,21 +88,20 @@ public abstract class TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { + protected ListenableFuture processAlarm(TbContext ctx, TbMsg msg) { String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg.getMetaData()); ListenableFuture alarmFuture; if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { @@ -67,11 +67,11 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); return Futures.transformAsync(asyncDetails, details -> { @@ -86,7 +86,7 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { + protected ListenableFuture processAlarm(TbContext ctx, TbMsg msg) { String alarmType; final Alarm msgAlarm; @@ -106,7 +106,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) { + private ListenableFuture createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) { ListenableFuture asyncAlarm; if (msgAlarm != null) { asyncAlarm = Futures.immediateFuture(msgAlarm); @@ -120,10 +120,10 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncCreated = Futures.transform(asyncAlarm, alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); - return Futures.transform(asyncCreated, alarm -> new AlarmResult(true, false, false, alarm), MoreExecutors.directExecutor()); + return Futures.transform(asyncCreated, alarm -> new TbAlarmResult(true, false, false, alarm), MoreExecutors.directExecutor()); } - private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { + private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { ctx.logJsEvalRequest(); ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function) details -> { ctx.logJsEvalResponse(); @@ -141,7 +141,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode new AlarmResult(false, true, false, a), MoreExecutors.directExecutor()); + return Futures.transform(asyncUpdated, a -> new TbAlarmResult(false, true, false, a), MoreExecutors.directExecutor()); } private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java new file mode 100644 index 0000000000..e307505fcd --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -0,0 +1,383 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Data; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule; +import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem; +import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; + +@Data +public class AlarmRuleState { + + private final AlarmSeverity severity; + private final AlarmRule alarmRule; + private final AlarmConditionSpec spec; + private final long requiredDurationInMs; + private final long requiredRepeats; + private PersistedAlarmRuleState state; + private boolean updateFlag; + + public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, PersistedAlarmRuleState state) { + this.severity = severity; + this.alarmRule = alarmRule; + if (state != null) { + this.state = state; + } else { + this.state = new PersistedAlarmRuleState(0L, 0L, 0L); + } + this.spec = getSpec(alarmRule); + long requiredDurationInMs = 0; + long requiredRepeats = 0; + switch (spec.getType()) { + case DURATION: + DurationAlarmConditionSpec duration = (DurationAlarmConditionSpec) spec; + requiredDurationInMs = duration.getUnit().toMillis(duration.getValue()); + break; + case REPEATING: + RepeatingAlarmConditionSpec repeating = (RepeatingAlarmConditionSpec) spec; + requiredRepeats = repeating.getCount(); + break; + } + this.requiredDurationInMs = requiredDurationInMs; + this.requiredRepeats = requiredRepeats; + } + + public AlarmConditionSpec getSpec(AlarmRule alarmRule) { + AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); + if (spec == null) { + spec = new SimpleAlarmConditionSpec(); + } + return spec; + } + + public boolean checkUpdate() { + if (updateFlag) { + updateFlag = false; + return true; + } else { + return false; + } + } + + public boolean eval(DeviceDataSnapshot data) { + boolean active = isActive(data.getTs()); + switch (spec.getType()) { + case SIMPLE: + return active && eval(alarmRule.getCondition(), data); + case DURATION: + return evalDuration(data, active); + case REPEATING: + return evalRepeating(data, active); + default: + return false; + } + } + + private boolean isActive(long eventTs) { + if (eventTs == 0L) { + eventTs = System.currentTimeMillis(); + } + if (alarmRule.getSchedule() == null) { + return true; + } + switch (alarmRule.getSchedule().getType()) { + case ANY_TIME: + return true; + case SPECIFIC_TIME: + return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs); + case CUSTOM: + return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs); + default: + throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType()); + } + } + + private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + if (schedule.getDaysOfWeek().size() != 7) { + int dayOfWeek = zdt.getDayOfWeek().getValue(); + if (!schedule.getDaysOfWeek().contains(dayOfWeek)) { + return false; + } + } + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay; + } + + private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue(); + for (CustomTimeScheduleItem item : schedule.getItems()) { + if (item.getDayOfWeek() == dayOfWeek) { + if (item.isEnabled()) { + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay; + } else { + return false; + } + } + } + return false; + } + + public void clear() { + if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) { + state.setEventCount(0L); + state.setLastEventTs(0L); + state.setDuration(0L); + updateFlag = true; + } + } + + private boolean evalRepeating(DeviceDataSnapshot data, boolean active) { + if (active && eval(alarmRule.getCondition(), data)) { + state.setEventCount(state.getEventCount() + 1); + updateFlag = true; + return state.getEventCount() >= requiredRepeats; + } else { + return false; + } + } + + private boolean evalDuration(DeviceDataSnapshot data, boolean active) { + if (active && eval(alarmRule.getCondition(), data)) { + if (state.getLastEventTs() > 0) { + if (data.getTs() > state.getLastEventTs()) { + state.setDuration(state.getDuration() + (data.getTs() - state.getLastEventTs())); + state.setLastEventTs(data.getTs()); + updateFlag = true; + } + } else { + state.setLastEventTs(data.getTs()); + state.setDuration(0L); + updateFlag = true; + } + return state.getDuration() > requiredDurationInMs; + } else { + return false; + } + } + + public boolean eval(long ts) { + switch (spec.getType()) { + case SIMPLE: + case REPEATING: + return false; + case DURATION: + if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { + long duration = state.getDuration() + (ts - state.getLastEventTs()); + return duration > requiredDurationInMs && isActive(ts); + } + default: + return false; + } + } + + private boolean eval(AlarmCondition condition, DeviceDataSnapshot data) { + boolean eval = true; + for (KeyFilter keyFilter : condition.getCondition()) { + EntityKeyValue value = data.getValue(keyFilter.getKey()); + if (value == null) { + return false; + } + eval = eval && eval(value, keyFilter.getPredicate()); + } + return eval; + } + + private boolean eval(EntityKeyValue value, KeyFilterPredicate predicate) { + switch (predicate.getType()) { + case STRING: + return evalStrPredicate(value, (StringFilterPredicate) predicate); + case NUMERIC: + return evalNumPredicate(value, (NumericFilterPredicate) predicate); + case COMPLEX: + return evalComplexPredicate(value, (ComplexFilterPredicate) predicate); + case BOOLEAN: + return evalBoolPredicate(value, (BooleanFilterPredicate) predicate); + default: + return false; + } + } + + private boolean evalComplexPredicate(EntityKeyValue ekv, ComplexFilterPredicate predicate) { + switch (predicate.getOperation()) { + case OR: + for (KeyFilterPredicate kfp : predicate.getPredicates()) { + if (eval(ekv, kfp)) { + return true; + } + } + return false; + case AND: + for (KeyFilterPredicate kfp : predicate.getPredicates()) { + if (!eval(ekv, kfp)) { + return false; + } + } + return true; + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private boolean evalBoolPredicate(EntityKeyValue ekv, BooleanFilterPredicate predicate) { + Boolean value; + switch (ekv.getDataType()) { + case LONG: + value = ekv.getLngValue() > 0; + break; + case DOUBLE: + value = ekv.getDblValue() > 0; + break; + case BOOLEAN: + value = ekv.getBoolValue(); + break; + case STRING: + try { + value = Boolean.parseBoolean(ekv.getStrValue()); + break; + } catch (RuntimeException e) { + return false; + } + case JSON: + try { + value = Boolean.parseBoolean(ekv.getJsonValue()); + break; + } catch (RuntimeException e) { + return false; + } + default: + return false; + } + if (value == null) { + return false; + } + switch (predicate.getOperation()) { + case EQUAL: + return value.equals(predicate.getValue().getDefaultValue()); + case NOT_EQUAL: + return !value.equals(predicate.getValue().getDefaultValue()); + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private boolean evalNumPredicate(EntityKeyValue ekv, NumericFilterPredicate predicate) { + Double value; + switch (ekv.getDataType()) { + case LONG: + value = ekv.getLngValue().doubleValue(); + break; + case DOUBLE: + value = ekv.getDblValue(); + break; + case BOOLEAN: + value = ekv.getBoolValue() ? 1.0 : 0.0; + break; + case STRING: + try { + value = Double.parseDouble(ekv.getStrValue()); + break; + } catch (RuntimeException e) { + return false; + } + case JSON: + try { + value = Double.parseDouble(ekv.getJsonValue()); + break; + } catch (RuntimeException e) { + return false; + } + default: + return false; + } + if (value == null) { + return false; + } + + Double predicateValue = predicate.getValue().getDefaultValue(); + switch (predicate.getOperation()) { + case NOT_EQUAL: + return !value.equals(predicateValue); + case EQUAL: + return value.equals(predicateValue); + case GREATER: + return value > predicateValue; + case GREATER_OR_EQUAL: + return value >= predicateValue; + case LESS: + return value < predicateValue; + case LESS_OR_EQUAL: + return value <= predicateValue; + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private boolean evalStrPredicate(EntityKeyValue ekv, StringFilterPredicate predicate) { + String val; + String predicateValue; + if (predicate.isIgnoreCase()) { + val = ekv.getStrValue().toLowerCase(); + predicateValue = predicate.getValue().getDefaultValue().toLowerCase(); + } else { + val = ekv.getStrValue(); + predicateValue = predicate.getValue().getDefaultValue(); + } + switch (predicate.getOperation()) { + case CONTAINS: + return val.contains(predicateValue); + case EQUAL: + return val.equals(predicateValue); + case STARTS_WITH: + return val.startsWith(predicateValue); + case ENDS_WITH: + return val.endsWith(predicateValue); + case NOT_EQUAL: + return !val.equals(predicateValue); + case NOT_CONTAINS: + return !val.contains(predicateValue); + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java new file mode 100644 index 0000000000..de9708dc58 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +public enum AlarmStateUpdateResult { + + NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java new file mode 100644 index 0000000000..f1b1067095 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DeviceDataSnapshot { + + private volatile boolean ready; + @Getter + @Setter + private long ts; + private final Set keys; + private final Map values = new ConcurrentHashMap<>(); + + public DeviceDataSnapshot(Set entityKeysToFetch) { + this.keys = entityKeysToFetch; + } + + void removeValue(EntityKey key) { + switch (key.getType()) { + case ATTRIBUTE: + values.remove(key); + values.remove(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE)); + values.remove(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE)); + values.remove(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE)); + break; + case CLIENT_ATTRIBUTE: + case SHARED_ATTRIBUTE: + case SERVER_ATTRIBUTE: + values.remove(key); + values.remove(getAttrKey(key, EntityKeyType.ATTRIBUTE)); + break; + default: + values.remove(key); + } + } + + void putValue(EntityKey key, EntityKeyValue value) { + switch (key.getType()) { + case ATTRIBUTE: + putIfKeyExists(key, value); + putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value); + putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value); + putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value); + break; + case CLIENT_ATTRIBUTE: + case SHARED_ATTRIBUTE: + case SERVER_ATTRIBUTE: + putIfKeyExists(key, value); + putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value); + break; + default: + putIfKeyExists(key, value); + } + } + + private void putIfKeyExists(EntityKey key, EntityKeyValue value) { + if (keys.contains(key)) { + values.put(key, value); + } + } + + EntityKeyValue getValue(EntityKey key) { + if (EntityKeyType.ATTRIBUTE.equals(key.getType())) { + EntityKeyValue value = values.get(key); + if (value == null) { + value = values.get(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE)); + if (value == null) { + value = values.get(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE)); + if (value == null) { + value = values.get(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE)); + } + } + } + return value; + } else { + return values.get(key); + } + } + + private EntityKey getAttrKey(EntityKey key, EntityKeyType clientAttribute) { + return new EntityKey(clientAttribute, key.getKey()); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java new file mode 100644 index 0000000000..f74d88fe62 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java @@ -0,0 +1,192 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceQueue; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.BiFunction; + +@Data +class DeviceProfileAlarmState { + + private final EntityId originator; + private DeviceProfileAlarm alarmDefinition; + private volatile List createRulesSortedBySeverityDesc; + private volatile AlarmRuleState clearState; + private volatile Alarm currentAlarm; + private volatile boolean initialFetchDone; + private volatile TbMsgMetaData lastMsgMetaData; + private volatile String lastMsgQueueName; + + public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) { + this.originator = originator; + this.updateState(alarmDefinition, alarmState); + } + + public boolean process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) throws ExecutionException, InterruptedException { + initCurrentAlarm(ctx); + lastMsgMetaData = msg.getMetaData(); + lastMsgQueueName = msg.getQueueName(); + return createOrClearAlarms(ctx, data, AlarmRuleState::eval); + } + + public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException { + initCurrentAlarm(ctx); + return createOrClearAlarms(ctx, ts, AlarmRuleState::eval); + } + + public boolean createOrClearAlarms(TbContext ctx, T data, BiFunction evalFunction) { + boolean stateUpdate = false; + AlarmSeverity resultSeverity = null; + for (AlarmRuleState state : createRulesSortedBySeverityDesc) { + boolean evalResult = evalFunction.apply(state, data); + stateUpdate |= state.checkUpdate(); + if (evalResult) { + resultSeverity = state.getSeverity(); + break; + } + } + if (resultSeverity != null) { + pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity)); + } else if (currentAlarm != null && clearState != null) { + Boolean evalResult = evalFunction.apply(clearState, data); + if (evalResult) { + stateUpdate |= clearState.checkUpdate(); + ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); + pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); + currentAlarm = null; + } + } + return stateUpdate; + } + + public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { + if (!initialFetchDone) { + Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); + if (alarm != null && !alarm.getStatus().isCleared()) { + currentAlarm = alarm; + } + initialFetchDone = true; + } + } + + public void pushMsg(TbContext ctx, TbAlarmResult alarmResult) { + JsonNode jsonNodes = JacksonUtil.valueToTree(alarmResult.getAlarm()); + String data = jsonNodes.toString(); + TbMsgMetaData metaData = lastMsgMetaData != null ? lastMsgMetaData.copy() : new TbMsgMetaData(); + String relationType; + if (alarmResult.isCreated()) { + relationType = "Alarm Created"; + metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isUpdated()) { + relationType = "Alarm Updated"; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isSeverityUpdated()) { + relationType = "Alarm Severity Updated"; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString()); + } else { + relationType = "Alarm Cleared"; + metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); + } + TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", originator, metaData, data); + ctx.tellNext(newMsg, relationType); + } + + public void updateState(DeviceProfileAlarm alarm, PersistedAlarmState alarmState) { + this.alarmDefinition = alarm; + this.createRulesSortedBySeverityDesc = new ArrayList<>(); + alarmDefinition.getCreateRules().forEach((severity, rule) -> { + PersistedAlarmRuleState ruleState = null; + if (alarmState != null) { + ruleState = alarmState.getCreateRuleStates().get(severity); + if (ruleState == null) { + ruleState = new PersistedAlarmRuleState(); + alarmState.getCreateRuleStates().put(severity, ruleState); + } + } + createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule, ruleState)); + }); + createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal())); + PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); + if (alarmDefinition.getClearRule() != null) { + clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), ruleState); + } + } + + private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmSeverity severity) { + if (currentAlarm != null) { + currentAlarm.setEndTs(System.currentTimeMillis()); + AlarmSeverity oldSeverity = currentAlarm.getSeverity(); + if (!oldSeverity.equals(severity)) { + currentAlarm.setSeverity(severity); + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + return new TbAlarmResult(false, false, true, false, currentAlarm); + } else { + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + return new TbAlarmResult(false, true, false, false, currentAlarm); + } + } else { + currentAlarm = new Alarm(); + currentAlarm.setType(alarmDefinition.getAlarmType()); + currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); + currentAlarm.setSeverity(severity); + currentAlarm.setStartTs(System.currentTimeMillis()); + currentAlarm.setEndTs(currentAlarm.getStartTs()); + currentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.createObjectNode()); + currentAlarm.setOriginator(originator); + currentAlarm.setTenantId(ctx.getTenantId()); + currentAlarm.setPropagate(alarmDefinition.isPropagate()); + if (alarmDefinition.getPropagateRelationTypes() != null) { + currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); + } + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + boolean updated = currentAlarm.getStartTs() != currentAlarm.getEndTs(); + return new TbAlarmResult(!updated, updated, false, false, currentAlarm); + } + } + + public boolean processAlarmClear(TbContext ctx, Alarm alarmNf) { + boolean updated = false; + if (currentAlarm != null && currentAlarm.getId().equals(alarmNf.getId())) { + currentAlarm = null; + for (AlarmRuleState state : createRulesSortedBySeverityDesc) { + state.clear(); + updated |= state.checkUpdate(); + } + } + return updated; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java new file mode 100644 index 0000000000..fd9037624e --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.AccessLevel; +import lombok.Getter; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.KeyFilter; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + + +class DeviceProfileState { + + private DeviceProfile deviceProfile; + @Getter(AccessLevel.PACKAGE) + private final List alarmSettings = new CopyOnWriteArrayList<>(); + @Getter(AccessLevel.PACKAGE) + private final Set entityKeys = ConcurrentHashMap.newKeySet(); + + DeviceProfileState(DeviceProfile deviceProfile) { + updateDeviceProfile(deviceProfile); + } + + void updateDeviceProfile(DeviceProfile deviceProfile) { + this.deviceProfile = deviceProfile; + alarmSettings.clear(); + if (deviceProfile.getProfileData().getAlarms() != null) { + alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); + for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { + for (AlarmRule alarmRule : alarm.getCreateRules().values()) { + for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { + entityKeys.add(keyFilter.getKey()); + } + } + } + } + } + + public DeviceProfileId getProfileId() { + return deviceProfile.getId(); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java new file mode 100644 index 0000000000..078be24f90 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -0,0 +1,380 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.google.gson.JsonParser; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; +import org.thingsboard.rule.engine.profile.state.PersistedDeviceState; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeStateId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.dao.sql.query.EntityKeyMapping; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +class DeviceState { + + private final boolean persistState; + private final DeviceId deviceId; + private RuleNodeState state; + private DeviceProfileState deviceProfile; + private PersistedDeviceState pds; + private DeviceDataSnapshot latestValues; + private final ConcurrentMap alarmStates = new ConcurrentHashMap<>(); + + public DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, DeviceProfileState deviceProfile, RuleNodeState state) { + this.persistState = config.isPersistAlarmRulesState(); + this.deviceId = deviceId; + this.deviceProfile = deviceProfile; + if (config.isPersistAlarmRulesState()) { + if (state != null) { + this.state = state; + } else { + this.state = ctx.findRuleNodeStateForEntity(deviceId); + } + if (this.state != null) { + pds = JacksonUtil.fromString(this.state.getStateData(), PersistedDeviceState.class); + } else { + this.state = new RuleNodeState(); + this.state.setRuleNodeId(ctx.getSelfId()); + this.state.setEntityId(deviceId); + pds = new PersistedDeviceState(); + pds.setAlarmStates(new HashMap<>()); + } + } + if (pds != null) { + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + alarmStates.computeIfAbsent(alarm.getId(), + a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + } + } + } + + public void updateProfile(TbContext ctx, DeviceProfile deviceProfile) throws ExecutionException, InterruptedException { + Set oldKeys = this.deviceProfile.getEntityKeys(); + this.deviceProfile.updateDeviceProfile(deviceProfile); + if (latestValues != null) { + Set keysToFetch = new HashSet<>(this.deviceProfile.getEntityKeys()); + keysToFetch.removeAll(oldKeys); + if (!keysToFetch.isEmpty()) { + addEntityKeysToSnapshot(ctx, deviceId, keysToFetch, latestValues); + } + } + Set newAlarmStateIds = this.deviceProfile.getAlarmSettings().stream().map(DeviceProfileAlarm::getId).collect(Collectors.toSet()); + alarmStates.keySet().removeIf(id -> !newAlarmStateIds.contains(id)); + for (DeviceProfileAlarm alarm : this.deviceProfile.getAlarmSettings()) { + if (alarmStates.containsKey(alarm.getId())) { + alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); + } else { + alarmStates.putIfAbsent(alarm.getId(), new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + } + } + } + + public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { + for (DeviceProfileAlarmState state : alarmStates.values()) { + state.process(ctx, ts); + } + } + + public void process(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + if (latestValues == null) { + latestValues = fetchLatestValues(ctx, deviceId); + } + boolean stateChanged = false; + if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { + stateChanged = processTelemetry(ctx, msg); + } else if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { + stateChanged = processAttributesUpdateRequest(ctx, msg); + } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { + stateChanged = processAttributesUpdateNotification(ctx, msg); + } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { + stateChanged = processAttributesDeleteNotification(ctx, msg); + } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { + stateChanged = processAlarmClearNotification(ctx, msg); + } else { + ctx.tellSuccess(msg); + } + if (persistState && stateChanged) { + state.setStateData(JacksonUtil.toString(pds)); + state = ctx.saveRuleNodeState(state); + } + } + + private boolean processAlarmClearNotification(TbContext ctx, TbMsg msg) { + boolean stateChanged = false; + Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); + } + ctx.tellSuccess(msg); + return stateChanged; + } + + private boolean processAttributesUpdateNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + String scope = msg.getMetaData().getValue("scope"); + if (StringUtils.isEmpty(scope)) { + scope = DataConstants.CLIENT_SCOPE; + } + return processAttributesUpdate(ctx, msg, attributes, scope); + } + + private boolean processAttributesDeleteNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + boolean stateChanged = false; + List keys = new ArrayList<>(); + new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); + String scope = msg.getMetaData().getValue("scope"); + if (StringUtils.isEmpty(scope)) { + scope = DataConstants.CLIENT_SCOPE; + } + if (!keys.isEmpty()) { + EntityKeyType keyType = getKeyTypeFromScope(scope); + keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues); + } + } + ctx.tellSuccess(msg); + return stateChanged; + } + + protected boolean processAttributesUpdateRequest(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + return processAttributesUpdate(ctx, msg, attributes, DataConstants.CLIENT_SCOPE); + } + + private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set attributes, String scope) throws ExecutionException, InterruptedException { + boolean stateChanged = false; + if (!attributes.isEmpty()) { + merge(latestValues, attributes, scope); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues); + } + } + ctx.tellSuccess(msg); + return stateChanged; + } + + protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + boolean stateChanged = false; + Map> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); + for (Map.Entry> entry : tsKvMap.entrySet()) { + Long ts = entry.getKey(); + List data = entry.getValue(); + merge(latestValues, ts, data); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues); + } + } + ctx.tellSuccess(msg); + return stateChanged; + } + + private void merge(DeviceDataSnapshot latestValues, Long ts, List data) { + latestValues.setTs(ts); + for (KvEntry entry : data) { + latestValues.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); + } + } + + private void merge(DeviceDataSnapshot latestValues, Set attributes, String scope) { + long ts = latestValues.getTs(); + for (AttributeKvEntry entry : attributes) { + ts = Math.max(ts, entry.getLastUpdateTs()); + latestValues.putValue(new EntityKey(getKeyTypeFromScope(scope), entry.getKey()), toEntityValue(entry)); + } + latestValues.setTs(ts); + } + + private static EntityKeyType getKeyTypeFromScope(String scope) { + switch (scope) { + case DataConstants.CLIENT_SCOPE: + return EntityKeyType.CLIENT_ATTRIBUTE; + case DataConstants.SHARED_SCOPE: + return EntityKeyType.SHARED_ATTRIBUTE; + case DataConstants.SERVER_SCOPE: + return EntityKeyType.SERVER_ATTRIBUTE; + } + return EntityKeyType.ATTRIBUTE; + } + + private DeviceDataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { + Set entityKeysToFetch = deviceProfile.getEntityKeys(); + DeviceDataSnapshot result = new DeviceDataSnapshot(entityKeysToFetch); + addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); + return result; + } + + private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set entityKeysToFetch, DeviceDataSnapshot result) throws InterruptedException, ExecutionException { + Set serverAttributeKeys = new HashSet<>(); + Set clientAttributeKeys = new HashSet<>(); + Set sharedAttributeKeys = new HashSet<>(); + Set commonAttributeKeys = new HashSet<>(); + Set latestTsKeys = new HashSet<>(); + + Device device = null; + for (EntityKey entityKey : entityKeysToFetch) { + String key = entityKey.getKey(); + switch (entityKey.getType()) { + case SERVER_ATTRIBUTE: + serverAttributeKeys.add(key); + break; + case CLIENT_ATTRIBUTE: + clientAttributeKeys.add(key); + break; + case SHARED_ATTRIBUTE: + sharedAttributeKeys.add(key); + break; + case ATTRIBUTE: + serverAttributeKeys.add(key); + clientAttributeKeys.add(key); + sharedAttributeKeys.add(key); + commonAttributeKeys.add(key); + break; + case TIME_SERIES: + latestTsKeys.add(key); + break; + case ENTITY_FIELD: + if (device == null) { + device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(originator.getId())); + } + if (device != null) { + switch (key) { + case EntityKeyMapping.NAME: + result.putValue(entityKey, EntityKeyValue.fromString(device.getName())); + break; + case EntityKeyMapping.TYPE: + result.putValue(entityKey, EntityKeyValue.fromString(device.getType())); + break; + case EntityKeyMapping.CREATED_TIME: + result.putValue(entityKey, EntityKeyValue.fromLong(device.getCreatedTime())); + break; + case EntityKeyMapping.LABEL: + result.putValue(entityKey, EntityKeyValue.fromString(device.getLabel())); + break; + } + } + break; + } + } + + if (!latestTsKeys.isEmpty()) { + List data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); + for (TsKvEntry entry : data) { + if (entry.getValue() != null) { + result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); + } + } + } + if (!clientAttributeKeys.isEmpty()) { + addToSnapshot(result, commonAttributeKeys, + ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.CLIENT_SCOPE, clientAttributeKeys).get()); + } + if (!sharedAttributeKeys.isEmpty()) { + addToSnapshot(result, commonAttributeKeys, + ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SHARED_SCOPE, sharedAttributeKeys).get()); + } + if (!serverAttributeKeys.isEmpty()) { + addToSnapshot(result, commonAttributeKeys, + ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SERVER_SCOPE, serverAttributeKeys).get()); + } + } + + private void addToSnapshot(DeviceDataSnapshot snapshot, Set commonAttributeKeys, List data) { + for (AttributeKvEntry entry : data) { + if (entry.getValue() != null) { + EntityKeyValue value = toEntityValue(entry); + snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), value); + if (commonAttributeKeys.contains(entry.getKey())) { + snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); + } + } + } + } + + private EntityKeyValue toEntityValue(KvEntry entry) { + switch (entry.getDataType()) { + case STRING: + return EntityKeyValue.fromString(entry.getStrValue().get()); + case LONG: + return EntityKeyValue.fromLong(entry.getLongValue().get()); + case DOUBLE: + return EntityKeyValue.fromDouble(entry.getDoubleValue().get()); + case BOOLEAN: + return EntityKeyValue.fromBool(entry.getBooleanValue().get()); + case JSON: + return EntityKeyValue.fromJson(entry.getJsonValue().get()); + default: + throw new RuntimeException("Can't parse entry: " + entry.getDataType()); + } + } + + public DeviceProfileId getProfileId() { + return deviceProfile.getProfileId(); + } + + private PersistedAlarmState getOrInitPersistedAlarmState(DeviceProfileAlarm alarm) { + if (pds != null) { + PersistedAlarmState alarmState = pds.getAlarmStates().get(alarm.getId()); + if (alarmState == null) { + alarmState = new PersistedAlarmState(); + alarmState.setCreateRuleStates(new HashMap<>()); + pds.getAlarmStates().put(alarm.getId(), alarmState); + } + return alarmState; + } else { + return null; + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java new file mode 100644 index 0000000000..08929bd2a2 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +public class EntityKeyState { + + + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java new file mode 100644 index 0000000000..40ca323307 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; + +class EntityKeyValue { + + @Getter + private DataType dataType; + private Long lngValue; + private Double dblValue; + private Boolean boolValue; + private String strValue; + + public Long getLngValue() { + return dataType == DataType.LONG ? lngValue : null; + } + + public void setLngValue(Long lngValue) { + this.dataType = DataType.LONG; + this.lngValue = lngValue; + } + + public Double getDblValue() { + return dataType == DataType.DOUBLE ? dblValue : null; + } + + public void setDblValue(Double dblValue) { + this.dataType = DataType.DOUBLE; + this.dblValue = dblValue; + } + + public Boolean getBoolValue() { + return dataType == DataType.BOOLEAN ? boolValue : null; + } + + public void setBoolValue(Boolean boolValue) { + this.dataType = DataType.BOOLEAN; + this.boolValue = boolValue; + } + + public String getStrValue() { + return dataType == DataType.STRING ? strValue : null; + } + + public void setStrValue(String strValue) { + this.dataType = DataType.STRING; + this.strValue = strValue; + } + + public void setJsonValue(String jsonValue) { + this.dataType = DataType.JSON; + this.strValue = jsonValue; + } + + public String getJsonValue() { + return dataType == DataType.JSON ? strValue : null; + } + + boolean isSet() { + return dataType != null; + } + + static EntityKeyValue fromString(String s) { + EntityKeyValue result = new EntityKeyValue(); + result.setStrValue(s); + return result; + } + + static EntityKeyValue fromBool(boolean b) { + EntityKeyValue result = new EntityKeyValue(); + result.setBoolValue(b); + return result; + } + + static EntityKeyValue fromLong(long l) { + EntityKeyValue result = new EntityKeyValue(); + result.setLngValue(l); + return result; + } + + static EntityKeyValue fromDouble(double d) { + EntityKeyValue result = new EntityKeyValue(); + result.setDblValue(d); + return result; + } + + static EntityKeyValue fromJson(String s) { + EntityKeyValue result = new EntityKeyValue(); + result.setJsonValue(s); + return result; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java new file mode 100644 index 0000000000..54a0fa7085 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.profile.state.PersistedDeviceState; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "device profile", + customRelations = true, + relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, + configClazz = TbDeviceProfileNodeConfiguration.class, + nodeDescription = "Process device messages based on device profile settings", + nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. Generates ", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbDeviceProfileConfig" +) +public class TbDeviceProfileNode implements TbNode { + private static final String PERIODIC_MSG_TYPE = "TbDeviceProfilePeriodicMsg"; + + private TbDeviceProfileNodeConfiguration config; + private RuleEngineDeviceProfileCache cache; + private final Map deviceStates = new ConcurrentHashMap<>(); + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, TbDeviceProfileNodeConfiguration.class); + this.cache = ctx.getDeviceProfileCache(); + scheduleAlarmHarvesting(ctx); + if (config.isFetchAlarmRulesStateOnStart()) { + PageLink pageLink = new PageLink(1024); + while (true) { + PageData states = ctx.findRuleNodeStates(pageLink); + if (!states.getData().isEmpty()) { + for (RuleNodeState rns : states.getData()) { + if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { + getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); + } + } + } + if (!states.hasNext()) { + break; + } else { + pageLink = pageLink.nextPageLink(); + } + } + } + } + + /** + * TODO: Dynamic values evaluation; + */ + @Override + public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + EntityType originatorType = msg.getOriginator().getEntityType(); + if (msg.getType().equals(PERIODIC_MSG_TYPE)) { + scheduleAlarmHarvesting(ctx); + harvestAlarms(ctx, System.currentTimeMillis()); + } else { + if (EntityType.DEVICE.equals(originatorType)) { + DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); + if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) { + invalidateDeviceProfileCache(deviceId, msg.getData()); + } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) { + deviceStates.remove(deviceId); + } else { + DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null); + if (deviceState != null) { + deviceState.process(ctx, msg); + } else { + ctx.tellFailure(msg, new IllegalStateException("Device profile for device [" + deviceId + "] not found!")); + } + } + } else if (EntityType.DEVICE_PROFILE.equals(originatorType)) { + if (msg.getType().equals("ENTITY_UPDATED")) { + DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class); + for (DeviceState state : deviceStates.values()) { + if (deviceProfile.getId().equals(state.getProfileId())) { + state.updateProfile(ctx, deviceProfile); + } + } + } + ctx.tellSuccess(msg); + } else { + ctx.tellSuccess(msg); + } + } + } + + public void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) { + DeviceState deviceState = deviceStates.get(deviceId); + if (deviceState != null) { + DeviceProfileId currentProfileId = deviceState.getProfileId(); + Device device = JacksonUtil.fromString(deviceJson, Device.class); + if (!currentProfileId.equals(device.getDeviceProfileId())) { + deviceStates.remove(deviceId); + } + } + } + + @Override + public void destroy() { + deviceStates.clear(); + } + + protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns) { + DeviceState deviceState = deviceStates.get(deviceId); + if (deviceState == null) { + DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); + if (deviceProfile != null) { + deviceState = new DeviceState(ctx, config, deviceId, new DeviceProfileState(deviceProfile), rns); + deviceStates.put(deviceId, deviceState); + } + } + return deviceState; + } + + protected void scheduleAlarmHarvesting(TbContext ctx) { + TbMsg periodicCheck = TbMsg.newMsg(PERIODIC_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, "{}"); + ctx.tellSelf(periodicCheck, TimeUnit.MINUTES.toMillis(1)); + } + + protected void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { + for (DeviceState state : deviceStates.values()) { + state.harvestAlarms(ctx, ts); + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java new file mode 100644 index 0000000000..0b32893c90 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class TbDeviceProfileNodeConfiguration implements NodeConfiguration { + + private boolean persistAlarmRulesState; + private boolean fetchAlarmRulesStateOnStart; + + @Override + public TbDeviceProfileNodeConfiguration defaultConfiguration() { + return new TbDeviceProfileNodeConfiguration(); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java new file mode 100644 index 0000000000..57bc424874 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile.state; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PersistedAlarmRuleState { + + private long lastEventTs; + private long duration; + private long eventCount; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java new file mode 100644 index 0000000000..b8a657c4bb --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile.state; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; + +import java.util.Map; + +@Data +public class PersistedAlarmState { + + private Map createRuleStates; + private PersistedAlarmRuleState clearRuleState; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java new file mode 100644 index 0000000000..c0acfc0768 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile.state; + +import lombok.Data; + +import java.util.Map; + +@Data +public class PersistedDeviceState { + + Map alarmStates; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index a0970238d9..72c0686fee 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -17,15 +17,13 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -53,6 +51,9 @@ public class TbMsgAttributesNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbMsgAttributesNodeConfiguration.class); + if (config.getNotifyDevice() == null) { + config.setNotifyDevice(true); + } } @Override @@ -63,7 +64,15 @@ public class TbMsgAttributesNode implements TbNode { } String src = msg.getData(); Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(src)); - ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(), config.getScope(), new ArrayList<>(attributes), new TelemetryNodeCallback(ctx, msg)); + String notifyDeviceStr = msg.getMetaData().getValue("notifyDevice"); + ctx.getTelemetryService().saveAndNotify( + ctx.getTenantId(), + msg.getOriginator(), + config.getScope(), + new ArrayList<>(attributes), + config.getNotifyDevice() || StringUtils.isEmpty(notifyDeviceStr) || Boolean.parseBoolean(notifyDeviceStr), + new TelemetryNodeCallback(ctx, msg) + ); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java index 4fbb82057e..5bc79d6314 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java @@ -24,10 +24,13 @@ public class TbMsgAttributesNodeConfiguration implements NodeConfiguration> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { @@ -89,6 +80,20 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(), tsKvEntryList, ttl, new TelemetryNodeCallback(ctx, msg)); } + public static long getTs(TbMsg msg) { + long ts = -1; + String tsStr = msg.getMetaData().getValue("ts"); + if (!StringUtils.isEmpty(tsStr)) { + try { + ts = Long.parseLong(tsStr); + } catch (NumberFormatException e) { + } + } else { + ts = msg.getTs(); + } + return ts; + } + @Override public void destroy() { } diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index 2b0ec5c862..1efc621033 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -12,5 +12,5 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */var g=function(e,t){return(g=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function y(e,t){function r(){this.constructor=e}g(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function b(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function h(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function C(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var v,F=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-node-empty-config",template:"
"}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],i}return y(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]]})},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(v||(v={}));var M,P=new Map([[v.CUSTOMER,"tb.rulenode.originator-customer"],[v.TENANT,"tb.rulenode.originator-tenant"],[v.RELATED,"tb.rulenode.originator-related"],[v.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,w=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,D=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var K,B=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(K||(K={}));var U,j,H,G=new Map([[K.TITLE,"tb.rulenode.entity-details-title"],[K.COUNTRY,"tb.rulenode.entity-details-country"],[K.STATE,"tb.rulenode.entity-details-state"],[K.ZIP,"tb.rulenode.entity-details-zip"],[K.ADDRESS,"tb.rulenode.entity-details-address"],[K.ADDRESS2,"tb.rulenode.entity-details-address2"],[K.PHONE,"tb.rulenode.entity-details-phone"],[K.EMAIL,"tb.rulenode.entity-details-email"],[K.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(U||(U={})),function(e){e.ASC="ASC",e.DESC="DESC"}(j||(j={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(H||(H={}));var z,$=new Map([[H.STANDARD,"tb.rulenode.sqs-queue-standard"],[H.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Q=["anonymous","basic","cert.PEM"],_=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),W=["sas","cert.PEM"],J=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(z||(z={}));var Y=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],Z=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),X=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return y(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=C(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",String)],r.prototype,"requiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyRequiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valRequiredText",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=b([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText }}\n {{ valText }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),h("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r,n,o){var i,l,m=e.call(this,t)||this;m.store=t,m.translate=r,m.truncate=n,m.fb=o,m.placeholder="tb.rulenode.message-type",m.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],m.messageTypes=[],m.messageTypesList=[],m.searchText="",m.propagateChange=function(e){},m.messageTypeConfigForm=m.fb.group({messageType:[null]});try{for(var u=C(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;m.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return m}var l;return y(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),b([t.Input(),h("design:type",String)],r.prototype,"label",void 0),b([t.Input(),h("design:type",Object)],r.prototype,"placeholder",void 0),b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.ViewChild("chipList",{static:!1}),h("design:type",d.MatChipList)],r.prototype,"chipList",void 0),b([t.ViewChild("messageTypeAutocomplete",{static:!1}),h("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),b([t.ViewChild("messageTypeInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=b([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),h("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),le=function(){function e(){}return e=b([t.NgModule({declarations:[ne,ae,oe,ie],imports:[r.CommonModule,a.SharedModule,m.HomeComponentsModule],exports:[ne,ae,oe,ie]})],e)}(),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=H,n.sqsQueueTypes=Object.keys(H),n.sqsQueueTypeTranslationsMap=$,n}return y(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Y,n.ToByteStandartCharsetTypeTranslationMap=Z,n}return y(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allMqttCredentialsTypes=Q,n.mqttCredentialsTypeTranslationsMap=_,n}return y(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],username:[e&&e.credentials?e.credentials.username:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;switch(t){case"anonymous":e.credentials={type:t};break;case"basic":e.credentials={type:t,username:e.credentials.username,password:e.credentials.password};break;case"cert.PEM":delete e.credentials.username}return e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.mqttConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("username").setValidators([]),t.get("password").setValidators([]),t.get("caCert").setValidators([]),t.get("caCertFileName").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"anonymous":break;case"basic":t.get("username").setValidators([i.Validators.required]),t.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("caCert").setValidators([i.Validators.required]),t.get("caCertFileName").setValidators([i.Validators.required]),t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("username").updateValueAndValidity({emitEvent:e}),t.get("password").updateValueAndValidity({emitEvent:e}),t.get("caCert").updateValueAndValidity({emitEvent:e}),t.get("caCertFileName").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n \n tb.rulenode.credentials\n \n {{ mqttCredentialsTypeTranslationsMap.get(mqttConfigForm.get(\'credentials\').get(\'type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ mqttCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return y(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(z),n}return y(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n \n \n \n tb.rulenode.max-parallel-requests-count\n \n \n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return y(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return y(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
tb.rulenode.select-queue-hint
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=W,n.azureIotHubCredentialsTypeTranslationsMap=J,n}return y(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(){function e(){}return e=b([t.NgModule({declarations:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he],imports:[r.CommonModule,a.SharedModule,le],exports:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he]})],e)}(),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n}return y(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return y(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Se=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=C(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("alarmStatusInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=b([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ne=function(){function e(){}return e=b([t.NgModule({declarations:[ve,Fe,xe,Te,qe,Se,Ie,ke],imports:[r.CommonModule,a.SharedModule,le],exports:[ve,Fe,xe,Te,qe,Se,Ie,ke]})],e)}(),Ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=G,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=C(Object.keys(K)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(K[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(G.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(G.get(K[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("detailsInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=b([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n.fetchMode=U,n.fetchModes=Object.keys(U),n.samplingOrders=Object.keys(j),n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===U.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(){function e(){}return e=b([t.NgModule({declarations:[Ve,Ee,Ae,Le,Me,Pe,Re,we],imports:[r.CommonModule,a.SharedModule,le],exports:[Ve,Ee,Ae,Le,Me,Pe,Re,we]})],e)}(),De=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=v,n.originatorSources=Object.keys(v),n.originatorSourceTranslationMap=P,n}return y(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===v.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(){function e(){}return e=b([t.NgModule({declarations:[De,Ke,Be],imports:[r.CommonModule,a.SharedModule,le],exports:[De,Ke,Be]})],e)}(),je=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","client-attributes-hint":"Client attributes, use ${metaKeyName} to substitute variables from metadata","shared-attributes":"Shared attributes","shared-attributes-hint":"Shared attributes, use ${metaKeyName} to substitute variables from metadata","server-attributes":"Server attributes","server-attributes-hint":"Server attributes, use ${metaKeyName} to substitute variables from metadata","latest-timeseries":"Latest timeseries","latest-timeseries-hint":"Latest timeseries, use ${metaKeyName} to substitute variables from metadata","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name."},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=b([t.NgModule({declarations:[F],imports:[r.CommonModule,a.SharedModule],exports:[Ce,Ne,Oe,Ue,F]}),h("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=je,e.ɵa=F,e.ɵb=Ce,e.ɵba=be,e.ɵbb=he,e.ɵbc=le,e.ɵbd=ne,e.ɵbe=ae,e.ɵbf=oe,e.ɵbg=ie,e.ɵbh=Ne,e.ɵbi=ve,e.ɵbj=Fe,e.ɵbk=xe,e.ɵbl=Te,e.ɵbm=qe,e.ɵbn=Se,e.ɵbo=Ie,e.ɵbp=ke,e.ɵbq=Oe,e.ɵbr=Ve,e.ɵbs=Ee,e.ɵbt=Ae,e.ɵbu=Le,e.ɵbv=Me,e.ɵbw=Pe,e.ɵbx=Re,e.ɵby=we,e.ɵbz=Ue,e.ɵc=x,e.ɵca=De,e.ɵcb=Ke,e.ɵcc=Be,e.ɵd=T,e.ɵe=q,e.ɵf=S,e.ɵg=I,e.ɵh=k,e.ɵi=N,e.ɵj=V,e.ɵk=E,e.ɵl=A,e.ɵm=L,e.ɵn=X,e.ɵo=ee,e.ɵp=te,e.ɵq=re,e.ɵr=se,e.ɵs=me,e.ɵt=ue,e.ɵu=de,e.ɵv=pe,e.ɵw=ce,e.ɵx=fe,e.ɵy=ge,e.ɵz=ye,Object.defineProperty(e,"__esModule",{value:!0})})); + ***************************************************************************** */var g=function(e,t){return(g=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function y(e,t){function r(){this.constructor=e}g(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function b(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function h(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function C(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var v,F=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-node-empty-config",template:"
"}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],i}return y(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]]})},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(v||(v={}));var M,P=new Map([[v.CUSTOMER,"tb.rulenode.originator-customer"],[v.TENANT,"tb.rulenode.originator-tenant"],[v.RELATED,"tb.rulenode.originator-related"],[v.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,w=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,D=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var K,B=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(K||(K={}));var U,j,H,G=new Map([[K.TITLE,"tb.rulenode.entity-details-title"],[K.COUNTRY,"tb.rulenode.entity-details-country"],[K.STATE,"tb.rulenode.entity-details-state"],[K.ZIP,"tb.rulenode.entity-details-zip"],[K.ADDRESS,"tb.rulenode.entity-details-address"],[K.ADDRESS2,"tb.rulenode.entity-details-address2"],[K.PHONE,"tb.rulenode.entity-details-phone"],[K.EMAIL,"tb.rulenode.entity-details-email"],[K.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(U||(U={})),function(e){e.ASC="ASC",e.DESC="DESC"}(j||(j={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(H||(H={}));var z,$=new Map([[H.STANDARD,"tb.rulenode.sqs-queue-standard"],[H.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Q=["anonymous","basic","cert.PEM"],_=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),W=["sas","cert.PEM"],J=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(z||(z={}));var Y=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],Z=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),X=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return y(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=C(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",String)],r.prototype,"requiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyRequiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valRequiredText",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=b([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText }}\n {{ valText }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),h("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r,n,o){var i,l,m=e.call(this,t)||this;m.store=t,m.translate=r,m.truncate=n,m.fb=o,m.placeholder="tb.rulenode.message-type",m.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],m.messageTypes=[],m.messageTypesList=[],m.searchText="",m.propagateChange=function(e){},m.messageTypeConfigForm=m.fb.group({messageType:[null]});try{for(var u=C(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;m.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return m}var l;return y(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),b([t.Input(),h("design:type",String)],r.prototype,"label",void 0),b([t.Input(),h("design:type",Object)],r.prototype,"placeholder",void 0),b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.ViewChild("chipList",{static:!1}),h("design:type",d.MatChipList)],r.prototype,"chipList",void 0),b([t.ViewChild("messageTypeAutocomplete",{static:!1}),h("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),b([t.ViewChild("messageTypeInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=b([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),h("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),le=function(){function e(){}return e=b([t.NgModule({declarations:[ne,ae,oe,ie],imports:[r.CommonModule,a.SharedModule,m.HomeComponentsModule],exports:[ne,ae,oe,ie]})],e)}(),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=H,n.sqsQueueTypes=Object.keys(H),n.sqsQueueTypeTranslationsMap=$,n}return y(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Y,n.ToByteStandartCharsetTypeTranslationMap=Z,n}return y(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allMqttCredentialsTypes=Q,n.mqttCredentialsTypeTranslationsMap=_,n}return y(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],username:[e&&e.credentials?e.credentials.username:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;switch(t){case"anonymous":e.credentials={type:t};break;case"basic":e.credentials={type:t,username:e.credentials.username,password:e.credentials.password};break;case"cert.PEM":delete e.credentials.username}return e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.mqttConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("username").setValidators([]),t.get("password").setValidators([]),t.get("caCert").setValidators([]),t.get("caCertFileName").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"anonymous":break;case"basic":t.get("username").setValidators([i.Validators.required]),t.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("caCert").setValidators([i.Validators.required]),t.get("caCertFileName").setValidators([i.Validators.required]),t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("username").updateValueAndValidity({emitEvent:e}),t.get("password").updateValueAndValidity({emitEvent:e}),t.get("caCert").updateValueAndValidity({emitEvent:e}),t.get("caCertFileName").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n \n tb.rulenode.credentials\n \n {{ mqttCredentialsTypeTranslationsMap.get(mqttConfigForm.get(\'credentials\').get(\'type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ mqttCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return y(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(z),n}return y(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n \n \n \n tb.rulenode.max-parallel-requests-count\n \n \n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return y(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return y(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
tb.rulenode.select-queue-hint
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=W,n.azureIotHubCredentialsTypeTranslationsMap=J,n}return y(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(){function e(){}return e=b([t.NgModule({declarations:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he,Ce],imports:[r.CommonModule,a.SharedModule,le],exports:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he,Ce]})],e)}(),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n}return y(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return y(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=C(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("alarmStatusInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=b([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(){function e(){}return e=b([t.NgModule({declarations:[Fe,xe,Te,qe,Se,Ie,ke,Ne],imports:[r.CommonModule,a.SharedModule,le],exports:[Fe,xe,Te,qe,Se,Ie,ke,Ne]})],e)}(),Ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=G,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=C(Object.keys(K)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(K[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(G.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(G.get(K[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("detailsInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=b([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n.fetchMode=U,n.fetchModes=Object.keys(U),n.samplingOrders=Object.keys(j),n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===U.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(){function e(){}return e=b([t.NgModule({declarations:[Ee,Ae,Le,Me,Pe,Re,we,Oe],imports:[r.CommonModule,a.SharedModule,le],exports:[Ee,Ae,Le,Me,Pe,Re,we,Oe]})],e)}(),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=v,n.originatorSources=Object.keys(v),n.originatorSourceTranslationMap=P,n}return y(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===v.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),je=function(){function e(){}return e=b([t.NgModule({declarations:[Ke,Be,Ue],imports:[r.CommonModule,a.SharedModule,le],exports:[Ke,Be,Ue]})],e)}(),He=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","client-attributes-hint":"Client attributes, use ${metaKeyName} to substitute variables from metadata","shared-attributes":"Shared attributes","shared-attributes-hint":"Shared attributes, use ${metaKeyName} to substitute variables from metadata","server-attributes":"Server attributes","server-attributes-hint":"Server attributes, use ${metaKeyName} to substitute variables from metadata","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","latest-timeseries-hint":"Latest timeseries, use ${metaKeyName} to substitute variables from metadata","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=b([t.NgModule({declarations:[F],imports:[r.CommonModule,a.SharedModule],exports:[ve,Ve,De,je,F]}),h("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=He,e.ɵa=F,e.ɵb=ve,e.ɵba=be,e.ɵbb=he,e.ɵbc=Ce,e.ɵbd=le,e.ɵbe=ne,e.ɵbf=ae,e.ɵbg=oe,e.ɵbh=ie,e.ɵbi=Ve,e.ɵbj=Fe,e.ɵbk=xe,e.ɵbl=Te,e.ɵbm=qe,e.ɵbn=Se,e.ɵbo=Ie,e.ɵbp=ke,e.ɵbq=Ne,e.ɵbr=De,e.ɵbs=Ee,e.ɵbt=Ae,e.ɵbu=Le,e.ɵbv=Me,e.ɵbw=Pe,e.ɵbx=Re,e.ɵby=we,e.ɵbz=Oe,e.ɵc=x,e.ɵca=je,e.ɵcb=Ke,e.ɵcc=Be,e.ɵcd=Ue,e.ɵd=T,e.ɵe=q,e.ɵf=S,e.ɵg=I,e.ɵh=k,e.ɵi=N,e.ɵj=V,e.ɵk=E,e.ɵl=A,e.ɵm=L,e.ɵn=X,e.ɵo=ee,e.ɵp=te,e.ɵq=re,e.ɵr=se,e.ɵs=me,e.ɵt=ue,e.ɵu=de,e.ɵv=pe,e.ɵw=ce,e.ɵx=fe,e.ɵy=ge,e.ɵz=ye,Object.defineProperty(e,"__esModule",{value:!0})})); //# sourceMappingURL=rulenode-core-config.umd.min.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index ae4fb3df1e..721d053b9c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -45,8 +45,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.alarm.AlarmOperationResult; -import org.thingsboard.server.dao.alarm.AlarmService; import javax.script.ScriptException; import java.io.IOException; @@ -65,9 +63,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.IS_CLEARED_ALARM; -import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.IS_EXISTING_ALARM; -import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.IS_NEW_ALARM; +import static org.thingsboard.server.common.data.DataConstants.IS_CLEARED_ALARM; +import static org.thingsboard.server.common.data.DataConstants.IS_EXISTING_ALARM; +import static org.thingsboard.server.common.data.DataConstants.IS_NEW_ALARM; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; import static org.thingsboard.server.common.data.alarm.AlarmStatus.ACTIVE_UNACK; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index f835cfd506..2b71d5d14f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; @@ -90,7 +91,7 @@ public class TbJsFilterNodeTest { public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); mockJsExecutor(); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java new file mode 100644 index 0000000000..8562b52bb9 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -0,0 +1,200 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +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.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.Collections; +import java.util.UUID; + +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class TbDeviceProfileNodeTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private TbDeviceProfileNode node; + + @Mock + private TbContext ctx; + @Mock + private RuleEngineDeviceProfileCache cache; + @Mock + private TimeseriesService timeseriesService; + @Mock + private RuleEngineAlarmService alarmService; + + private TenantId tenantId = new TenantId(UUID.randomUUID()); + private DeviceId deviceId = new DeviceId(UUID.randomUUID()); + private DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + + @Test + public void testRandomMessageType() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 42); + TbMsg msg = TbMsg.newMsg("123456789", deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testEmptyProfile() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 42); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testAlarmCreate() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + KeyFilter highTempFilter = new KeyFilter(); + highTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + highTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate(); + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperaturePredicate.setValue(new FilterPredicateValue<>(30.0)); + highTempFilter.setPredicate(highTemperaturePredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("highTemperatureAlarmID"); + dpa.setAlarmType("highTemperatureAlarm"); + dpa.setCreateRules(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)); + + KeyFilter lowTempFilter = new KeyFilter(); + lowTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lowTemperaturePredicate = new NumericFilterPredicate(); + lowTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + lowTemperaturePredicate.setValue(new FilterPredicateValue<>(10.0)); + lowTempFilter.setPredicate(lowTemperaturePredicate); + AlarmRule clearRule = new AlarmRule(); + AlarmCondition clearCondition = new AlarmCondition(); + clearCondition.setCondition(Collections.singletonList(lowTempFilter)); + clearRule.setCondition(clearCondition); + dpa.setClearRule(clearRule); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 42); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + + TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + + + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg2); + verify(ctx).tellSuccess(msg2); + verify(ctx).tellNext(theMsg2, "Alarm Updated"); + + } + + private void init() throws TbNodeException { + Mockito.when(ctx.getTenantId()).thenReturn(tenantId); + Mockito.when(ctx.getDeviceProfileCache()).thenReturn(cache); + Mockito.when(ctx.getTimeseriesService()).thenReturn(timeseriesService); + Mockito.when(ctx.getAlarmService()).thenReturn(alarmService); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.createObjectNode()); + node = new TbDeviceProfileNode(); + node.init(ctx, nodeConfiguration); + } + +} diff --git a/tools/pom.xml b/tools/pom.xml index 8fe3bc66b9..8d596c8493 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard tools diff --git a/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java b/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java index 38d28d684b..cb810ac59a 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java +++ b/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java @@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import javax.net.ssl.*; import java.io.File; @@ -71,7 +72,7 @@ public class MqttSslClient { MqttConnectOptions options = new MqttConnectOptions(); options.setSocketFactory(sslContext.getSocketFactory()); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, CLIENT_ID); + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, CLIENT_ID, new MemoryPersistence()); client.connect(options); Thread.sleep(3000); MqttMessage message = new MqttMessage(); diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 2558c4d841..1b7abf06db 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index f04a7559b8..3f57b32fab 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -77,11 +77,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 2f3e423b7f..f5ae869c75 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index a97a58cd85..77d5f30fa7 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -70,11 +70,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 30e112a3ce..a1a45f54b8 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index b3804e3531..f01b15c77a 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -45,7 +45,6 @@ transport: mqtt: bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" bind_port: "${MQTT_BIND_PORT:1883}" - adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}" timeout: "${MQTT_TIMEOUT:10000}" netty: leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" @@ -67,6 +66,8 @@ transport: key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" # Type of the key store key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" @@ -98,11 +99,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/transport/pom.xml b/transport/pom.xml index 7e1a6bd998..429c953a46 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard transport diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 72886b1a8b..3cad30304a 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -137,7 +137,8 @@ "react-is", "hoist-non-react-statics", "classnames", - "raf" + "raf", + "moment-timezone" ] }, "configurations": { @@ -248,4 +249,4 @@ "cli": { "packageManager": "yarn" } -} \ No newline at end of file +} diff --git a/ui-ngx/e2e/tsconfig.e2e.json b/ui-ngx/e2e/tsconfig.e2e.json index 415179b489..77d311e88d 100644 --- a/ui-ngx/e2e/tsconfig.e2e.json +++ b/ui-ngx/e2e/tsconfig.e2e.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "module": "commonjs", @@ -10,4 +10,4 @@ "node" ] } -} \ No newline at end of file +} diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js index 87b95cab07..9adc0e5852 100644 --- a/ui-ngx/extra-webpack.config.js +++ b/ui-ngx/extra-webpack.config.js @@ -32,7 +32,7 @@ module.exports = { SUPPORTED_LANGS: JSON.stringify(langs), }), new CompressionPlugin({ - filename: "[path].gz[query]", + filename: "[path][base].gz[query]", algorithm: "gzip", test: /\.js$|\.css$|\.html$|\.svg?.+$|\.jpg$|\.ttf?.+$|\.woff?.+$|\.eot?.+$|\.json$/, threshold: 10240, diff --git a/ui-ngx/package.json b/ui-ngx/package.json index ef12a0d8de..4612631ff6 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "3.1.1", + "version": "3.2.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --open", @@ -12,33 +12,33 @@ }, "private": true, "dependencies": { - "@angular/animations": "^10.0.9", - "@angular/cdk": "^10.1.3", - "@angular/common": "^10.0.9", - "@angular/compiler": "^10.0.9", - "@angular/core": "^10.0.9", + "@angular/animations": "^10.1.5", + "@angular/cdk": "^10.2.4", + "@angular/common": "^10.1.5", + "@angular/compiler": "^10.1.5", + "@angular/core": "^10.1.5", "@angular/flex-layout": "^10.0.0-beta.32", - "@angular/forms": "^10.0.9", - "@angular/material": "^10.1.3", - "@angular/platform-browser": "^10.0.9", - "@angular/platform-browser-dynamic": "^10.0.9", - "@angular/router": "^10.0.9", + "@angular/forms": "^10.1.5", + "@angular/material": "^10.2.4", + "@angular/platform-browser": "^10.1.5", + "@angular/platform-browser-dynamic": "^10.1.5", + "@angular/router": "^10.1.5", "@auth0/angular-jwt": "^5.0.1", "@date-io/date-fns": "^2.6.1", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "^0.4.4", "@juggle/resize-observer": "^3.1.3", - "@mat-datetimepicker/core": "^5.0.1", + "@mat-datetimepicker/core": "^5.1.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/pickers": "^3.2.10", - "@ngrx/effects": "^10.0.0", - "@ngrx/store": "^10.0.0", - "@ngrx/store-devtools": "^10.0.0", + "@ngrx/effects": "^10.0.1", + "@ngrx/store": "^10.0.1", + "@ngrx/store-devtools": "^10.0.1", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", "ace-builds": "^1.4.12", - "angular-gridster2": "^10.1.3", + "angular-gridster2": "^10.1.6", "angular2-hotkeys": "^2.2.0", "canvas-gauges": "^2.1.7", "compass-sass-mixins": "^0.12.7", @@ -48,13 +48,13 @@ "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", "font-awesome": "^4.7.0", "jquery": "^3.5.1", - "jquery.terminal": "^2.16.0", - "js-beautify": "^1.12.0", + "jquery.terminal": "^2.18.3", + "js-beautify": "^1.13.0", "json-schema-defaults": "^0.4.0", "jstree": "^3.3.10", "jstree-bootstrap-theme": "^1.0.1", "jszip": "^3.5.0", - "leaflet": "^1.6.0", + "leaflet": "^1.7.1", "leaflet-editable": "^1.2.0", "leaflet-polylinedecorator": "^1.6.0", "leaflet-providers": "^1.10.2", @@ -62,61 +62,64 @@ "leaflet.markercluster": "^1.4.1", "material-design-icons": "^3.0.1", "messageformat": "^2.3.0", - "moment": "^2.27.0", + "moment": "^2.29.1", + "moment-timezone": "^0.5.31", + "mousetrap": "1.6.3", "ngx-clipboard": "^13.0.1", - "ngx-color-picker": "^10.0.1", - "ngx-daterangepicker-material": "^3.0.4", + "ngx-color-picker": "^10.1.0", + "ngx-daterangepicker-material": "^4.0.1", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-hm-carousel": "^2.0.0-rc.1", "ngx-sharebuttons": "^8.0.1", "ngx-translate-messageformat-compiler": "^4.8.0", "objectpath": "^2.0.0", - "prettier": "^2.0.5", + "prettier": "^2.1.2", "prop-types": "^15.7.2", "raphael": "^2.3.0", - "rc-select": "^11.1.3", + "rc-select": "^11.3.3", "react": "^16.13.1", - "react-ace": "^9.1.3", + "react-ace": "^9.1.4", "react-dom": "^16.13.1", - "react-dropzone": "^11.0.3", + "react-dropzone": "^11.2.0", "reactcss": "^1.2.3", - "rxjs": "^6.6.2", + "rxjs": "^6.6.3", "schema-inspector": "^1.7.0", "screenfull": "^5.0.2", "split.js": "^1.6.2", "systemjs": "0.21.5", - "tinycolor2": "^1.4.1", + "tinycolor2": "^1.4.2", "tooltipster": "^4.2.8", - "tslib": "^2.0.1", + "tslib": "^2.0.2", "tv4": "^1.3.0", - "typeface-roboto": "^0.0.75", - "zone.js": "~0.10.3" + "typeface-roboto": "^1.1.13", + "zone.js": "~0.11.1" }, "devDependencies": { - "@angular-builders/custom-webpack": "^10.0.0", - "@angular-devkit/build-angular": "^0.1000.5", - "@angular/cli": "^10.0.5", - "@angular/compiler-cli": "^10.0.9", - "@angular/language-service": "^10.0.9", + "@angular-builders/custom-webpack": "^10.0.1", + "@angular-devkit/build-angular": "^0.1001.5", + "@angular/cli": "^10.1.5", + "@angular/compiler-cli": "^10.1.5", + "@angular/language-service": "^10.1.5", "@types/canvas-gauges": "^2.1.2", "@types/flot": "^0.0.31", "@types/jasmine": "^3.5.12", "@types/jasminewd2": "^2.0.8", - "@types/jquery": "^3.5.1", + "@types/jquery": "^3.5.2", "@types/js-beautify": "^1.11.0", "@types/jstree": "^3.3.40", - "@types/jszip": "^3.4.1", "@types/leaflet": "^1.5.17", - "@types/leaflet-markercluster": "^1.0.3", "@types/leaflet-polylinedecorator": "^1.6.0", - "@types/lodash": "^4.14.159", + "@types/leaflet.markercluster": "^1.4.3", + "@types/lodash": "^4.14.161", + "@types/moment-timezone": "^0.5.30", + "@types/mousetrap": "1.6.3", "@types/raphael": "^2.3.0", - "@types/react": "^16.9.46", + "@types/react": "^16.9.51", "@types/react-dom": "^16.9.8", "@types/tinycolor2": "^1.4.2", "@types/tooltipster": "^0.0.30", - "codelyzer": "^6.0.0", - "compression-webpack-plugin": "^4.0.1", + "codelyzer": "^6.0.1", + "compression-webpack-plugin": "^6.0.2", "directory-tree": "^2.2.4", "jasmine-core": "~3.6.0", "jasmine-spec-reporter": "~5.0.2", @@ -127,9 +130,9 @@ "karma-jasmine-html-reporter": "^1.5.4", "ngrx-store-freeze": "^0.2.4", "protractor": "~7.0.0", - "ts-node": "^8.10.2", + "ts-node": "^9.0.0", "tslint": "~6.1.3", - "typescript": "~3.9.7", - "webpack": "^4.44.1" + "typescript": "~4.0.3", + "webpack": "^4.44.2" } } diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 80e13e995d..14b9317e4a 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.1.1-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 0eb71c7e90..c574fd426a 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -47,7 +47,7 @@ import { import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; import { CancelAnimationFrame } from '@core/services/raf.service'; import { EntityType } from '@shared/models/entity-type.models'; -import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils'; +import { createLabelFromDatasource, deepClone, isDefined, isDefinedAndNotNull, isEqual } from '@core/utils'; import { EntityId } from '@app/shared/models/id/entity-id'; import * as moment_ from 'moment'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; @@ -403,7 +403,7 @@ export class WidgetSubscription implements IWidgetSubscription { configDatasource: datasource, configDatasourceIndex: index, dataLoaded: (pageData, data1, datasourceIndex, pageLink) => { - this.dataLoaded(pageData, data1, datasourceIndex, pageLink, true) + this.dataLoaded(pageData, data1, datasourceIndex, pageLink, true); }, initialPageDataChanged: this.initialPageDataChanged.bind(this), dataUpdated: this.dataUpdated.bind(this), @@ -575,7 +575,7 @@ export class WidgetSubscription implements IWidgetSubscription { updateDataVisibility(index: number): void { if (this.displayLegend) { - const hidden = this.legendData.keys[index].dataKey.hidden; + const hidden = this.legendData.keys.find(key => key.dataIndex === index).dataKey.hidden; if (hidden) { this.hiddenData[index].data = this.data[index].data; this.data[index].data = []; @@ -804,7 +804,7 @@ export class WidgetSubscription implements IWidgetSubscription { configDatasourceIndex: datasourceIndex, subscriptionTimewindow: this.subscriptionTimewindow, dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { - this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true) + this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); }, dataUpdated: this.dataUpdated.bind(this), updateRealtimeSubscription: () => { @@ -1149,7 +1149,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.onSubscriptionMessage({ severity: 'warn', message - }) + }); } } if (isUpdate) { @@ -1274,10 +1274,10 @@ export class WidgetSubscription implements IWidgetSubscription { const configuredDatasource = this.configuredDatasources[datasourceIndex]; const startIndex = configuredDatasource.dataKeyStartIndex; const dataKeysCount = configuredDatasource.dataKeys.length; - const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex; + const index = startIndex + dataIndex * dataKeysCount + dataKeyIndex; let update = true; let currentData: DataSetHolder; - if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) { + if (this.displayLegend && this.legendData.keys.find(key => key.dataIndex === index).dataKey.hidden) { currentData = this.hiddenData[index]; } else { currentData = this.data[index]; @@ -1331,8 +1331,8 @@ export class WidgetSubscription implements IWidgetSubscription { } private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) { - const dataKey = this.legendData.keys[dataIndex].dataKey; - const decimals = isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; + const dataKey = this.legendData.keys.find(key => key.dataIndex === dataIndex).dataKey; + const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : this.decimals; const units = dataKey.units && dataKey.units.length ? dataKey.units : this.units; const legendKeyData = this.legendData.data[dataIndex]; if (this.legendConfig.showMin) { diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts new file mode 100644 index 0000000000..8cdc8c188b --- /dev/null +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { DeviceProfile, DeviceProfileInfo } from '@shared/models/device.models'; + +@Injectable({ + providedIn: 'root' +}) +export class DeviceProfileService { + + constructor( + private http: HttpClient + ) { } + + public getDeviceProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable { + return this.http.post('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteDeviceProfile(deviceProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/deviceProfile/${deviceProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultDeviceProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/deviceProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileInfo(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 2d31503bd1..9d842d79a6 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -46,12 +46,24 @@ export class DeviceService { defaultHttpOptionsFromConfig(config)); } + public getTenantDeviceInfosByDeviceProfileId(pageLink: PageLink, deviceProfileId: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/deviceInfos${pageLink.toQuery()}&deviceProfileId=${deviceProfileId}`, + defaultHttpOptionsFromConfig(config)); + } + public getCustomerDeviceInfos(customerId: string, pageLink: PageLink, type: string = '', config?: RequestConfig): Observable> { return this.http.get>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&type=${type}`, defaultHttpOptionsFromConfig(config)); } + public getCustomerDeviceInfosByDeviceProfileId(customerId: string, pageLink: PageLink, deviceProfileId: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&deviceProfileId=${deviceProfileId}`, + defaultHttpOptionsFromConfig(config)); + } + public getDevice(deviceId: string, config?: RequestConfig): Observable { return this.http.get(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 0f5f1d554a..a510735855 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -62,6 +62,7 @@ import { entityInfoFields, EntityKey, EntityKeyType, + EntityKeyValueType, FilterPredicateType, singleEntityDataPageLink, StringOperation @@ -399,6 +400,7 @@ export class EntityService { keyFilters: searchText && searchText.length ? [ { key: nameField, + valueType: EntityKeyValueType.STRING, predicate: { type: FilterPredicateType.STRING, operation: StringOperation.STARTS_WITH, @@ -593,10 +595,10 @@ export class EntityService { return entityTypes; } - private getEntityFieldKeys (entityType: EntityType, searchText: string): Array { + private getEntityFieldKeys(entityType: EntityType, searchText: string): Array { const entityFieldKeys: string[] = [entityFields.createdTime.keyName]; const query = searchText.toLowerCase(); - switch(entityType) { + switch (entityType) { case EntityType.USER: entityFieldKeys.push(entityFields.name.keyName); entityFieldKeys.push(entityFields.email.keyName); @@ -863,7 +865,7 @@ export class EntityService { const tasks: Observable[] = []; const result: Device | Asset = entity as (Device | Asset); const additionalInfo = result.additionalInfo || {}; - if(result.label !== entityData.label || + if (result.label !== entityData.label || result.type !== entityData.type || additionalInfo.description !== entityData.description || (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index 2ac567ac30..e7ab664ae9 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -68,6 +68,12 @@ export class RuleChainService { return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); } + public createDefaultRuleChain(ruleChainName: string, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/device/default', { + name: ruleChainName + }, defaultHttpOptionsFromConfig(config)); + } + public saveRuleChain(ruleChain: RuleChain, config?: RequestConfig): Observable { return this.http.post('/api/ruleChain', ruleChain, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/tenant-profile.service.ts b/ui-ngx/src/app/core/http/tenant-profile.service.ts new file mode 100644 index 0000000000..f7d0b7ccdf --- /dev/null +++ b/ui-ngx/src/app/core/http/tenant-profile.service.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { EntityInfoData } from '@shared/models/entity.models'; + +@Injectable({ + providedIn: 'root' +}) +export class TenantProfileService { + + constructor( + private http: HttpClient + ) { } + + public getTenantProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfile(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenantProfile/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveTenantProfile(tenantProfile: TenantProfile, config?: RequestConfig): Observable { + return this.http.post('/api/tenantProfile', tenantProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteTenantProfile(tenantProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/tenantProfile/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultTenantProfile(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/tenantProfile/${tenantProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultTenantProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/tenantProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfileInfo(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenantProfileInfo/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/tenant.service.ts b/ui-ngx/src/app/core/http/tenant.service.ts index b98955f16b..2306c4852f 100644 --- a/ui-ngx/src/app/core/http/tenant.service.ts +++ b/ui-ngx/src/app/core/http/tenant.service.ts @@ -20,7 +20,7 @@ import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; -import { Tenant } from '@shared/models/tenant.model'; +import { Tenant, TenantInfo } from '@shared/models/tenant.model'; @Injectable({ providedIn: 'root' @@ -35,10 +35,18 @@ export class TenantService { return this.http.get>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getTenantInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + public getTenant(tenantId: string, config?: RequestConfig): Observable { return this.http.get(`/api/tenant/${tenantId}`, defaultHttpOptionsFromConfig(config)); } + public getTenantInfo(tenantId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/info/${tenantId}`, defaultHttpOptionsFromConfig(config)); + } + public saveTenant(tenant: Tenant, config?: RequestConfig): Observable { return this.http.post('/api/tenant', tenant, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 35a1331060..4440d8a4a9 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -43,6 +43,8 @@ export class WidgetService { private systemWidgetsBundles: Array; private tenantWidgetsBundles: Array; + private loadWidgetsBundleCacheSubject: ReplaySubject; + constructor( private http: HttpClient, private utils: UtilsService, @@ -238,34 +240,36 @@ export class WidgetService { private loadWidgetsBundleCache(config?: RequestConfig): Observable { if (!this.allWidgetsBundles) { - const loadWidgetsBundleCacheSubject = new ReplaySubject(); - this.http.get>('/api/widgetsBundles', - defaultHttpOptionsFromConfig(config)).subscribe( - (allWidgetsBundles) => { - this.allWidgetsBundles = allWidgetsBundles; - this.systemWidgetsBundles = new Array(); - this.tenantWidgetsBundles = new Array(); - this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { - let res = wb1.title.localeCompare(wb2.title); - if (res === 0) { - res = wb2.createdTime - wb1.createdTime; - } - return res; - }); - this.allWidgetsBundles.forEach((widgetsBundle) => { - if (widgetsBundle.tenantId.id === NULL_UUID) { - this.systemWidgetsBundles.push(widgetsBundle); - } else { - this.tenantWidgetsBundles.push(widgetsBundle); - } + if (!this.loadWidgetsBundleCacheSubject) { + this.loadWidgetsBundleCacheSubject = new ReplaySubject(); + this.http.get>('/api/widgetsBundles', + defaultHttpOptionsFromConfig(config)).subscribe( + (allWidgetsBundles) => { + this.allWidgetsBundles = allWidgetsBundles; + this.systemWidgetsBundles = new Array(); + this.tenantWidgetsBundles = new Array(); + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { + let res = wb1.title.localeCompare(wb2.title); + if (res === 0) { + res = wb2.createdTime - wb1.createdTime; + } + return res; + }); + this.allWidgetsBundles.forEach((widgetsBundle) => { + if (widgetsBundle.tenantId.id === NULL_UUID) { + this.systemWidgetsBundles.push(widgetsBundle); + } else { + this.tenantWidgetsBundles.push(widgetsBundle); + } + }); + this.loadWidgetsBundleCacheSubject.next(); + this.loadWidgetsBundleCacheSubject.complete(); + }, + () => { + this.loadWidgetsBundleCacheSubject.error(null); }); - loadWidgetsBundleCacheSubject.next(); - loadWidgetsBundleCacheSubject.complete(); - }, - () => { - loadWidgetsBundleCacheSubject.error(null); - }); - return loadWidgetsBundleCacheSubject.asObservable(); + } + return this.loadWidgetsBundleCacheSubject.asObservable(); } else { return of(null); } @@ -275,6 +279,7 @@ export class WidgetService { this.allWidgetsBundles = undefined; this.systemWidgetsBundles = undefined; this.tenantWidgetsBundles = undefined; + this.loadWidgetsBundleCacheSubject = undefined; } } diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 0897053bb9..2874ed6cda 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -249,7 +249,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } else { this.activeRequests--; } - if (this.activeRequests === 1) { + if (this.activeRequests === 1 && isLoading) { this.store.dispatch(new ActionLoadStart()); } else if (this.activeRequests === 0) { this.store.dispatch(new ActionLoadFinish()); diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index f9715b3555..028a0f5380 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -14,9 +14,11 @@ /// limitations under the License. /// +import { HasUUID } from '@shared/models/id/has-uuid'; + export declare type MenuSectionType = 'link' | 'toggle'; -export class MenuSection { +export interface MenuSection extends HasUUID{ name: string; type: MenuSectionType; path: string; @@ -26,12 +28,12 @@ export class MenuSection { pages?: Array; } -export class HomeSection { +export interface HomeSection { name: string; places: Array; } -export class HomeSectionPlace { +export interface HomeSectionPlace { name: string; icon: string; isMdiIcon?: boolean; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 6032bf24f3..05477f1ac1 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -24,6 +24,7 @@ import { HomeSection, MenuSection } from '@core/services/menu.models'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { Authority } from '@shared/models/authority.enum'; import { AuthUser } from '@shared/models/user.model'; +import { guid } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -74,24 +75,36 @@ export class MenuService { const sections: Array = []; sections.push( { + id: guid(), name: 'home.home', type: 'link', path: '/home', icon: 'home' }, { + id: guid(), name: 'tenant.tenants', type: 'link', path: '/tenants', icon: 'supervisor_account' }, { + id: guid(), + name: 'tenant-profile.tenant-profiles', + type: 'link', + path: '/tenantProfiles', + icon: 'mdi:alpha-t-box', + isMdiIcon: true + }, + { + id: guid(), name: 'widget.widget-library', type: 'link', path: '/widgets-bundles', icon: 'now_widgets' }, { + id: guid(), name: 'admin.system-settings', type: 'toggle', path: '/settings', @@ -99,18 +112,21 @@ export class MenuService { icon: 'settings', pages: [ { + id: guid(), name: 'admin.general', type: 'link', path: '/settings/general', icon: 'settings_applications' }, { + id: guid(), name: 'admin.outgoing-mail', type: 'link', path: '/settings/outgoing-mail', icon: 'mail' }, { + id: guid(), name: 'admin.security-settings', type: 'link', path: '/settings/security-settings', @@ -132,7 +148,13 @@ export class MenuService { name: 'tenant.tenants', icon: 'supervisor_account', path: '/tenants' - } + }, + { + name: 'tenant-profile.tenant-profiles', + icon: 'mdi:alpha-t-box', + isMdiIcon: true, + path: '/tenantProfiles' + }, ] }, { @@ -173,54 +195,71 @@ export class MenuService { const sections: Array = []; sections.push( { + id: guid(), name: 'home.home', type: 'link', path: '/home', icon: 'home' }, { + id: guid(), name: 'rulechain.rulechains', type: 'link', path: '/ruleChains', icon: 'settings_ethernet' }, { + id: guid(), name: 'customer.customers', type: 'link', path: '/customers', icon: 'supervisor_account' }, { + id: guid(), name: 'asset.assets', type: 'link', path: '/assets', icon: 'domain' }, { + id: guid(), name: 'device.devices', type: 'link', path: '/devices', icon: 'devices_other' }, { + id: guid(), + name: 'device-profile.device-profiles', + type: 'link', + path: '/deviceProfiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true + }, + { + id: guid(), name: 'entity-view.entity-views', type: 'link', path: '/entityViews', icon: 'view_quilt' }, { + id: guid(), name: 'widget.widget-library', type: 'link', path: '/widgets-bundles', icon: 'now_widgets' }, { + id: guid(), name: 'dashboard.dashboards', type: 'link', path: '/dashboards', icon: 'dashboards' }, { + id: guid(), name: 'audit-log.audit-logs', type: 'link', path: '/auditLogs', @@ -270,6 +309,12 @@ export class MenuService { name: 'device.devices', icon: 'devices_other', path: '/devices' + }, + { + name: 'device-profile.device-profiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true, + path: '/deviceProfiles' } ] }, @@ -316,30 +361,35 @@ export class MenuService { const sections: Array = []; sections.push( { + id: guid(), name: 'home.home', type: 'link', path: '/home', icon: 'home' }, { + id: guid(), name: 'asset.assets', type: 'link', path: '/assets', icon: 'domain' }, { + id: guid(), name: 'device.devices', type: 'link', path: '/devices', icon: 'devices_other' }, { + id: guid(), name: 'entity-view.entity-views', type: 'link', path: '/entityViews', icon: 'view_quilt' }, { + id: guid(), name: 'dashboard.dashboards', type: 'link', path: '/dashboards', diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index e6955106bb..7c56df331e 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -60,6 +60,7 @@ import * as AngularMaterialStepper from '@angular/material/stepper'; import * as AngularMaterialTable from '@angular/material/table'; import * as AngularMaterialTabs from '@angular/material/tabs'; import * as AngularMaterialToolbar from '@angular/material/toolbar'; +import * as AngularMaterialTooltip from '@angular/material/tooltip'; import * as AngularMaterialTree from '@angular/material/tree'; import * as NgrxStore from '@ngrx/store'; import * as RxJs from 'rxjs'; @@ -119,6 +120,7 @@ export const modulesMap: {[key: string]: any} = { '@angular/material/table': SystemJS.newModule(AngularMaterialTable), '@angular/material/tabs': SystemJS.newModule(AngularMaterialTabs), '@angular/material/toolbar': SystemJS.newModule(AngularMaterialToolbar), + '@angular/material/tooltip': SystemJS.newModule(AngularMaterialTooltip), '@angular/material/tree': SystemJS.newModule(AngularMaterialTree), '@ngrx/store': SystemJS.newModule(NgrxStore), rxjs: SystemJS.newModule(RxJs), diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 724287eb90..918227fc3e 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -246,11 +246,11 @@ widgetsList.length === 0 && widgetsBundle" fxFlex fxLayoutAlign="center center" - style="text-transform: uppercase; display: flex;" + style="display: flex;" class="mat-headline">widgets-bundle.empty widget.select-widgets-bundle diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.scss b/ui-ngx/src/app/modules/home/components/details-panel.component.scss index bdefe6df37..d53bd7864b 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.scss @@ -41,7 +41,6 @@ font-size: 1rem; font-weight: 400; text-overflow: ellipsis; - text-transform: uppercase; white-space: nowrap; @media #{$mat-gt-sm} { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 6c27a8c9f7..ac20e70128 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -36,9 +36,9 @@ import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; +import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute } from '@angular/router'; @@ -59,6 +59,7 @@ import { DAY, historyInterval, HistoryWindowType, Timewindow } from '@shared/mod import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; +import { HasUUID } from '../../../../shared/models/id/has-uuid'; @Component({ selector: 'tb-entities-table', @@ -401,16 +402,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn true ).subscribe((result) => { if (result) { - const tasks: Observable[] = []; + const tasks: Observable[] = []; entities.forEach((entity) => { if (this.entitiesTableConfig.deleteEnabled(entity)) { - tasks.push(this.entitiesTableConfig.deleteEntity(entity.id)); + tasks.push(this.entitiesTableConfig.deleteEntity(entity.id).pipe( + map(() => entity.id), + catchError(() => of(null) + ))); } }); forkJoin(tasks).subscribe( - () => { + (ids) => { this.updateData(); - this.entitiesTableConfig.entitiesDeleted(entities.map((e) => e.id)); + this.entitiesTableConfig.entitiesDeleted(ids.filter(id => id !== null)); } ); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index f6ef733560..8eb046c384 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -251,13 +251,14 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit } onToggleEditMode(isEdit: boolean) { - this.isEdit = isEdit; - if (!this.isEdit) { + if (!isEdit) { this.entityComponent.entity = this.entity; if (this.entityTabsComponent) { this.entityTabsComponent.entity = this.entity; } + this.isEdit = isEdit; } else { + this.isEdit = isEdit; this.editingEntity = deepClone(this.entity); this.entityComponent.entity = this.editingEntity; if (this.entityTabsComponent) { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts index 68e4ccc41e..6d3c7e1453 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts @@ -58,14 +58,13 @@ export abstract class EntityComponent, } get isAdd(): boolean { - return this.entityValue && !this.entityValue.id; + return this.entityValue && (!this.entityValue.id || !this.entityValue.id.id); } @Input() set entity(entity: T) { this.entityValue = entity; if (this.entityForm) { - this.entityForm.reset(undefined, {emitEvent: false}); this.entityForm.markAsPristine(); this.updateForm(entity); } @@ -124,7 +123,7 @@ export abstract class EntityComponent, if (isString(obj[curr])) { acc[curr] = obj[curr].trim(); } else if (isObject(obj[curr])) { - acc[curr] = this.deepTrim(obj[curr]) + acc[curr] = this.deepTrim(obj[curr]); } else { acc[curr] = obj[curr]; } diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html index 5e4c597447..d05e4b3775 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html @@ -24,7 +24,8 @@ - diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index e7e1dd5c81..aa264697b6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -39,6 +39,8 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @Input() disabled: boolean; + @Input() allowUserDynamicSource = true; + valueTypeEnum = EntityKeyValueType; booleanFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index fe0b217063..4502e9da67 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -37,6 +37,8 @@ @@ -45,6 +47,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index 09607d1690..8fe9e5a1b6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -32,9 +32,11 @@ import { export interface ComplexFilterPredicateDialogData { complexPredicate: ComplexFilterPredicateInfo; key: string; - disabled: boolean; + readonly: boolean; isAdd: boolean; valueType: EntityKeyValueType; + displayUserParameters: boolean; + allowUserDynamicSource: boolean; } @Component({ @@ -73,6 +75,9 @@ export class ComplexFilterPredicateDialogComponent extends predicates: [this.data.complexPredicate.predicates, [Validators.required]] } ); + if (this.data.readonly) { + this.complexFilterFormGroup.disable({emitEvent: false}); + } } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html index 3157ff4f5b..1a6351be10 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html @@ -19,11 +19,10 @@ filter.complex-filter diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 8a9b5a8531..87bad30d5a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -48,6 +48,10 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @Input() key: string; + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + private propagateChange = null; private complexFilterPredicate: ComplexFilterPredicateInfo; @@ -79,11 +83,13 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - complexPredicate: deepClone(this.complexFilterPredicate), - disabled: this.disabled, + complexPredicate: this.disabled ? this.complexFilterPredicate : deepClone(this.complexFilterPredicate), + readonly: this.disabled, valueType: this.valueType, isAdd: false, - key: this.key + key: this.key, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html index dd42106dd3..ffef5e2349 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html @@ -33,7 +33,8 @@ - +   @@ -50,6 +51,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index d993e973e4..2e59e85123 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -62,6 +62,10 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @Input() operation: ComplexOperation = ComplexOperation.AND; + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + filterListFormGroup: FormGroup; valueTypeEnum = EntityKeyValueType; @@ -150,10 +154,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { complexPredicate: predicate.keyFilterPredicate as ComplexFilterPredicateInfo, - disabled: this.disabled, + readonly: this.disabled, valueType: this.valueType, key: this.key, - isAdd: true + isAdd: true, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource } }).afterClosed().pipe( map((result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html index e9e38c71f5..52f0b0eeea 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -55,7 +55,7 @@ {{'filter.no-dynamic-value' | translate}} - {{dynamicValueSourceTypeTranslations.get(dynamicValueSourceTypeEnum[sourceType]) | translate}} + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} @@ -76,6 +76,7 @@ type="button" matTooltip="{{ (dynamicMode ? 'filter.switch-to-default-value' : 'filter.switch-to-dynamic-value') | translate }}" matTooltipPosition="above" + *ngIf="allow" (click)="dynamicMode = !dynamicMode"> diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts index 548137976e..d12a0fe2f2 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -46,19 +46,34 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @Input() disabled: boolean; + @Input() + set allowUserDynamicSource(allow: boolean) { + this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT, + DynamicValueSourceType.CURRENT_CUSTOMER]; + this.allow = allow; + if (allow) { + this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_USER); + } else { + this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_DEVICE); + } + } + @Input() valueType: EntityKeyValueType; valueTypeEnum = EntityKeyValueType; - dynamicValueSourceTypes = Object.keys(DynamicValueSourceType); - dynamicValueSourceTypeEnum = DynamicValueSourceType; + dynamicValueSourceTypes: DynamicValueSourceType[] = [DynamicValueSourceType.CURRENT_TENANT, + DynamicValueSourceType.CURRENT_CUSTOMER, DynamicValueSourceType.CURRENT_USER]; + dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap; filterPredicateValueFormGroup: FormGroup; dynamicMode = false; + allow = true; + private propagateChange = null; constructor(private fb: FormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html index 27ea3685bb..3b9d89008b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html @@ -19,27 +19,35 @@
- + - + - +
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss new file mode 100644 index 0000000000..3f232f3ce2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-filter-text { + overflow-y: auto; + &.disabled { + opacity: 0.7; + } + &.required { + color: #f44336; + } + } +} + +:host ::ng-deep { + .tb-filter-text { + line-height: 1.8em; + span { + display: inline-block; + vertical-align: middle; + line-height: 1.4em; + } + .tb-filter-predicate { + padding-right: 4px; + padding-left: 4px; + } + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source { + font-weight: bold; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + } + .tb-filter-entity-key, .tb-filter-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + } + .tb-filter-dynamic-source { + } + .tb-filter-entity-key { + color: #305680; + } + .tb-filter-value { + color: #ff5722; + } + .tb-filter-simple-operation { + font-size: 0.9em; + } + .tb-filter-complex-operation { + font-weight: bold; + } + .tb-filter-dynamic-value { + .tb-filter-dynamic-source, .tb-filter-value { + color: #0c959c; + } + } + .tb-filter-bracket { + .tb-left-bracket, .tb-right-bracket { + font-size: 1.2em; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts new file mode 100644 index 0000000000..e6eb5955b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { KeyFilter, keyFiltersToText } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-filter-text', + templateUrl: './filter-text.component.html', + styleUrls: ['./filter-text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextComponent), + multi: true + } + ] +}) +export class FilterTextComponent implements ControlValueAccessor, OnInit { + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + noFilterText = this.translate.instant('filter.no-filter-text'); + + @Input() + addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + + requiredClass = false; + + private filterText: string; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private translate: TranslateService, + private datePipe: DatePipe) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: Array): void { + this.updateFilterText(value); + } + + private updateFilterText(value: Array) { + this.requiredClass = false; + if (value && value.length) { + this.filterText = keyFiltersToText(this.translate, this.datePipe, value); + } else { + if (this.required && !this.disabled) { + this.filterText = this.addFilterPrompt; + this.requiredClass = true; + } else { + this.filterText = this.noFilterText; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html index dbe1d2b623..a8d205479f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html @@ -17,7 +17,7 @@ -->
-

filter.edit-filter-user-params

+

{{(data.readonly ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate}}

diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts index fe4063bb12..58e644f725 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts @@ -23,7 +23,7 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { - BooleanOperation, + BooleanOperation, createDefaultFilterPredicateUserInfo, EntityKeyValueType, generateUserFilterValueLabel, KeyFilterPredicateUserInfo, NumericOperation, StringOperation @@ -35,6 +35,7 @@ export interface FilterUserInfoDialogData { valueType: EntityKeyValueType; operation: StringOperation | BooleanOperation | NumericOperation; keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo; + readonly: boolean; } @Component({ @@ -60,18 +61,24 @@ export class FilterUserInfoDialogComponent extends private translate: TranslateService) { super(store, router, dialogRef); + const userInfo: KeyFilterPredicateUserInfo = this.data.keyFilterPredicateUserInfo || createDefaultFilterPredicateUserInfo(); + this.filterUserInfoFormGroup = this.fb.group( { - editable: [this.data.keyFilterPredicateUserInfo.editable], - label: [this.data.keyFilterPredicateUserInfo.label], - autogeneratedLabel: [this.data.keyFilterPredicateUserInfo.autogeneratedLabel], - order: [this.data.keyFilterPredicateUserInfo.order] + editable: [userInfo.editable], + label: [userInfo.label], + autogeneratedLabel: [userInfo.autogeneratedLabel], + order: [userInfo.order] } ); this.onAutogeneratedLabelChange(); - this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => { - this.onAutogeneratedLabelChange(); - }); + if (!this.data.readonly) { + this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => { + this.onAutogeneratedLabelChange(); + }); + } else { + this.filterUserInfoFormGroup.disable({emitEvent: false}); + } } private onAutogeneratedLabelChange() { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html index bcdae3452f..40cc46be35 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html @@ -17,10 +17,9 @@ --> diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts index 968e6c95e9..5bf5c2a82b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts @@ -76,7 +76,7 @@ export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { this.keyFilterPredicateUserInfo = keyFilterPredicateUserInfo; } - private openFilterUserInfoDialog() { + public openFilterUserInfoDialog() { this.dialog.open(FilterUserInfoDialogComponent, { disableClose: true, @@ -85,7 +85,8 @@ export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { keyFilterPredicateUserInfo: deepClone(this.keyFilterPredicateUserInfo), valueType: this.valueType, key: this.key, - operation: this.operation + operation: this.operation, + readonly: this.disabled } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index e0930b8e7d..f601814920 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{(data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate}}

+

{{(data.isAdd ? 'filter.add-key-filter' : (data.readonly ? 'filter.key-filter' : 'filter.edit-key-filter')) | translate}}

@@ -87,7 +90,7 @@ [disabled]="(isLoading$ | async)" (click)="cancel()" cdkFocusInitial> - {{ 'action.cancel' | translate }} + {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index e475d37f29..202d37be34 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -39,6 +39,10 @@ import { filter, map, startWith } from 'rxjs/operators'; export interface KeyFilterDialogData { keyFilter: KeyFilterInfo; isAdd: boolean; + displayUserParameters: boolean; + allowUserDynamicSource: boolean; + readonly: boolean; + telemetryKeysOnly: boolean; } @Component({ @@ -53,7 +57,10 @@ export class KeyFilterDialogComponent extends keyFilterFormGroup: FormGroup; - entityKeyTypes = [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; + entityKeyTypes = + this.data.telemetryKeysOnly ? + [EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES] : + [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; entityKeyTypeTranslations = entityKeyTypeTranslationMap; @@ -95,32 +102,37 @@ export class KeyFilterDialogComponent extends predicates: [this.data.keyFilter.predicates, [Validators.required]] } ); - this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { - const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; - const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; - if (prevValue && prevValue !== valueType && predicates && predicates.length) { - this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), - this.translate.instant('filter.key-value-type-change-message')).subscribe( - (result) => { - if (result) { - this.keyFilterFormGroup.get('predicates').setValue([]); - } else { - this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false}); + + if (!this.data.readonly) { + this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { + const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; + const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; + if (prevValue && prevValue !== valueType && predicates && predicates.length) { + this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), + this.translate.instant('filter.key-value-type-change-message')).subscribe( + (result) => { + if (result) { + this.keyFilterFormGroup.get('predicates').setValue([]); + } else { + this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false}); + } } - } - ); - } - }); - - this.keyFilterFormGroup.get('key.key').valueChanges.pipe( - filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) - ).subscribe((keyName: string) => { - const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; - const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; - if (prevValueType !== newValueType) { - this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); - } - }); + ); + } + }); + + this.keyFilterFormGroup.get('key.key').valueChanges.pipe( + filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) + ).subscribe((keyName: string) => { + const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; + const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; + if (prevValueType !== newValueType) { + this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); + } + }); + } else { + this.keyFilterFormGroup.disable({emitEvent: false}); + } this.entityFields = entityFields; this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort(); diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html index 213f86b702..26dbaaec4a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -16,65 +16,77 @@ -->
- - - -
filter.key-filters
-
-
-
- -
- - -   -   -
-
- -
-
-
- filter.operation.and + + + + +
filter.key-filters
+
+
+
+ +
+ + +   +  
-
-
-
{{ keyFilterControl.value.key.key }}
-
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
- - +
+ +
+
+
+ filter.operation.and +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
+ + +
+
-
+ filter.no-key-filters +
+
+ +
+ + + + +
filter.preview
+
+
+
+
- filter.no-key-filters -
-
- -
- + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss index 3951b5d640..bc47f04915 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss @@ -26,4 +26,17 @@ color: #666; font-weight: 500; } + .tb-filter-preview { + padding: 8px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + } +} + +:host ::ng-deep { + .tb-filter-preview { + .tb-filter-text { + max-height: 200px; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index 760d455a68..2779a35be9 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -19,13 +19,18 @@ import { AbstractControl, ControlValueAccessor, FormArray, - FormBuilder, + FormBuilder, FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; -import { EntityKeyType, entityKeyTypeTranslationMap, KeyFilterInfo } from '@shared/models/query/query.models'; +import { + EntityKeyType, + entityKeyTypeTranslationMap, + KeyFilter, + KeyFilterInfo, keyFilterInfosToKeyFilters +} from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; import { deepClone } from '@core/utils'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; @@ -46,10 +51,18 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @Input() disabled: boolean; + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + + @Input() telemetryKeysOnly = false; + keyFilterListFormGroup: FormGroup; entityKeyTypeTranslations = entityKeyTypeTranslationMap; + keyFiltersControl: FormControl; + private propagateChange = null; private valueChangeSubscription: Subscription = null; @@ -62,6 +75,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { this.keyFilterListFormGroup = this.fb.group({}); this.keyFilterListFormGroup.addControl('keyFilters', this.fb.array([])); + this.keyFiltersControl = this.fb.control(null); } keyFiltersFormArray(): FormArray { @@ -79,8 +93,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { this.disabled = isDisabled; if (this.disabled) { this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersControl.disable({emitEvent: false}); } else { this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersControl.enable({emitEvent: false}); } } @@ -103,6 +119,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { } else { this.keyFilterListFormGroup.enable({emitEvent: false}); } + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } public removeKeyFilter(index: number) { @@ -147,8 +165,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - keyFilter: keyFilter ? deepClone(keyFilter): null, - isAdd + keyFilter: keyFilter ? (this.disabled ? keyFilter : deepClone(keyFilter)) : null, + isAdd, + readonly: this.disabled, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource, + telemetryKeysOnly: this.telemetryKeysOnly } }).afterClosed(); } @@ -160,5 +182,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { } else { this.propagateChange(null); } + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html index 7a4a9c552b..71cbbd0335 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html @@ -24,7 +24,8 @@ - diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index e9e4f89c3b..32d5779c63 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -40,6 +40,8 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On @Input() disabled: boolean; + @Input() allowUserDynamicSource = true; + @Input() valueType: EntityKeyValueType; numericFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html index ef9f2c01e7..92d470c6b8 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html @@ -28,7 +28,8 @@ - diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index f115748e48..dffd52b274 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -40,6 +40,8 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI @Input() disabled: boolean; + @Input() allowUserDynamicSource = true; + valueTypeEnum = EntityKeyValueType; stringFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index f2b8e1f2d6..8a715c7406 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -84,6 +84,30 @@ import { UserFilterDialogComponent } from '@home/components/filter/user-filter-d import { FilterUserInfoComponent } from './filter/filter-user-info.component'; import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component'; import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component'; +import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component'; +import { TenantProfileComponent } from './profile/tenant-profile.component'; +import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component'; +import { TenantProfileDataComponent } from './profile/tenant-profile-data.component'; +import { DefaultDeviceProfileConfigurationComponent } from './profile/device/default-device-profile-configuration.component'; +import { DeviceProfileConfigurationComponent } from './profile/device/device-profile-configuration.component'; +import { DeviceProfileDataComponent } from './profile/device-profile-data.component'; +import { DeviceProfileComponent } from './profile/device-profile.component'; +import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component'; +import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component'; +import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.component'; +import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component'; +import { MqttDeviceProfileTransportConfigurationComponent } from './profile/device/mqtt-device-profile-transport-configuration.component'; +import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component'; +import { DeviceProfileAlarmsComponent } from './profile/alarm/device-profile-alarms.component'; +import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alarm.component'; +import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; +import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; +import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; +import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; +import { FilterTextComponent } from './filter/filter-text.component'; +import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; +import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; +import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; @NgModule({ declarations: @@ -145,12 +169,36 @@ import { FilterPredicateValueComponent } from './filter/filter-predicate-value.c FilterDialogComponent, FiltersDialogComponent, FilterSelectComponent, + FilterTextComponent, FiltersEditComponent, FiltersEditPanelComponent, UserFilterDialogComponent, FilterUserInfoComponent, FilterUserInfoDialogComponent, - FilterPredicateValueComponent + FilterPredicateValueComponent, + TenantProfileAutocompleteComponent, + TenantProfileDataComponent, + TenantProfileComponent, + TenantProfileDialogComponent, + DeviceProfileAutocompleteComponent, + DefaultDeviceProfileConfigurationComponent, + DeviceProfileConfigurationComponent, + DefaultDeviceProfileTransportConfigurationComponent, + MqttDeviceProfileTransportConfigurationComponent, + Lwm2mDeviceProfileTransportConfigurationComponent, + DeviceProfileTransportConfigurationComponent, + CreateAlarmRulesComponent, + AlarmRuleComponent, + AlarmRuleKeyFiltersDialogComponent, + AlarmRuleConditionComponent, + DeviceProfileAlarmComponent, + DeviceProfileAlarmsComponent, + DeviceProfileDataComponent, + DeviceProfileComponent, + DeviceProfileDialogComponent, + AddDeviceProfileDialogComponent, + RuleChainAutocompleteComponent, + AlarmScheduleComponent ], imports: [ CommonModule, @@ -205,8 +253,32 @@ import { FilterPredicateValueComponent } from './filter/filter-predicate-value.c FilterDialogComponent, FiltersDialogComponent, FilterSelectComponent, + FilterTextComponent, FiltersEditComponent, - UserFilterDialogComponent + UserFilterDialogComponent, + TenantProfileAutocompleteComponent, + TenantProfileDataComponent, + TenantProfileComponent, + TenantProfileDialogComponent, + DeviceProfileAutocompleteComponent, + DefaultDeviceProfileConfigurationComponent, + DeviceProfileConfigurationComponent, + DefaultDeviceProfileTransportConfigurationComponent, + MqttDeviceProfileTransportConfigurationComponent, + Lwm2mDeviceProfileTransportConfigurationComponent, + DeviceProfileTransportConfigurationComponent, + CreateAlarmRulesComponent, + AlarmRuleComponent, + AlarmRuleKeyFiltersDialogComponent, + AlarmRuleConditionComponent, + DeviceProfileAlarmComponent, + DeviceProfileAlarmsComponent, + DeviceProfileDataComponent, + DeviceProfileComponent, + DeviceProfileDialogComponent, + AddDeviceProfileDialogComponent, + RuleChainAutocompleteComponent, + AlarmScheduleComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index 210c04d6b2..53c70ce0ee 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -421,7 +421,7 @@ export class ImportExportService { } public exportJSZip(data: object, filename: string) { - const jsZip: JSZip = new JSZip(); + const jsZip = new JSZip(); for (const keyName in data) { if (data.hasOwnProperty(keyName)) { const valueData = data[keyName]; diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html new file mode 100644 index 0000000000..d7f119a3b7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -0,0 +1,113 @@ + +
+ +

device-profile.add

+ + +
+ + +
+
+ + +
+ {{ 'device-profile.device-profile-details' | translate }} +
+ + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.type-required' | translate }} + + + + device-profile.description + + +
+
+
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.transport-type-required' | translate }} + + + + +
+
+ +
+ {{'device-profile.alarm-rules' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+
+
+
+ + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss new file mode 100644 index 0000000000..cafcce74b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + display: flex; + flex-direction: column; + overflow: hidden; + + .mat-stepper-horizontal { + display: flex; + flex-direction: column; + overflow: hidden; + } + } +} + +:host ::ng-deep { + .mat-dialog-content { + .mat-stepper-horizontal { + .mat-horizontal-content-container { + overflow: auto; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts new file mode 100644 index 0000000000..476c69488f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileType, + deviceProfileTypeTranslationMap, + DeviceTransportType, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MatHorizontalStepper } from '@angular/material/stepper'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; + +export interface AddDeviceProfileDialogData { + deviceProfileName: string; +} + +@Component({ + selector: 'tb-add-device-profile-dialog', + templateUrl: './add-device-profile-dialog.component.html', + providers: [], + styleUrls: ['./add-device-profile-dialog.component.scss'] +}) +export class AddDeviceProfileDialogComponent extends + DialogComponent implements AfterViewInit { + + @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatHorizontalStepper; + + selectedIndex = 0; + + entityType = EntityType; + + deviceProfileTypes = Object.keys(DeviceProfileType); + + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + deviceProfileDetailsFormGroup: FormGroup; + + transportConfigFormGroup: FormGroup; + + alarmRulesFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.deviceProfileDetailsFormGroup = this.fb.group( + { + name: [data.deviceProfileName, [Validators.required]], + type: [DeviceProfileType.DEFAULT, [Validators.required]], + defaultRuleChainId: [null, []], + description: ['', []] + } + ); + this.transportConfigFormGroup = this.fb.group( + { + transportType: [DeviceTransportType.DEFAULT, [Validators.required]], + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), + [Validators.required]] + } + ); + this.transportConfigFormGroup.get('transportType').valueChanges.subscribe(() => { + this.deviceProfileTransportTypeChanged(); + }); + + this.alarmRulesFormGroup = this.fb.group( + { + alarms: [null] + } + ); + } + + private deviceProfileTransportTypeChanged() { + const deviceTransportType: DeviceTransportType = this.transportConfigFormGroup.get('transportType').value; + this.transportConfigFormGroup.patchValue( + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); + } + + ngAfterViewInit(): void { + } + + cancel(): void { + this.dialogRef.close(null); + } + + previousStep() { + this.addDeviceProfileStepper.previous(); + } + + nextStep() { + if (this.selectedIndex < 2) { + this.addDeviceProfileStepper.next(); + } else { + this.add(); + } + } + + selectedForm(): FormGroup { + switch (this.selectedIndex) { + case 0: + return this.deviceProfileDetailsFormGroup; + case 1: + return this.transportConfigFormGroup; + case 2: + return this.alarmRulesFormGroup; + } + } + + private add(): void { + const deviceProfile: DeviceProfile = { + name: this.deviceProfileDetailsFormGroup.get('name').value, + type: this.deviceProfileDetailsFormGroup.get('type').value, + transportType: this.transportConfigFormGroup.get('transportType').value, + description: this.deviceProfileDetailsFormGroup.get('description').value, + profileData: { + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, + alarms: this.alarmRulesFormGroup.get('alarms').value + } + }; + if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) { + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value); + } + this.deviceProfileService.saveDeviceProfile(deviceProfile).subscribe( + (savedDeviceProfile) => { + this.dialogRef.close(savedDeviceProfile); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html new file mode 100644 index 0000000000..170182ded3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -0,0 +1,35 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss new file mode 100644 index 0000000000..1d654bc0f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + a.mat-button { + &:hover, &:focus { + border-bottom: none; + } + } + .tb-alarm-rule-condition { + padding: 8px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + cursor: pointer; + } +} + +:host ::ng-deep { + .tb-alarm-rule-condition { + .tb-filter-text { + max-height: 200px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts new file mode 100644 index 0000000000..7416b944eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -0,0 +1,135 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { KeyFilter } from '@shared/models/query/query.models'; +import { deepClone } from '@core/utils'; +import { + AlarmRuleKeyFiltersDialogComponent, + AlarmRuleKeyFiltersDialogData +} from './alarm-rule-key-filters-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; + +@Component({ + selector: 'tb-alarm-rule-condition', + templateUrl: './alarm-rule-condition.component.html', + styleUrls: ['./alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true, + } + ] +}) +export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + alarmRuleConditionControl: FormControl; + + private modelValue: Array; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private translate: TranslateService, + private datePipe: DatePipe) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleConditionControl = this.fb.control(null); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleConditionControl.disable({emitEvent: false}); + } else { + this.alarmRuleConditionControl.enable({emitEvent: false}); + } + } + + writeValue(value: Array): void { + this.modelValue = value; + this.updateConditionInfo(); + } + + public conditionSet() { + return this.modelValue && this.modelValue.length; + } + + public validate(c: FormControl) { + return this.conditionSet() ? null : { + alarmRuleCondition: { + valid: false, + }, + }; + } + + public openFilterDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open>(AlarmRuleKeyFiltersDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue) + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue = result; + this.updateModel(); + } + }); + } + + private updateConditionInfo() { + this.alarmRuleConditionControl.patchValue(this.modelValue); + } + + private updateModel() { + this.updateConditionInfo(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html new file mode 100644 index 0000000000..26defe62ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html @@ -0,0 +1,56 @@ + +
+ +

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

+ + +
+ + +
+
+
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts new file mode 100644 index 0000000000..a25fe9a1c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; + +export interface AlarmRuleKeyFiltersDialogData { + readonly: boolean; + keyFilters: Array; +} + +@Component({ + selector: 'tb-alarm-rule-key-filters-dialog', + templateUrl: './alarm-rule-key-filters-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}], + styleUrls: [] +}) +export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent> + implements OnInit, ErrorStateMatcher { + + readonly = this.data.readonly; + keyFilters = this.data.keyFilters; + + keyFiltersFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef>, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.keyFiltersFormGroup = this.fb.group({ + keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required] + }); + if (this.readonly) { + this.keyFiltersFormGroup.disable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value); + this.dialogRef.close(this.keyFilters); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html new file mode 100644 index 0000000000..28c286f33c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -0,0 +1,107 @@ + +
+ + + + +
+
+ + device-profile.condition-type + + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + + + {{ 'device-profile.condition-type-required' | translate }} + + +
+ + + + + {{ 'device-profile.condition-duration-value-required' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-pattern' | translate }} + + + + + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + +
+
+ + + + + {{ 'device-profile.condition-repeating-value-required' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-pattern' | translate }} + + +
+
+
+
+ + + + + + + device-profile.alarm-details + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss new file mode 100644 index 0000000000..8af986af69 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .row { + margin-top: 1em; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts new file mode 100644 index 0000000000..7b63660362 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -0,0 +1,185 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-alarm-rule', + templateUrl: './alarm-rule.component.html', + styleUrls: ['./alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true, + } + ] +}) +export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { + + timeUnits = Object.keys(TimeUnit); + timeUnitTranslations = timeUnitTranslationMap; + alarmConditionTypes = Object.keys(AlarmConditionType); + AlarmConditionType = AlarmConditionType; + alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; + + @Input() + disabled: boolean; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + private modelValue: AlarmRule; + + alarmRuleFormGroup: FormGroup; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleFormGroup = this.fb.group({ + condition: this.fb.group({ + condition: [null, Validators.required], + spec: this.fb.group({ + type: [AlarmConditionType.SIMPLE, Validators.required], + unit: [{value: null, disable: true}, Validators.required], + value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], + count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] + }) + }, Validators.required), + schedule: [null], + alarmDetails: [null] + }); + this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { + this.updateValidators(type, true, true); + }); + this.alarmRuleFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRule): void { + this.modelValue = value; + if (this.modelValue?.condition?.spec === null) { + this.modelValue.condition.spec = { + type: AlarmConditionType.SIMPLE + }; + } + this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + this.updateValidators(this.modelValue?.condition?.spec?.type); + } + + public validate(c: FormControl) { + return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { + alarmRule: { + valid: false, + }, + }; + } + + private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { + switch (type) { + case AlarmConditionType.DURATION: + this.alarmRuleFormGroup.get('condition.spec.value').enable(); + this.alarmRuleFormGroup.get('condition.spec.unit').enable(); + this.alarmRuleFormGroup.get('condition.spec.count').disable(); + if (resetDuration) { + this.alarmRuleFormGroup.get('condition.spec').patchValue({ + count: null + }); + } + break; + case AlarmConditionType.REPEATING: + this.alarmRuleFormGroup.get('condition.spec.count').enable(); + this.alarmRuleFormGroup.get('condition.spec.value').disable(); + this.alarmRuleFormGroup.get('condition.spec.unit').disable(); + if (resetDuration) { + this.alarmRuleFormGroup.get('condition.spec').patchValue({ + value: null, + unit: null + }); + } + break; + case AlarmConditionType.SIMPLE: + this.alarmRuleFormGroup.get('condition.spec.value').disable(); + this.alarmRuleFormGroup.get('condition.spec.unit').disable(); + this.alarmRuleFormGroup.get('condition.spec.count').disable(); + if (resetDuration) { + this.alarmRuleFormGroup.get('condition.spec').patchValue({ + value: null, + unit: null, + count: null + }); + } + break; + } + this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent}); + this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent}); + this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent}); + } + + private updateModel() { + const value = this.alarmRuleFormGroup.value; + if (this.modelValue) { + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html new file mode 100644 index 0000000000..cad6e5cf03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html @@ -0,0 +1,224 @@ + +
+ + + + + {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} + + + + {{ 'device-profile.schedule-type-required' | translate }} + + +
+ + +
+
device-profile.schedule-days
+
+
+ + {{ 'device-profile.schedule-day.monday' | translate }} + + + {{ 'device-profile.schedule-day.tuesday' | translate }} + + + {{ 'device-profile.schedule-day.wednesday' | translate }} + + + {{ 'device-profile.schedule-day.thursday' | translate }} + +
+
+ + {{ 'device-profile.schedule-day.friday' | translate }} + + + {{ 'device-profile.schedule-day.saturday' | translate }} + + + {{ 'device-profile.schedule-day.sunday' | translate }} + +
+
+
device-profile.schedule-time
+
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
device-profile.schedule-days
+
+
+
+ + {{ 'device-profile.schedule-day.monday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.tuesday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.wednesday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.thursday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+ + {{ 'device-profile.schedule-day.friday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.saturday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.sunday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts new file mode 100644 index 0000000000..8cf9dc8d30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -0,0 +1,259 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import * as _moment from 'moment-timezone'; +import { MatCheckboxChange } from '@angular/material/checkbox'; + +@Component({ + selector: 'tb-alarm-schedule', + templateUrl: './alarm-schedule.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }] +}) +export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { + @Input() + disabled: boolean; + + alarmScheduleForm: FormGroup; + + defaultTimezone = _moment.tz.guess(); + + alarmScheduleTypes = Object.keys(AlarmScheduleType); + alarmScheduleType = AlarmScheduleType; + alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; + + private modelValue: AlarmSchedule; + + private defaultItems = Array.from({length: 7}, (value, i) => ({ + enabled: true, + dayOfWeek: i + })); + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.alarmScheduleForm = this.fb.group({ + type: [AlarmScheduleType.ANY_TIME, Validators.required], + timezone: [null, Validators.required], + daysOfWeek: this.fb.array(new Array(7).fill(false)), + startsOn: [0, Validators.required], + endsOn: [0, Validators.required], + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i))) + }); + this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { + this.alarmScheduleForm.reset({type, items: this.defaultItems}, {emitEvent: false}); + this.updateValidators(type, true); + this.alarmScheduleForm.updateValueAndValidity(); + }); + this.alarmScheduleForm.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmScheduleForm.disable({emitEvent: false}); + } else { + this.alarmScheduleForm.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmSchedule): void { + this.modelValue = value; + if (!isDefinedAndNotNull(this.modelValue)) { + this.modelValue = { + type: AlarmScheduleType.ANY_TIME + }; + } + switch (this.modelValue.type) { + case AlarmScheduleType.SPECIFIC_TIME: + let daysOfWeek = new Array(7).fill(false); + if (isDefined(this.modelValue.daysOfWeek)) { + daysOfWeek = daysOfWeek.map((item, index) => this.modelValue.daysOfWeek.indexOf(index + 1) > -1); + } + this.alarmScheduleForm.patchValue({ + type: this.modelValue.type, + timezone: this.modelValue.timezone, + daysOfWeek, + startsOn: this.timestampToTime(this.modelValue.startsOn), + endsOn: this.timestampToTime(this.modelValue.endsOn) + }, {emitEvent: false}); + break; + case AlarmScheduleType.CUSTOM: + if (this.modelValue.items) { + const alarmDays = []; + this.modelValue.items + .sort((a, b) => a.dayOfWeek - b.dayOfWeek) + .forEach((item, index) => { + if (item.enabled) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent: false}); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent: false}); + } + alarmDays.push({ + enabled: item.enabled, + startsOn: this.timestampToTime(item.startsOn), + endsOn: this.timestampToTime(item.endsOn) + }); + }); + this.alarmScheduleForm.patchValue({ + type: this.modelValue.type, + timezone: this.modelValue.timezone, + items: alarmDays + }, {emitEvent: false}); + } + break; + default: + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); + } + this.updateValidators(this.modelValue.type); + } + + validate(control: FormGroup): ValidationErrors | null { + return this.alarmScheduleForm.valid ? null : { + alarmScheduler: { + valid: false + } + }; + } + + weeklyRepeatControl(index: number): FormControl { + return (this.alarmScheduleForm.get('daysOfWeek') as FormArray).at(index) as FormControl; + } + + private updateValidators(type: AlarmScheduleType, changedType = false){ + switch (type){ + case AlarmScheduleType.ANY_TIME: + this.alarmScheduleForm.get('timezone').disable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('items').disable({emitEvent: false}); + break; + case AlarmScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').enable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('items').disable({emitEvent: false}); + break; + case AlarmScheduleType.CUSTOM: + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); + if (changedType) { + this.alarmScheduleForm.get('items').enable({emitEvent: false}); + } + break; + } + } + + private updateModel() { + const value = this.alarmScheduleForm.value; + if (this.modelValue) { + if (isDefined(value.daysOfWeek)) { + value.daysOfWeek = value.daysOfWeek + .map((day: boolean, index: number) => day ? index + 1 : null) + .filter(day => !!day); + } + if (isDefined(value.startsOn) && value.startsOn !== 0) { + value.startsOn = this.timeToTimestamp(value.startsOn); + } + if (isDefined(value.endsOn) && value.endsOn !== 0) { + value.endsOn = this.timeToTimestamp(value.endsOn); + } + if (isDefined(value.items)){ + value.items = this.alarmScheduleForm.getRawValue().items; + value.items = value.items.map((item) => { + return { ...item, startsOn: this.timeToTimestamp(item.startsOn), endsOn: this.timeToTimestamp(item.endsOn)}; + }); + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + private timeToTimestamp(date: Date | number): number { + if (typeof date === 'number' || date === null) { + return 0; + } + return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); + } + + private timestampToTime(time = 0): Date { + return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); + } + + private defaultItemsScheduler(index): FormGroup { + return this.fb.group({ + enabled: [true], + dayOfWeek: [index], + startsOn: [0, Validators.required], + endsOn: [0, Validators.required] + }); + } + + changeCustomScheduler($event: MatCheckboxChange, index: number) { + const value = $event.checked; + if (value) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable(); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable(); + } + } + + private get itemsSchedulerForm(): FormArray { + return this.alarmScheduleForm.get('items') as FormArray; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html new file mode 100644 index 0000000000..6886e19152 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -0,0 +1,64 @@ + +
+
+
+ + alarm.severity + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + + {{ 'device-profile.alarm-severity-required' | translate }} + + + + + +
+ +
+
+ device-profile.no-create-alarm-rules +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss new file mode 100644 index 0000000000..b823629076 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .create-alarm-rule { + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding: 8px; + } +} + +:host ::ng-deep { + .mat-form-field.severity { + .mat-form-field-infix { + width: 160px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts new file mode 100644 index 0000000000..6dac1f341e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { Subscription } from 'rxjs'; +import { AlarmSeverity, alarmSeverityTranslations } from '../../../../../shared/models/alarm.models'; + +@Component({ + selector: 'tb-create-alarm-rules', + templateUrl: './create-alarm-rules.component.html', + styleUrls: ['./create-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true, + } + ] +}) +export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator { + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + @Input() + disabled: boolean; + + createAlarmRulesFormGroup: FormGroup; + + private usedSeverities: AlarmSeverity[] = []; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.createAlarmRulesFormGroup = this.fb.group({ + createAlarmRules: this.fb.array([]) + }); + } + + createAlarmRulesFormArray(): FormArray { + return this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const createAlarmRulesControls: Array = []; + if (createAlarmRules) { + Object.keys(createAlarmRules).forEach((severity) => { + const createAlarmRule = createAlarmRules[severity]; + if (severity === 'empty') { + severity = null; + } + createAlarmRulesControls.push(this.fb.group({ + severity: [severity, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + }); + } + this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls)); + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.updateUsedSeverities(); + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + public removeCreateAlarmRule(index: number) { + (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index); + } + + public addCreateAlarmRule() { + const createAlarmRule: AlarmRule = { + condition: { + condition: [] + } + }; + const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + createAlarmRulesArray.push(this.fb.group({ + severity: [null, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + this.createAlarmRulesFormGroup.updateValueAndValidity(); + } + + public validate(c: FormControl) { + return (this.createAlarmRulesFormGroup.valid) ? null : { + createAlarmRules: { + valid: false, + }, + }; + } + + public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean { + const usedIndex = this.usedSeverities.indexOf(severity); + return usedIndex > -1 && usedIndex !== index; + } + + private updateUsedSeverities() { + this.usedSeverities = []; + const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + value.forEach((rule, index) => { + this.usedSeverities[index] = AlarmSeverity[rule.severity]; + }); + } + + private updateModel() { + const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + const createAlarmRules: {[severity: string]: AlarmRule} = {}; + value.forEach(v => { + createAlarmRules[v.severity] = v.alarmRule; + }); + this.updateUsedSeverities(); + this.propagateChange(createAlarmRules); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html new file mode 100644 index 0000000000..d4a72da751 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html @@ -0,0 +1,118 @@ + + + +
+ +
+ {{ alarmFormGroup.get('alarmType').value }} +
+
+ + {{'device-profile.alarm-type' | translate}} + + + {{ 'device-profile.alarm-type-required' | translate }} + + + + + +
+
+
+
device-profile.create-alarm-rules
+ + +
device-profile.clear-alarm-rule
+
+
+ + +
+ +
+
+ device-profile.no-clear-alarm-rule +
+
+ +
+
+ + + +
+
device-profile.advanced-settings
+
+
+
+ + {{ 'device-profile.propagate-alarm' | translate }} + +
+ + device-profile.alarm-rule-relation-types-list + + + {{key}} + close + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss new file mode 100644 index 0000000000..9180f507da --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + display: block; + .clear-alarm-rule { + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding: 8px; + } + .mat-expansion-panel { + box-shadow: none; + &.device-profile-alarm { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 80px; + } + } + } + &.advanced-settings { + border: none; + padding: 0; + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.device-profile-alarm { + .mat-expansion-panel-body { + padding: 0 8px; + } + } + &.advanced-settings { + .mat-expansion-panel-body { + padding: 0; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts new file mode 100644 index 0000000000..7fa60e79d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; + +@Component({ + selector: 'tb-device-profile-alarm', + templateUrl: './device-profile-alarm.component.html', + styleUrls: ['./device-profile-alarm.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true, + } + ] +}) +export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Output() + removeAlarm = new EventEmitter(); + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + expanded = false; + + private modelValue: DeviceProfileAlarm; + + alarmFormGroup: FormGroup; + + private propagateChange = null; + private propagateChangePending = false; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmFormGroup = this.fb.group({ + id: [null, Validators.required], + alarmType: [null, Validators.required], + createRules: [null], + clearRule: [null], + propagate: [null], + propagateRelationTypes: [null] + }); + this.alarmFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmFormGroup.disable({emitEvent: false}); + } else { + this.alarmFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileAlarm): void { + this.propagateChangePending = false; + this.modelValue = value; + if (!this.modelValue.alarmType) { + this.expanded = true; + } + this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + if (!this.disabled && !this.alarmFormGroup.valid) { + this.updateModel(); + } + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + condition: [] + } + }; + this.alarmFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + public removeClearAlarmRule() { + this.alarmFormGroup.patchValue({clearRule: null}); + } + + public validate(c: FormControl) { + return (this.alarmFormGroup.valid) ? null : { + alarm: { + valid: false, + }, + }; + } + + removeRelationType(key: string): void { + const keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); + } + } + + addRelationType(event: MatChipInputEvent): void { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value; + if (!keys || keys.indexOf(value) === -1) { + if (!keys) { + keys = []; + } + keys.push(value); + this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); + } + } + if (input) { + input.value = ''; + } + } + + + private updateModel() { + const value = this.alarmFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html new file mode 100644 index 0000000000..7bd7c546db --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html @@ -0,0 +1,42 @@ + +
+
+
+ + +
+
+
+ device-profile.no-alarm-rules +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss new file mode 100644 index 0000000000..4bb4c03b37 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../scss/constants'; + +:host { + .tb-device-profile-alarms { + overflow-y: auto; + &.mat-padding { + padding: 8px; + @media #{$mat-gt-sm} { + padding: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts new file mode 100644 index 0000000000..632623115e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts @@ -0,0 +1,168 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceProfileAlarm } from '@shared/models/device.models'; +import { guid } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-device-profile-alarms', + templateUrl: './device-profile-alarms.component.html', + styleUrls: ['./device-profile-alarms.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true, + } + ] +}) +export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator { + + deviceProfileAlarmsFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder, + private dialog: MatDialog) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileAlarmsFormGroup = this.fb.group({ + alarms: this.fb.array([]) + }); + } + + alarmsFormArray(): FormArray { + return this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileAlarmsFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileAlarmsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(alarms: Array | null): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const alarmsControls: Array = []; + if (alarms) { + alarms.forEach((alarm) => { + alarmsControls.push(this.fb.control(alarm, [Validators.required])); + }); + } + this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls)); + if (this.disabled) { + this.deviceProfileAlarmsFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileAlarmsFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.deviceProfileAlarmsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + public trackByAlarm(index: number, alarmControl: AbstractControl): string { + if (alarmControl) { + return alarmControl.value.id; + } else { + return null; + } + } + + public removeAlarm(index: number) { + (this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray).removeAt(index); + } + + public addAlarm() { + const alarm: DeviceProfileAlarm = { + id: guid(), + alarmType: '', + createRules: { + empty: { + condition: { + condition: [] + } + } + } + }; + const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; + alarmsArray.push(this.fb.control(alarm, [Validators.required])); + this.deviceProfileAlarmsFormGroup.updateValueAndValidity(); + } + + public validate(c: FormControl) { + return (this.deviceProfileAlarmsFormGroup.valid) ? null : { + alarms: { + valid: false, + }, + }; + } + + private updateModel() { + const alarms: Array = this.deviceProfileAlarmsFormGroup.get('alarms').value; + this.propagateChange(alarms); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html new file mode 100644 index 0000000000..a8af9c2738 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -0,0 +1,69 @@ + + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('device-profile.no-device-profiles-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + device-profile.create-new-device-profile + +
+
+
+ + {{ 'device-profile.device-profile-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts new file mode 100644 index 0000000000..c419b7f11c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -0,0 +1,343 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, NgZone, + OnInit, + Output, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { entityIdEquals } from '@shared/models/id/entity-id'; +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; +import { ENTER } from '@angular/cdk/keycodes'; +import { MatDialog } from '@angular/material/dialog'; +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileInfo, + DeviceProfileType, + DeviceTransportType +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component'; + +@Component({ + selector: 'tb-device-profile-autocomplete', + templateUrl: './device-profile-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), + multi: true + }] +}) +export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectDeviceProfileFormGroup: FormGroup; + + modelValue: DeviceProfileId | null; + + @Input() + selectDefaultProfile = false; + + @Input() + displayAllOnEmpty = false; + + @Input() + editProfileEnabled = true; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Output() + deviceProfileUpdated = new EventEmitter(); + + @Output() + deviceProfileChanged = new EventEmitter(); + + @ViewChild('deviceProfileInput', {static: true}) deviceProfileInput: ElementRef; + + @ViewChild('deviceProfileAutocomplete', {static: true}) deviceProfileAutocomplete: MatAutocomplete; + + filteredDeviceProfiles: Observable>; + + searchText = ''; + + private dirty = false; + + private ignoreClosedPanel = false; + + private allDeviceProfile: DeviceProfileInfo = { + name: this.translate.instant('device-profile.all-device-profiles'), + type: DeviceProfileType.DEFAULT, + transportType: DeviceTransportType.DEFAULT, + id: null + }; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder, + private zone: NgZone, + private dialog: MatDialog) { + this.selectDeviceProfileFormGroup = this.fb.group({ + deviceProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredDeviceProfiles = this.selectDeviceProfileFormGroup.get('deviceProfile').valueChanges + .pipe( + tap((value: DeviceProfileInfo | string) => { + let modelValue: DeviceProfileInfo | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + if (!this.displayAllOnEmpty || modelValue) { + this.updateView(modelValue); + } + }), + map(value => { + if (value) { + if (typeof value === 'string') { + return value; + } else { + if (this.displayAllOnEmpty && value === this.allDeviceProfile) { + return ''; + } else { + return value.name; + } + } + } else { + return ''; + } + }), + mergeMap(name => this.fetchDeviceProfiles(name) ), + share() + ); + } + + selectDefaultDeviceProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe( + (profile) => { + if (profile) { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); + this.updateView(profile); + } + } + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: DeviceProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new DeviceProfileId(profile.id.id); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); + this.deviceProfileChanged.emit(profile); + } + ); + } else if (this.displayAllOnEmpty) { + this.modelValue = null; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(this.allDeviceProfile, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: false}); + this.selectDefaultDeviceProfileIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectDeviceProfileFormGroup.get('deviceProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + onPanelClosed() { + if (this.ignoreClosedPanel) { + this.ignoreClosedPanel = false; + } else { + if (this.displayAllOnEmpty && !this.selectDeviceProfileFormGroup.get('deviceProfile').value) { + this.zone.run(() => { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(this.allDeviceProfile, {emitEvent: true}); + }, 0); + } + } + } + + updateView(deviceProfile: DeviceProfileInfo | null) { + const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null; + if (!entityIdEquals(this.modelValue, idValue)) { + this.modelValue = idValue; + this.propagateChange(this.modelValue); + this.deviceProfileChanged.emit(deviceProfile); + } + } + + displayDeviceProfileFn(profile?: DeviceProfileInfo): string | undefined { + return profile ? profile.name : undefined; + } + + fetchDeviceProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe( + map(pageData => { + let data = pageData.data; + if (this.displayAllOnEmpty) { + data = [this.allDeviceProfile, ...data]; + } + return data; + }) + ); + } + + clear() { + this.ignoreClosedPanel = true; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.deviceProfileInput.nativeElement.blur(); + this.deviceProfileInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + deviceProfileEnter($event: KeyboardEvent) { + if (this.editProfileEnabled && $event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createDeviceProfile($event, this.searchText); + } + } + } + + createDeviceProfile($event: Event, profileName: string) { + $event.preventDefault(); + const deviceProfile: DeviceProfile = { + name: profileName + } as DeviceProfile; + this.openDeviceProfileDialog(deviceProfile, true); + } + + editDeviceProfile($event: Event) { + $event.preventDefault(); + this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe( + (deviceProfile) => { + this.openDeviceProfileDialog(deviceProfile, false); + } + ); + } + + openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { + let deviceProfileObservable: Observable; + if (!isAdd) { + deviceProfileObservable = this.dialog.open(DeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: false, + deviceProfile + } + }).afterClosed(); + } else { + deviceProfileObservable = this.dialog.open(AddDeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceProfileName: deviceProfile.name + } + }).afterClosed(); + } + deviceProfileObservable.subscribe( + (savedDeviceProfile) => { + if (!savedDeviceProfile) { + setTimeout(() => { + this.deviceProfileInput.nativeElement.blur(); + this.deviceProfileInput.nativeElement.focus(); + }, 0); + } else { + this.deviceProfileService.getDeviceProfileInfo(savedDeviceProfile.id.id).subscribe( + (profile) => { + this.modelValue = new DeviceProfileId(profile.id.id); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); + if (isAdd) { + this.propagateChange(this.modelValue); + } else { + this.deviceProfileUpdated.next(savedDeviceProfile.id); + } + this.deviceProfileChanged.emit(profile); + } + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html new file mode 100644 index 0000000000..daae838285 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html @@ -0,0 +1,55 @@ + +
+ + + + +
device-profile.profile-configuration
+
+
+ + +
+ + + +
device-profile.transport-configuration
+
+
+ + +
+ + + +
{{'device-profile.alarm-rules' | translate: + {count: deviceProfileDataFormGroup.get('alarms').value ? + deviceProfileDataFormGroup.get('alarms').value.length : 0} }}
+
+
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts new file mode 100644 index 0000000000..7d7fc55057 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts @@ -0,0 +1,110 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceProfileData, + DeviceProfileType, + deviceProfileTypeConfigurationInfoMap, + DeviceTransportType, deviceTransportTypeConfigurationInfoMap +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-profile-data', + templateUrl: './device-profile-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileDataComponent), + multi: true + }] +}) +export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit { + + deviceProfileDataFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + displayProfileConfiguration: boolean; + displayTransportConfiguration: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileDataFormGroup = this.fb.group({ + configuration: [null, Validators.required], + transportConfiguration: [null, Validators.required], + alarms: [null] + }); + this.deviceProfileDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileDataFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileData | null): void { + const deviceProfileType = value?.configuration?.type; + this.displayProfileConfiguration = deviceProfileType && + deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasProfileConfiguration; + const deviceTransportType = value?.transportConfiguration?.type; + this.displayTransportConfiguration = deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration; + this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + this.deviceProfileDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); + this.deviceProfileDataFormGroup.patchValue({alarms: value?.alarms}, {emitEvent: false}); + } + + private updateModel() { + let deviceProfileData: DeviceProfileData = null; + if (this.deviceProfileDataFormGroup.valid) { + deviceProfileData = this.deviceProfileDataFormGroup.getRawValue(); + } + this.propagateChange(deviceProfileData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html new file mode 100644 index 0000000000..ad7282e7ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts new file mode 100644 index 0000000000..b3e96c228d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DeviceProfile } from '@shared/models/device.models'; +import { DeviceProfileComponent } from './device-profile.component'; +import { DeviceProfileService } from '@core/http/device-profile.service'; + +export interface DeviceProfileDialogData { + deviceProfile: DeviceProfile; + isAdd: boolean; +} + +@Component({ + selector: 'tb-device-profile-dialog', + templateUrl: './device-profile-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileDialogComponent}], + styleUrls: [] +}) +export class DeviceProfileDialogComponent extends + DialogComponent implements ErrorStateMatcher, AfterViewInit { + + isAdd: boolean; + deviceProfile: DeviceProfile; + + submitted = false; + + @ViewChild('deviceProfileComponent', {static: true}) deviceProfileComponent: DeviceProfileComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DeviceProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private deviceProfileService: DeviceProfileService) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.deviceProfile = this.data.deviceProfile; + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.deviceProfileComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.deviceProfileComponent.entityForm.valid) { + this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()}; + this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe( + (deviceProfile) => { + this.dialogRef.close(deviceProfile); + } + ); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html new file mode 100644 index 0000000000..8af17883c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -0,0 +1,88 @@ + +
+ + +
+ +
+
+
+
+
+ + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.type-required' | translate }} + + + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.transport-type-required' | translate }} + + + + + + device-profile.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts new file mode 100644 index 0000000000..e3c441c8a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -0,0 +1,155 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, Input, Optional } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityComponent } from '../entity/entity.component'; +import { + createDeviceProfileConfiguration, + DeviceProfile, + DeviceProfileData, + DeviceProfileType, + deviceProfileTypeTranslationMap, + DeviceTransportType, + deviceTransportTypeTranslationMap, + createDeviceProfileTransportConfiguration +} from '@shared/models/device.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; + +@Component({ + selector: 'tb-device-profile', + templateUrl: './device-profile.component.html', + styleUrls: [] +}) +export class DeviceProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + entityType = EntityType; + + deviceProfileTypes = Object.keys(DeviceProfileType); + + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + constructor(protected store: Store, + protected translate: TranslateService, + @Optional() @Inject('entity') protected entityValue: DeviceProfile, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: DeviceProfile): FormGroup { + const form = this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity ? entity.type : null, [Validators.required]], + transportType: [entity ? entity.transportType : null, [Validators.required]], + profileData: [entity && !this.isAdd ? entity.profileData : {}, []], + defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], + description: [entity ? entity.description : '', []], + } + ); + form.get('type').valueChanges.subscribe(() => { + this.deviceProfileTypeChanged(form); + }); + form.get('transportType').valueChanges.subscribe(() => { + this.deviceProfileTransportTypeChanged(form); + }); + this.checkIsNewDeviceProfile(entity, form); + return form; + } + + private checkIsNewDeviceProfile(entity: DeviceProfile, form: FormGroup) { + if (entity && !entity.id) { + form.get('type').patchValue(DeviceProfileType.DEFAULT, {emitEvent: true}); + form.get('transportType').patchValue(DeviceTransportType.DEFAULT, {emitEvent: true}); + } + } + + private deviceProfileTypeChanged(form: FormGroup) { + const deviceProfileType: DeviceProfileType = form.get('type').value; + let profileData: DeviceProfileData = form.getRawValue().profileData; + if (!profileData) { + profileData = { + configuration: null, + transportConfiguration: null + }; + } + profileData.configuration = createDeviceProfileConfiguration(deviceProfileType); + form.patchValue({profileData}); + } + + private deviceProfileTransportTypeChanged(form: FormGroup) { + const deviceTransportType: DeviceTransportType = form.get('transportType').value; + let profileData: DeviceProfileData = form.getRawValue().profileData; + if (!profileData) { + profileData = { + configuration: null, + transportConfiguration: null + }; + } + profileData.transportConfiguration = createDeviceProfileTransportConfiguration(deviceTransportType); + form.patchValue({profileData}); + } + + updateForm(entity: DeviceProfile) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({type: entity.type}, {emitEvent: false}); + this.entityForm.patchValue({transportType: entity.transportType}, {emitEvent: false}); + this.entityForm.patchValue({profileData: entity.profileData}); + this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}); + this.entityForm.patchValue({description: entity.description}); + } + + prepareFormValue(formValue: any): any { + if (formValue.defaultRuleChainId) { + formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId); + } + return formValue; + } + + onDeviceProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html new file mode 100644 index 0000000000..200d3d3623 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts new file mode 100644 index 0000000000..211cd5ada2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceProfileConfiguration, + DeviceProfileConfiguration, + DeviceProfileType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-profile-configuration', + templateUrl: './default-device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceProfileConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceProfileConfiguration | null): void { + this.defaultDeviceProfileConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileConfiguration = null; + if (this.defaultDeviceProfileConfigurationFormGroup.valid) { + configuration = this.defaultDeviceProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceProfileType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..94890f3673 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..1d620f7b6e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceProfileTransportConfiguration, + DeviceProfileTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-profile-transport-configuration', + templateUrl: './default-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceProfileTransportConfiguration | null): void { + this.defaultDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html new file mode 100644 index 0000000000..3b7879b933 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts new file mode 100644 index 0000000000..766c78fb62 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-profile-configuration', + templateUrl: './device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileConfigurationComponent), + multi: true + }] +}) +export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceProfileType = DeviceProfileType; + + deviceProfileConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + type: DeviceProfileType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceProfileConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileConfiguration = null; + if (this.deviceProfileConfigurationFormGroup.valid) { + configuration = this.deviceProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..001502cd83 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html @@ -0,0 +1,39 @@ + +
+
+ + + + + + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..e85aebdd89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-profile-transport-configuration', + templateUrl: './device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class DeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceTransportType = DeviceTransportType; + + deviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + transportType: DeviceTransportType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileTransportConfiguration | null): void { + this.transportType = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.deviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.deviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.transportType; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..03530c2b2e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..ed81a143fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceProfileTransportConfiguration, + DeviceTransportType, Lwm2mDeviceProfileTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-lwm2m-device-profile-transport-configuration', + templateUrl: './lwm2m-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + lwm2mDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.lwm2mDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.lwm2mDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Lwm2mDeviceProfileTransportConfiguration | null): void { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.lwm2mDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.lwm2mDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.LWM2M; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..00ff4760ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -0,0 +1,72 @@ + +
+
+
+ device-profile.mqtt-device-topic-filters +
+ + device-profile.mqtt-device-payload-type + + + {{mqttTransportPayloadTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.mqtt-payload-type-required' | translate }} + + +
+ + device-profile.telemetry-topic-filter + + + {{ 'device-profile.telemetry-topic-filter-required' | translate}} + + + {{ 'device-profile.not-valid-single-character' | translate}} + + + {{ 'device-profile.not-valid-multi-character' | translate}} + + + + device-profile.attributes-topic-filter + + + {{ 'device-profile.attributes-topic-filter-required' | translate}} + + + {{ 'device-profile.not-valid-single-character' | translate}} + + + {{ 'device-profile.not-valid-multi-character' | translate}} + + +
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss new file mode 100644 index 0000000000..4e9bfe914b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + .fields-group { + padding: 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + } + + .tb-hint{ + padding: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..18dc1b2bf4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -0,0 +1,150 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALUE_ACCESSOR, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + MqttTransportPayloadType, + DeviceProfileTransportConfiguration, + DeviceTransportType, + MqttDeviceProfileTransportConfiguration, mqttTransportPayloadTypeTranslationMap +} from '@shared/models/device.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-mqtt-device-profile-transport-configuration', + templateUrl: './mqtt-device-profile-transport-configuration.component.html', + styleUrls: ['./mqtt-device-profile-transport-configuration.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + mqttTransportPayloadTypes = Object.keys(MqttTransportPayloadType); + + mqttTransportPayloadTypeTranslations = mqttTransportPayloadTypeTranslationMap; + + + mqttDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: this.fb.group({ + deviceAttributesTopic: [null, [Validators.required, this.validationMQTTTopic()]], + deviceTelemetryTopic: [null, [Validators.required, this.validationMQTTTopic()]], + transportPayloadType: [MqttTransportPayloadType.JSON, Validators.required] + }) + }); + this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.mqttDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.mqttDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MqttDeviceProfileTransportConfiguration | null): void { + if (isDefinedAndNotNull(value)) { + this.mqttDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.MQTT; + } + this.propagateChange(configuration); + } + + private validationMQTTTopic(): ValidatorFn { + return (c: FormControl) => { + const newTopic = c.value; + const wildcardSymbols = /[#+]/g; + let findSymbol = wildcardSymbols.exec(newTopic); + while (findSymbol) { + const index = findSymbol.index; + const currentSymbol = findSymbol[0]; + const prevSymbol = index > 0 ? newTopic[index - 1] : null; + const nextSymbol = index < (newTopic.length - 1) ? newTopic[index + 1] : null; + if (currentSymbol === '#' && (index !== (newTopic.length - 1) || (prevSymbol !== null && prevSymbol !== '/'))) { + return { + invalidMultiTopicCharacter: { + valid: false + } + }; + } + if (currentSymbol === '+' && ((prevSymbol !== null && prevSymbol !== '/') || (nextSymbol !== null && nextSymbol !== '/'))) { + return { + invalidSingleTopicCharacter: { + valid: false + } + }; + } + findSymbol = wildcardSymbols.exec(newTopic); + } + return null; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html new file mode 100644 index 0000000000..9dd73c5efe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html @@ -0,0 +1,68 @@ + + + + + + + + + + +
+
+ tenant-profile.no-tenant-profiles-found +
+ + + {{ translate.get('tenant-profile.no-tenant-profiles-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + tenant-profile.create-new-tenant-profile + +
+
+
+ + {{ 'tenant-profile.tenant-profile-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts new file mode 100644 index 0000000000..3cdb0b3a4e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts @@ -0,0 +1,254 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, share, startWith, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; +import { EntityInfoData } from '@shared/models/entity.models'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; +import { entityIdEquals } from '@shared/models/id/entity-id'; +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; +import { ENTER } from '@angular/cdk/keycodes'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { MatDialog } from '@angular/material/dialog'; +import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component'; + +@Component({ + selector: 'tb-tenant-profile-autocomplete', + templateUrl: './tenant-profile-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileAutocompleteComponent), + multi: true + }] +}) +export class TenantProfileAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectTenantProfileFormGroup: FormGroup; + + modelValue: TenantProfileId | null; + + @Input() + selectDefaultProfile = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Output() + tenantProfileUpdated = new EventEmitter(); + + @ViewChild('tenantProfileInput', {static: true}) tenantProfileInput: ElementRef; + + filteredTenantProfiles: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private tenantProfileService: TenantProfileService, + private fb: FormBuilder, + private dialog: MatDialog) { + this.selectTenantProfileFormGroup = this.fb.group({ + tenantProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredTenantProfiles = this.selectTenantProfileFormGroup.get('tenantProfile').valueChanges + .pipe( + tap((value: EntityInfoData | string) => { + let modelValue: TenantProfileId | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = new TenantProfileId(value.id.id); + } + this.updateView(modelValue); + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchTenantProfiles(name) ), + share() + ); + } + + selectDefaultTenantProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.tenantProfileService.getDefaultTenantProfileInfo().subscribe( + (profile) => { + if (profile) { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); + this.propagateChange(this.modelValue); + } + } + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: TenantProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); + } + ); + } else { + this.modelValue = null; + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: false}); + this.selectDefaultTenantProfileIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectTenantProfileFormGroup.get('tenantProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: TenantProfileId | null) { + if (!entityIdEquals(this.modelValue, value)) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayTenantProfileFn(profile?: EntityInfoData): string | undefined { + return profile ? profile.name : undefined; + } + + fetchTenantProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.tenantProfileService.getTenantProfileInfos(pageLink, {ignoreLoading: true}).pipe( + map(pageData => { + return pageData.data; + }) + ); + } + + clear() { + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.tenantProfileInput.nativeElement.blur(); + this.tenantProfileInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + tenantProfileEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createTenantProfile($event, this.searchText); + } + } + } + + createTenantProfile($event: Event, profileName: string) { + $event.preventDefault(); + const tenantProfile: TenantProfile = { + id: null, + name: profileName + }; + this.openTenantProfileDialog(tenantProfile, true); + } + + editTenantProfile($event: Event) { + $event.preventDefault(); + this.tenantProfileService.getTenantProfile(this.modelValue.id).subscribe( + (tenantProfile) => { + this.openTenantProfileDialog(tenantProfile, false); + } + ); + } + + openTenantProfileDialog(tenantProfile: TenantProfile, isAdd: boolean) { + this.dialog.open(TenantProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + tenantProfile + } + }).afterClosed().subscribe( + (savedTenantProfile) => { + if (!savedTenantProfile) { + setTimeout(() => { + this.tenantProfileInput.nativeElement.blur(); + this.tenantProfileInput.nativeElement.focus(); + }, 0); + } else { + this.tenantProfileService.getTenantProfileInfo(savedTenantProfile.id.id).subscribe( + (profile) => { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: true}); + if (isAdd) { + this.propagateChange(this.modelValue); + } else { + this.tenantProfileUpdated.next(savedTenantProfile.id); + } + } + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html new file mode 100644 index 0000000000..a3d504d0e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts new file mode 100644 index 0000000000..377b6f0978 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TenantProfileData } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-profile-data', + templateUrl: './tenant-profile-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileDataComponent), + multi: true + }] +}) +export class TenantProfileDataComponent implements ControlValueAccessor, OnInit { + + tenantProfileDataFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.tenantProfileDataFormGroup = this.fb.group({ + tenantProfileData: [null, Validators.required] + }); + this.tenantProfileDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.tenantProfileDataFormGroup.disable({emitEvent: false}); + } else { + this.tenantProfileDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TenantProfileData | null): void { + this.tenantProfileDataFormGroup.get('tenantProfileData').patchValue(value, {emitEvent: false}); + } + + private updateModel() { + let tenantProfileData: TenantProfileData = null; + if (this.tenantProfileDataFormGroup.valid) { + tenantProfileData = this.tenantProfileDataFormGroup.getRawValue().tenantProfileData; + } + this.propagateChange(tenantProfileData); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html new file mode 100644 index 0000000000..79bf7983a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (isAdd ? 'tenant-profile.add' : 'tenant-profile.edit' ) | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts new file mode 100644 index 0000000000..8134c4934d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { TenantProfileComponent } from './tenant-profile.component'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; + +export interface TenantProfileDialogData { + tenantProfile: TenantProfile; + isAdd: boolean; +} + +@Component({ + selector: 'tb-tenant-profile-dialog', + templateUrl: './tenant-profile-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: TenantProfileDialogComponent}], + styleUrls: [] +}) +export class TenantProfileDialogComponent extends + DialogComponent implements ErrorStateMatcher, AfterViewInit { + + isAdd: boolean; + tenantProfile: TenantProfile; + + submitted = false; + + @ViewChild('tenantProfileComponent', {static: true}) tenantProfileComponent: TenantProfileComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: TenantProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private tenantProfileService: TenantProfileService) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.tenantProfile = this.data.tenantProfile; + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.tenantProfileComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.tenantProfileComponent.entityForm.valid) { + this.tenantProfile = {...this.tenantProfile, ...this.tenantProfileComponent.entityFormValue()}; + this.tenantProfileService.saveTenantProfile(this.tenantProfile).subscribe( + (tenantProfile) => { + this.dialogRef.close(tenantProfile); + } + ); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html new file mode 100644 index 0000000000..744e460ed8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -0,0 +1,72 @@ + +
+ + +
+ +
+
+
+
+
+ + tenant-profile.name + + + {{ 'tenant-profile.name-required' | translate }} + + +
+ +
{{ 'tenant.isolated-tb-core' | translate }}
+
{{'tenant.isolated-tb-core-details' | translate}}
+
+ +
{{ 'tenant.isolated-tb-rule-engine' | translate }}
+
{{'tenant.isolated-tb-rule-engine-details' | translate}}
+
+
+ + + + tenant-profile.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss rename to ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts new file mode 100644 index 0000000000..2aadb551de --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, Input, Optional } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TenantProfile } from '@app/shared/models/tenant.model'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityComponent } from '../entity/entity.component'; + +@Component({ + selector: 'tb-tenant-profile', + templateUrl: './tenant-profile.component.html', + styleUrls: ['./tenant-profile.component.scss'] +}) +export class TenantProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + constructor(protected store: Store, + protected translate: TranslateService, + @Optional() @Inject('entity') protected entityValue: TenantProfile, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: TenantProfile): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], + isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], + profileData: [entity && !this.isAdd ? entity.profileData : {}, []], + description: [entity ? entity.description : '', []], + } + ); + } + + updateForm(entity: TenantProfile) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); + this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); + this.entityForm.patchValue({profileData: entity.profileData}); + this.entityForm.patchValue({description: entity.description}); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + if (!this.isAdd) { + this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); + this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); + } + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + onTenantProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('tenant-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html new file mode 100644 index 0000000000..626b82aad3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -0,0 +1,57 @@ + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('rulechain.no-rulechains-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + rulechain.create-new-rulechain + +
+
+
+ + {{ 'rulechain.rulechain-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts new file mode 100644 index 0000000000..53449aad10 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -0,0 +1,209 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityService } from '@core/http/entity.service'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; + +@Component({ + selector: 'tb-rule-chain-autocomplete', + templateUrl: './rule-chain-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleChainAutocompleteComponent), + multi: true + }] +}) +export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectRuleChainFormGroup: FormGroup; + + ruleChainLabel = 'rulechain.rulechain'; + + modelValue: string | null; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('ruleChainInput', {static: true}) ruleChainInput: ElementRef; + @ViewChild('ruleChainInput', {read: MatAutocompleteTrigger}) ruleChainAutocomplete: MatAutocompleteTrigger; + + filteredRuleChains: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private entityService: EntityService, + private ruleChainService: RuleChainService, + private fb: FormBuilder) { + this.selectRuleChainFormGroup = this.fb.group({ + ruleChainId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredRuleChains = this.selectRuleChainFormGroup.get('ruleChainId').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchRuleChain(name) ), + share() + ); + } + + ngAfterViewInit(): void {} + + getCurrentEntity(): BaseData | null { + const currentRuleChain = this.selectRuleChainFormGroup.get('ruleChainId').value; + if (currentRuleChain && typeof currentRuleChain !== 'string') { + return currentRuleChain as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectRuleChainFormGroup.disable({emitEvent: false}); + } else { + this.selectRuleChainFormGroup.enable({emitEvent: false}); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + writeValue(value: string | null): void { + this.searchText = ''; + if (value != null) { + const targetEntityType = EntityType.RULE_CHAIN; + this.entityService.getEntity(targetEntityType, value, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + this.modelValue = null; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectRuleChainFormGroup.get('ruleChainId').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayRuleChainFn(ruleChain?: BaseData): string | undefined { + return ruleChain ? ruleChain.name : undefined; + } + + fetchRuleChain(searchText?: string): Observable>> { + this.searchText = searchText; + return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, + 50, null, {ignoreLoading: true}); + } + + clear() { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.ruleChainInput.nativeElement.blur(); + this.ruleChainInput.nativeElement.focus(); + }, 0); + } + + createDefaultRuleChain($event: Event, ruleChainName: string) { + $event.preventDefault(); + this.ruleChainAutocomplete.closePanel(); + this.ruleChainService.createDefaultRuleChain(ruleChainName).subscribe((ruleChain) => { + this.updateView(ruleChain.id.id); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss index 32d865eaa4..6766f2e978 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss @@ -51,7 +51,6 @@ label { padding: 4px; color: #00acc1; - text-transform: uppercase; background: rgba(220, 220, 220, .35); border-radius: 5px; &:not(:last-child) { diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index f6c9f29eb1..dc1a48c3c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -41,6 +41,7 @@ import { CustomDialogService } from '@home/components/widget/dialog/custom-dialo import { DatePipe } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; import { DomSanitizer } from '@angular/platform-browser'; +import { Router } from '@angular/router'; @Directive() export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { @@ -77,6 +78,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.ctx.translate = $injector.get(TranslateService); this.ctx.http = $injector.get(HttpClient); this.ctx.sanitizer = $injector.get(DomSanitizer); + this.ctx.router = $injector.get(Router); this.ctx.$scope = this; if (this.ctx.defaultSubscription) { diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/legend.component.html index 3d0eda2856..272e5fae64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.html @@ -32,7 +32,7 @@ + [ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden, 'tb-horizontal': isHorizontal }"> {{ legendKey.dataKey.label }} {{ legendData.data[legendKey.dataIndex].min }} @@ -47,7 +47,7 @@ + [ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden}"> {{ legendKey.dataKey.label }} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts index b47ba317c0..55fffa2b5b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts @@ -52,8 +52,9 @@ export class LegendComponent implements OnInit { } toggleHideData(index: number) { - if (!this.legendData.keys[index].dataKey.settings.disableDataHiding) { - this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden; + const dataKey = this.legendData.keys.find(key => key.dataIndex === index).dataKey; + if (!dataKey.settings.disableDataHiding) { + dataKey.hidden = !dataKey.hidden; this.legendKeyHiddenChange.emit(index); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 7e9a67c485..d269fbbe92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -61,7 +61,7 @@
- diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index d21fbe9c77..26055c55b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -247,11 +247,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } public onDataUpdated() { - this.ngZone.run(() => { - this.updateTitle(true); - this.alarmsDatasource.updateAlarms(); - this.ctx.detectChanges(); - }); + this.updateTitle(true); + this.alarmsDatasource.updateAlarms(); } public pageLinkSortDirection(): SortDirection { @@ -565,6 +562,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return column.def; } + public trackByRowIndex(index: number) { + return index; + } + public headerStyle(key: EntityColumn): any { const columnWidth = this.columnWidth[key.def]; return widthStyle(columnWidth); @@ -606,7 +607,19 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } else { content = this.defaultContent(key, contentInfo, value); } - return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + } else { return ''; } @@ -804,7 +817,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, const alarmField = alarmFields[key.name]; if (alarmField) { if (alarmField.time) { - return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss'); + return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : ''; } else if (alarmField.value === alarmFields.severity.value) { return this.translate.instant(alarmSeverityTranslations.get(value)); } else if (alarmField.value === alarmFields.status.value) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts index 89c12860dd..564065e9be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts @@ -163,7 +163,7 @@ export const analogueCompassSettingsSchema: JsonSettingsSchema = { form: [ { key: 'majorTicks', - items:[ + items: [ 'majorTicks[]' ] }, @@ -267,7 +267,7 @@ export const analogueCompassSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts index 11da2efd90..d3a8db18a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts @@ -43,7 +43,7 @@ export class TbAnalogueCompass extends TbBaseGauge 0) ? deepClone(settings.majorTicks) : - ['N','NE','E','SE','S','SW','W','NW']; + ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']; majorTicks.push(majorTicks[0]); return { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index 19e6525a91..7038a2f18c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -492,7 +492,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -581,7 +581,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -670,7 +670,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -759,7 +759,7 @@ export const analogueGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -842,7 +842,7 @@ export abstract class TbBaseGauge { private gauge: BaseGauge; protected constructor(protected ctx: WidgetContext, canvasId: string) { - const gaugeElement = $('#'+canvasId, ctx.$container)[0]; + const gaugeElement = $('#' + canvasId, ctx.$container)[0]; const settings: S = ctx.settings; const gaugeData: O = this.createGaugeOptions(gaugeElement, settings); this.gauge = this.createGauge(gaugeData as O).draw(); @@ -859,7 +859,7 @@ export abstract class TbBaseGauge { const tvPair = cellData.data[cellData.data.length - 1]; const value = tvPair[1]; - if(value !== this.gauge.value) { + if (value !== this.gauge.value) { this.gauge.value = value; } } @@ -876,10 +876,10 @@ export abstract class TbBaseGauge { } } -export abstract class TbAnalogueGauge extends TbBaseGauge { +export abstract class TbAnalogueGauge extends TbBaseGauge { protected constructor(ctx: WidgetContext, canvasId: string) { - super(ctx,canvasId); + super(ctx, canvasId); } protected createGaugeOptions(gaugeElement: HTMLElement, settings: S): O { @@ -891,26 +891,26 @@ export abstract class TbAnalogueGauge{ +export class TbAnalogueLinearGauge extends TbAnalogueGauge{ static get settingsSchema(): JsonSettingsSchema { return analogueLinearGaugeSettingsSchemaValue; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts index 1571c5c3d0..05c1969657 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts @@ -28,7 +28,7 @@ import BaseGauge = CanvasGauges.BaseGauge; const analogueRadialGaugeSettingsSchemaValue = getAnalogueRadialGaugeSettingsSchema(); -export class TbAnalogueRadialGauge extends TbAnalogueGauge{ +export class TbAnalogueRadialGauge extends TbAnalogueGauge{ static get settingsSchema(): JsonSettingsSchema { return analogueRadialGaugeSettingsSchemaValue; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts index 507d84fcc4..82c0904a6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -18,7 +18,7 @@ import * as CanvasGauges from 'canvas-gauges'; import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; import * as tinycolor_ from 'tinycolor2'; import { ColorFormats } from 'tinycolor2'; -import { isDefined, isString, isUndefined, padValue } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isString, isUndefined, padValue } from '@core/utils'; import GenericOptions = CanvasGauges.GenericOptions; import BaseGauge = CanvasGauges.BaseGauge; @@ -220,7 +220,7 @@ export class CanvasDigitalGauge extends BaseGauge { public _value: number; constructor(options: CanvasDigitalGaugeOptions) { - options = {...defaultDigitalGaugeOptions,...(options || {})}; + options = {...defaultDigitalGaugeOptions, ...(options || {})}; super(CanvasDigitalGauge.configure(options)); this.initValueClone(); } @@ -236,10 +236,10 @@ export class CanvasDigitalGauge extends BaseGauge { } if (options.gaugeType === 'donut') { - if (!options.donutStartAngle) { + if (!isDefinedAndNotNull(options.donutStartAngle)) { options.donutStartAngle = 1.5 * Math.PI; } - if (!options.donutEndAngle) { + if (!isDefinedAndNotNull(options.donutEndAngle)) { options.donutEndAngle = options.donutStartAngle + 2 * Math.PI; } } @@ -255,7 +255,7 @@ export class CanvasDigitalGauge extends BaseGauge { const levelColor: any = options.levelColors[i]; if (levelColor !== null) { let percentage: number; - if(isColorProperty){ + if (isColorProperty) { percentage = inc * i; } else { percentage = CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue); @@ -280,7 +280,7 @@ export class CanvasDigitalGauge extends BaseGauge { options.ticksValue = []; for (const tick of options.ticks) { if (tick !== null) { - options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue)) + options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue)); } } @@ -294,7 +294,7 @@ export class CanvasDigitalGauge extends BaseGauge { return options; } - static normalizeValue (value: number, min: number, max: number): number { + static normalizeValue(value: number, min: number, max: number): number { const normalValue = (value - min) / (max - min); if (normalValue <= 0) { return 0; @@ -539,8 +539,8 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, titleOffset += bd.fontSizeFactor * 2; bd.titleY = bd.baseY + titleOffset; titleOffset += bd.fontSizeFactor * 2; - bd.Cy += titleOffset/2; - bd.Ro -= titleOffset/2; + bd.Cy += titleOffset / 2; + bd.Ro -= titleOffset / 2; } bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; bd.Cx = bd.baseX + bd.width / 2; @@ -575,8 +575,8 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, const valueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor).height; const labelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor).height; const total = valueHeight + labelHeight; - bd.labelY = bd.Cy + total/2; - bd.valueY = bd.Cy - total/2 + valueHeight/2; + bd.labelY = bd.Cy + total / 2; + bd.valueY = bd.Cy - total / 2 + valueHeight / 2; } else { bd.valueY = bd.Cy; } @@ -586,8 +586,8 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, bd.labelY = bd.Cy + (8 + options.fontLabelSize) * bd.fontSizeFactor; bd.minY = bd.maxY = bd.labelY; if (options.roundedLineCap) { - bd.minY += bd.strokeWidth/2; - bd.maxY += bd.strokeWidth/2; + bd.minY += bd.strokeWidth / 2; + bd.maxY += bd.strokeWidth / 2; } bd.minX = bd.Cx - bd.Rm; bd.maxX = bd.Cx + bd.Rm; @@ -604,15 +604,15 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, if (options.hideMinMax && options.label === '') { bd.labelY = bd.barBottom; - bd.barLeft = bd.origBaseX + options.fontMinMaxSize/3 * bd.fontSizeFactor; - bd.barRight = bd.origBaseX + w + /*bd.width*/ - options.fontMinMaxSize/3 * bd.fontSizeFactor; + bd.barLeft = bd.origBaseX + options.fontMinMaxSize / 3 * bd.fontSizeFactor; + bd.barRight = bd.origBaseX + w + /*bd.width*/ -options.fontMinMaxSize / 3 * bd.fontSizeFactor; } else { context.font = Drawings.font(options, 'MinMax', bd.fontSizeFactor); - const minTextWidth = context.measureText(options.minValue+'').width; - const maxTextWidth = context.measureText(options.maxValue+'').width; + const minTextWidth = context.measureText(options.minValue + '').width; + const maxTextWidth = context.measureText(options.maxValue + '').width; const maxW = Math.max(minTextWidth, maxTextWidth); - bd.minX = bd.origBaseX + maxW/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor; - bd.maxX = bd.origBaseX + w + /*bd.width*/ - maxW/2 - options.fontMinMaxSize/3 * bd.fontSizeFactor; + bd.minX = bd.origBaseX + maxW / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor; + bd.maxX = bd.origBaseX + w + /*bd.width*/ -maxW / 2 - options.fontMinMaxSize / 3 * bd.fontSizeFactor; bd.barLeft = bd.minX; bd.barRight = bd.maxX; bd.labelY = bd.barBottom + (8 + options.fontLabelSize) * bd.fontSizeFactor; @@ -632,7 +632,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, bd.barBottom = bd.labelY - (8 + options.fontLabelSize) * bd.fontSizeFactor; } bd.minX = bd.maxX = - bd.baseX + bd.width/2 + bd.strokeWidth/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor; + bd.baseX + bd.width / 2 + bd.strokeWidth / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor; bd.minY = bd.barBottom; bd.maxY = bd.barTop; bd.fontMinMaxBaseline = 'middle'; @@ -658,13 +658,13 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, // tslint:disable-next-line:no-bitwise dashCount = (dashCount - 1) | 1; } - bd.dashLength = Math.ceil(circumference/dashCount); + bd.dashLength = Math.ceil(circumference / dashCount); } return bd; } -function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string, baseSize: number): FontHeightInfo { +function determineFontHeight(options: CanvasDigitalGaugeOptions, target: string, baseSize: number): FontHeightInfo { const fontStyleStr = 'font-style:' + options['font' + target + 'Style'] + ';font-weight:' + options['font' + target + 'Weight'] + ';font-size:' + options['font' + target + 'Size'] * baseSize + 'px;font-family:' + @@ -688,9 +688,9 @@ function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string try { result = {}; - block.css({ verticalAlign: 'baseline' }); + block.css({verticalAlign: 'baseline'}); result.ascent = block.offset().top - text.offset().top; - block.css({ verticalAlign: 'bottom' }); + block.css({verticalAlign: 'bottom'}); result.height = block.offset().top - text.offset().top; result.descent = result.height - result.ascent; } finally { @@ -720,15 +720,15 @@ function drawBackground(context: DigitalGaugeCanvasRenderingContext2D, options: context.stroke(); } else if (options.gaugeType === 'arc') { context.arc(context.barDimensions.Cx, context.barDimensions.Cy, - context.barDimensions.Rm, Math.PI, 2*Math.PI); + context.barDimensions.Rm, Math.PI, 2 * Math.PI); context.stroke(); } else if (options.gaugeType === 'horizontalBar') { - context.moveTo(barLeft,barTop + strokeWidth/2); - context.lineTo(barRight,barTop + strokeWidth/2); + context.moveTo(barLeft, barTop + strokeWidth / 2); + context.lineTo(barRight, barTop + strokeWidth / 2); context.stroke(); } else if (options.gaugeType === 'verticalBar') { - context.moveTo(baseX + width/2, barBottom); - context.lineTo(baseX + width/2, barTop); + context.moveTo(baseX + width / 2, barBottom); + context.lineTo(baseX + width / 2, barTop); context.stroke(); } } @@ -740,7 +740,9 @@ function drawText(context: DigitalGaugeCanvasRenderingContext2D, options: Canvas } function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { - if (!options.title || typeof options.title !== 'string') return; + if (!options.title || typeof options.title !== 'string') { + return; + } const {titleY, width, baseX, fontSizeFactor} = context.barDimensions; @@ -756,7 +758,9 @@ function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options } function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { - if (!options.label || options.label === '') return; + if (!options.label || options.label === '') { + return; + } const {labelY, baseX, width, fontSizeFactor} = context.barDimensions; @@ -772,7 +776,9 @@ function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options } function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { - if (options.hideMinMax || options.gaugeType === 'donut') return; + if (options.hideMinMax || options.gaugeType === 'donut') { + return; + } const {minY, maxY, minX, maxX, fontSizeFactor, fontMinMaxAlign, fontMinMaxBaseline} = context.barDimensions; @@ -782,12 +788,14 @@ function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, option context.textBaseline = fontMinMaxBaseline; context.font = Drawings.font(options, 'MinMax', fontSizeFactor); context.lineWidth = 0; - drawText(context, options, 'MinMax', options.minValue+'', minX, minY); - drawText(context, options, 'MinMax', options.maxValue+'', maxX, maxY); + drawText(context, options, 'MinMax', options.minValue + '', minX, minY); + drawText(context, options, 'MinMax', options.maxValue + '', maxX, maxY); } function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, value: any) { - if (options.hideValue) return; + if (options.hideValue) { + return; + } const {valueY, baseX, width, fontSizeFactor, fontValueBaseline} = context.barDimensions; @@ -837,16 +845,16 @@ function drawArcGlow(context: DigitalGaugeCanvasRenderingContext2D, context.setLineDash([]); const strokeWidth = Ro - Ri; const blur = 0.55; - const edge = strokeWidth*blur; - context.lineWidth = strokeWidth+edge; - const stop = blur/(2*blur+2); - const glowGradient = context.createRadialGradient(Cx,Cy,Ri-edge/2,Cx,Cy,Ro+edge/2); + const edge = strokeWidth * blur; + context.lineWidth = strokeWidth + edge; + const stop = blur / (2 * blur + 2); + const glowGradient = context.createRadialGradient(Cx, Cy, Ri - edge / 2, Cx, Cy, Ro + edge / 2); const color1 = tinycolor(color).setAlpha(0.5).toRgbString(); const color2 = tinycolor(color).setAlpha(0).toRgbString(); - glowGradient.addColorStop(0,color2); - glowGradient.addColorStop(stop,color1); - glowGradient.addColorStop(1.0-stop,color1); - glowGradient.addColorStop(1,color2); + glowGradient.addColorStop(0, color2); + glowGradient.addColorStop(stop, color1); + glowGradient.addColorStop(1.0 - stop, color1); + glowGradient.addColorStop(1, color2); context.strokeStyle = glowGradient; context.beginPath(); const e = 0.01 * Math.PI; @@ -863,21 +871,21 @@ function drawBarGlow(context: DigitalGaugeCanvasRenderingContext2D, startX: numb endX: number, endY: number, color: string, strokeWidth: number, isVertical: boolean) { context.setLineDash([]); const blur = 0.55; - const edge = strokeWidth*blur; - context.lineWidth = strokeWidth+edge; - const stop = blur/(2*blur+2); - const gradientStartX = isVertical ? startX - context.lineWidth/2 : 0; - const gradientStartY = isVertical ? 0 : startY - context.lineWidth/2; - const gradientStopX = isVertical ? startX + context.lineWidth/2 : 0; - const gradientStopY = isVertical ? 0 : startY + context.lineWidth/2; - - const glowGradient = context.createLinearGradient(gradientStartX,gradientStartY,gradientStopX,gradientStopY); + const edge = strokeWidth * blur; + context.lineWidth = strokeWidth + edge; + const stop = blur / (2 * blur + 2); + const gradientStartX = isVertical ? startX - context.lineWidth / 2 : 0; + const gradientStartY = isVertical ? 0 : startY - context.lineWidth / 2; + const gradientStopX = isVertical ? startX + context.lineWidth / 2 : 0; + const gradientStopY = isVertical ? 0 : startY + context.lineWidth / 2; + + const glowGradient = context.createLinearGradient(gradientStartX, gradientStartY, gradientStopX, gradientStopY); const color1 = tinycolor(color).setAlpha(0.5).toRgbString(); const color2 = tinycolor(color).setAlpha(0).toRgbString(); - glowGradient.addColorStop(0,color2); - glowGradient.addColorStop(stop,color1); - glowGradient.addColorStop(1.0-stop,color1); - glowGradient.addColorStop(1,color2); + glowGradient.addColorStop(0, color2); + glowGradient.addColorStop(stop, color1); + glowGradient.addColorStop(1.0 - stop, color1); + glowGradient.addColorStop(1, color2); context.strokeStyle = glowGradient; const dx = isVertical ? 0 : 0.05 * context.lineWidth; const dy = isVertical ? 0.05 * context.lineWidth : 0; @@ -984,12 +992,12 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, context.strokeStyle = neonColor; } context.beginPath(); - context.moveTo(barLeft,barTop + strokeWidth/2); - context.lineTo(barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2); + context.moveTo(barLeft, barTop + strokeWidth / 2); + context.lineTo(barLeft + (barRight - barLeft) * progress, barTop + strokeWidth / 2); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { - drawBarGlow(context, barLeft, barTop + strokeWidth/2, - barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2, + drawBarGlow(context, barLeft, barTop + strokeWidth / 2, + barLeft + (barRight - barLeft) * progress, barTop + strokeWidth / 2, neonColor, strokeWidth, false); } drawTickBar(context, options.ticksValue, barLeft, barTop, barRight - barLeft, strokeWidth, @@ -999,12 +1007,12 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, context.strokeStyle = neonColor; } context.beginPath(); - context.moveTo(baseX + width/2, barBottom); - context.lineTo(baseX + width/2, barBottom - (barBottom-barTop)*progress); + context.moveTo(baseX + width / 2, barBottom); + context.lineTo(baseX + width / 2, barBottom - (barBottom - barTop) * progress); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { - drawBarGlow(context, baseX + width/2, barBottom, - baseX + width/2, barBottom - (barBottom-barTop)*progress, + drawBarGlow(context, baseX + width / 2, barBottom, + baseX + width / 2, barBottom - (barBottom - barTop) * progress, neonColor, strokeWidth, true); } drawTickBar(context, options.ticksValue, baseX + width / 2, barTop, barTop - barBottom, strokeWidth, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts index 3c1b7d12c2..fa622cb2a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts @@ -694,7 +694,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -783,7 +783,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -872,7 +872,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', @@ -961,7 +961,7 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { }, { value: '700', - label: '800' + label: '700' }, { value: '800', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index c2372135ad..11cc8dc732 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -25,7 +25,7 @@ import { FixedLevelColors } from '@home/components/widget/lib/digital-gauge.models'; import * as tinycolor_ from 'tinycolor2'; -import { isDefined } from '@core/utils'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { prepareFontSettings } from '@home/components/widget/lib/settings.models'; import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; import { DatePipe } from '@angular/common'; @@ -53,7 +53,7 @@ export class TbCanvasDigitalGauge { } constructor(protected ctx: WidgetContext, canvasId: string) { - const gaugeElement = $('#'+canvasId, ctx.$container)[0]; + const gaugeElement = $('#' + canvasId, ctx.$container)[0]; const settings: DigitalGaugeSettings = ctx.settings; this.localSettings = {}; @@ -80,7 +80,7 @@ export class TbCanvasDigitalGauge { this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false; if (!settings.useFixedLevelColor) { - if (!settings.levelColors || settings.levelColors.length <= 0) { + if (!settings.levelColors || settings.levelColors.length === 0) { this.localSettings.levelColors = [keyColor]; } else { this.localSettings.levelColors = settings.levelColors.slice(); @@ -97,14 +97,15 @@ export class TbCanvasDigitalGauge { this.localSettings.colorTicks = settings.colorTicks || '#666'; this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals : - ((isDefined(settings.decimals) && settings.decimals !== null) - ? settings.decimals : ctx.decimals); + (isDefinedAndNotNull(settings.decimals) ? settings.decimals : ctx.decimals); this.localSettings.units = dataKey.units && dataKey.units.length ? dataKey.units : (isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units); this.localSettings.hideValue = settings.showValue !== true; this.localSettings.hideMinMax = settings.showMinMax !== true; + this.localSettings.donutStartAngle = isDefinedAndNotNull(settings.donutStartAngle) ? + -TbCanvasDigitalGauge.toRadians(settings.donutStartAngle) : null; this.localSettings.title = ((settings.showTitle === true) ? (settings.title && settings.title.length > 0 ? @@ -191,14 +192,15 @@ export class TbCanvasDigitalGauge { hideValue: this.localSettings.hideValue, hideMinMax: this.localSettings.hideMinMax, + donutStartAngle: this.localSettings.donutStartAngle, + valueDec: this.localSettings.decimals, neonGlowBrightness: this.localSettings.neonGlowBrightness, // animations animation: settings.animation !== false && !ctx.isMobile, - animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null) - ? settings.animationDuration : 500, + animationDuration: isDefinedAndNotNull(settings.animationDuration) ? settings.animationDuration : 500, animationRule: settings.animationRule || 'linear', isMobile: ctx.isMobile @@ -241,7 +243,7 @@ export class TbCanvasDigitalGauge { if (findDataKey) { findDataKey.settings.push(settings); } else { - datasource.dataKeys.push(dataKey) + datasource.dataKeys.push(dataKey); } } else { const datasourceAttribute: Datasource = { @@ -257,17 +259,23 @@ export class TbCanvasDigitalGauge { return datasources; } + private static toRadians(angle: number): number { + return angle * (Math.PI / 180); + } + init() { - if (this.localSettings.useFixedLevelColor) { - if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) { - this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); - } + let updateSetting = false; - if (this.localSettings.showTicks) { - if (this.localSettings.ticksValue && this.localSettings.ticksValue.length) { - this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue); - } - } + if (this.localSettings.useFixedLevelColor && this.localSettings.fixedLevelColors?.length > 0) { + this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); + updateSetting = true; + } + if (this.localSettings.showTicks && this.localSettings.ticksValue?.length) { + this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue); + updateSetting = true; + } + + if (updateSetting) { this.updateSetting(); } } @@ -281,7 +289,7 @@ export class TbCanvasDigitalGauge { predefineLevelColors.push({ value: levelSetting.value, color - }) + }); } else if (levelSetting.entityAlias && levelSetting.attribute) { try { levelColorsDatasource = TbCanvasDigitalGauge.generateDatasource(this.ctx, levelColorsDatasource, @@ -293,7 +301,7 @@ export class TbCanvasDigitalGauge { } } - for(const levelColor of options){ + for (const levelColor of options) { if (levelColor.from) { setLevelColor.call(this, levelColor.from, levelColor.color); } @@ -313,9 +321,9 @@ export class TbCanvasDigitalGauge { let ticksDatasource: Datasource[] = []; const predefineTicks: number[] = []; - for(const tick of options){ + for (const tick of options) { if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) { - predefineTicks.push(tick.value) + predefineTicks.push(tick.value); } else if (tick.entityAlias && tick.attribute) { try { ticksDatasource = TbCanvasDigitalGauge @@ -398,7 +406,7 @@ export class TbCanvasDigitalGauge { filter.transform(timestamp, this.localSettings.timestampFormat); } const value = tvPair[1]; - if(value !== this.gauge.value) { + if (value !== this.gauge.value) { if (!this.gauge.options.animation) { this.gauge._value = value; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index a2c70a3a89..2eb4b372b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -38,7 +38,7 @@
-
{{ column.title }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index ef560dbfc5..dae33f3cfe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -206,11 +206,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } public onDataUpdated() { - this.ngZone.run(() => { - this.updateTitle(true); - this.entityDatasource.dataUpdated(); - this.ctx.detectChanges(); - }); + this.updateTitle(true); + this.entityDatasource.dataUpdated(); } public pageLinkSortDirection(): SortDirection { @@ -488,6 +485,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni return column.def; } + public trackByRowIndex(index: number) { + return index; + } + public headerStyle(key: EntityColumn): any { const columnWidth = this.columnWidth[key.def]; return widthStyle(columnWidth); @@ -529,7 +530,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } else { content = this.defaultContent(key, contentInfo, value); } - return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + } else { return ''; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index d334d5c99a..03f6d1b975 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -380,7 +380,7 @@ export class TbFlot { const yaxesMap: {[units: string]: TbFlotAxisOptions} = {}; const predefinedThresholds: TbFlotThresholdMarking[] = []; const thresholdsDatasources: Datasource[] = []; - if (this.settings.customLegendEnabled) { + if (this.settings.customLegendEnabled && this.settings.dataKeysListForLabels?.length) { this.labelPatternsSourcesData = []; const labelPatternsDatasources: Datasource[] = []; this.settings.dataKeysListForLabels.forEach((item) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss index ed84b55a6d..5f21a32c51 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss @@ -30,7 +30,6 @@ position: relative; display: flex; height: 40px; - text-transform: uppercase; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 6bf8fc9ce2..5d6f39850e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -86,8 +86,7 @@ export default abstract class LeafletMap { public initSettings(options: MapSettings) { this.options.tinyColor = tinycolor(this.options.color || defaultSettings.color); - const { disableScrollZooming, - useClusterMarkers, + const { useClusterMarkers, zoomOnClick, showCoverageOnHover, removeOutsideVisibleBounds, @@ -95,9 +94,6 @@ export default abstract class LeafletMap { chunkedLoading, maxClusterRadius, maxZoom }: MapSettings = options; - if (disableScrollZooming) { - this.map.scrollWheelZoom.disable(); - } if (useClusterMarkers) { const clusteringSettings: MarkerClusterGroupOptions = { zoomToBoundsOnClick: zoomOnClick, @@ -307,8 +303,11 @@ export default abstract class LeafletMap { } else { this.bounds = new L.LatLngBounds(null, null); } + if (this.options.disableScrollZooming) { + this.map.scrollWheelZoom.disable(); + } if (this.options.draggableMarker) { - this.addMarkerControl(); + this.addMarkerControl(); } if (this.options.editablePolygon) { this.addPolygonControl(); @@ -623,10 +622,10 @@ export default abstract class LeafletMap { // Polyline - updatePolylines(polyData: FormattedData[][], updateBounds = true, data?: FormattedData) { + updatePolylines(polyData: FormattedData[][], updateBounds = true, activePolyline?: FormattedData) { const keys: string[] = []; polyData.forEach((dataSource: FormattedData[]) => { - data = data || dataSource[0]; + const data = activePolyline || dataSource[0]; if (dataSource.length && data.entityName === dataSource[0].entityName) { if (this.polylines.get(data.entityName)) { this.updatePolyline(data, dataSource, this.options, updateBounds); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts index a4c1d1c50b..f5f48c86c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -67,6 +67,7 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { title = ''; minValue: number; maxValue: number; + newValue = 0; private startDeg = -1; private currentDeg = 0; @@ -175,16 +176,15 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { const offset = this.knob.offset(); const center = { - y : offset.top + this.knob.height()/2, - x: offset.left + this.knob.width()/2 + y: offset.top + this.knob.height() / 2, + x: offset.left + this.knob.width() / 2 }; - const rad2deg = 180/Math.PI; + const rad2deg = 180 / Math.PI; const t: Touch = ((e.originalEvent as any).touches) ? (e.originalEvent as any).touches[0] : e; - const a = center.y - t.pageY; const b = center.x - t.pageX; - let deg = Math.atan2(a,b)*rad2deg; - if(deg < 0){ + let deg = Math.atan2(a, b) * rad2deg; + if (deg < 0) { deg = 360 + deg; } if (deg > this.maxDeg) { @@ -196,13 +196,17 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { } this.currentDeg = deg; this.lastDeg = deg; - this.knobTopPointerContainer.css('transform','rotate('+(this.currentDeg)+'deg)'); + this.knobTopPointerContainer.css('transform', 'rotate(' + (this.currentDeg) + 'deg)'); this.turn(this.degreeToRatio(this.currentDeg)); this.rotation = this.currentDeg; this.startDeg = -1; + this.rpcUpdateValue(this.newValue); }); + + this.knob.on('mousedown touchstart', (e) => { + this.moving = false; e.preventDefault(); const offset = this.knob.offset(); const center = { @@ -211,7 +215,7 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { }; const rad2deg = 180/Math.PI; - this.knob.on('mousemove.rem touchmove.rem', (ev) => { + $(document).on('mousemove.rem touchmove.rem', (ev) => { this.moving = true; const t: Touch = ((ev.originalEvent as any).touches) ? (ev.originalEvent as any).touches[0] : ev; @@ -262,6 +266,9 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { }); $(document).on('mouseup.rem touchend.rem',() => { + if(this.newValue !== this.rpcValue && this.moving) { + this.rpcUpdateValue(this.newValue); + } this.knob.off('.rem'); $(document).off('.rem'); this.rotation = this.currentDeg; @@ -308,12 +315,12 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { } private turn(ratio: number) { - const value = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals)); - if (this.canvasBar.value !== value) { - this.canvasBar.value = value; + this.newValue = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals)); + if (this.canvasBar.value !== this.newValue) { + this.canvasBar.value = this.newValue; } this.updateColor(this.canvasBar.getValueColor()); - this.onValue(value); + this.onValue(this.newValue); } private resize() { @@ -379,7 +386,6 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { private onValue(value: number) { this.value = this.formatValue(value); this.checkValueSize(); - this.rpcUpdateValue(value); this.ctx.detectChanges(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss index f878f45320..68bea36407 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss @@ -112,6 +112,9 @@ $error-height: 14px !default; height: 90%; } + .mat-slide-toggle-label{ + height: 100%; + } .mat-slide-toggle-thumb { top: 0; left: 0; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 28e6e58c25..ebf5387862 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -40,7 +40,7 @@ import { } from '@shared/models/widget.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { hashCode, isDefined, isNumber } from '@core/utils'; +import {hashCode, isDefined, isDefinedAndNotNull, isNumber} from '@core/utils'; import cssjs from '@core/css/css'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; @@ -197,11 +197,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } public onDataUpdated() { - this.ngZone.run(() => { - this.sources.forEach((source) => { - source.timeseriesDatasource.dataUpdated(this.data); - }); - this.ctx.detectChanges(); + this.sources.forEach((source) => { + source.timeseriesDatasource.dataUpdated(this.data); }); } @@ -410,7 +407,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const units = contentInfo.units || this.ctx.widgetConfig.units; content = this.ctx.utils.formatValue(value, decimals, units, true); } - return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } } } @@ -515,26 +523,20 @@ class TimeseriesDatasource implements DataSource { row[d + 1] = cellData[1]; }); } + const rows: TimeseriesRow[] = []; - for (const t of Object.keys(rowsMap)) { - if (this.hideEmptyLines) { - let hideLine = true; - for (let c = 0; (c < data.length) && hideLine; c++) { - if (rowsMap[t][c + 1]) { - hideLine = false; - } - } - if (!hideLine) { - rows.push(rowsMap[t]); - } + + for (const value of Object.values(rowsMap)) { + if (this.hideEmptyLines && isDefinedAndNotNull(value[1])) { + rows.push(value); } else { - rows.push(rowsMap[t]); + rows.push(value); } } + return rows; } - isEmpty(): Observable { return this.rowsSubject.pipe( map((rows) => !rows.length) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index 30a30ed95f..3d692305b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -38,7 +38,7 @@
widget.no-data
diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index a636217eaa..9788241693 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -50,7 +50,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni sidenavMode: 'over' | 'push' | 'side' = 'side'; sidenavOpened = true; - logo = require('../../../assets/logo_title_white.svg').default; + logo = 'assets/logo_title_white.svg'; @ViewChild('sidenav') sidenav: MatSidenav; diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index 4ca7eb8f4c..b8acb0f624 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -24,7 +24,7 @@ [ngClass]="{'tb-toggled' : sectionActive()}">
    -
  • +
diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts index 4054bc4b4f..da1b5d6a42 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -44,4 +44,8 @@ export class MenuToggleComponent implements OnInit { return '0px'; } } + + trackBySectionPages(index: number, section: MenuSection){ + return section.id; + } } diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html index b2302966dd..3819af5777 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.html +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -16,7 +16,7 @@ -->
    -
  • +
  • diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss index 026eae6935..bd3bf999ca 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -32,7 +32,6 @@ } a.mat-button { - text-transform: uppercase; display: flex; overflow: hidden; line-height: 40px; diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts index 40222e1110..92e80d38aa 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts @@ -16,6 +16,7 @@ import { Component, OnInit } from '@angular/core'; import { MenuService } from '@core/services/menu.service'; +import { MenuSection } from '@core/services/menu.models'; @Component({ selector: 'tb-side-menu', @@ -29,6 +30,10 @@ export class SideMenuComponent implements OnInit { constructor(private menuService: MenuService) { } + trackByMenuSection(index: number, section: MenuSection){ + return section.id; + } + ngOnInit() { } diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index ddc2aa5a1f..8a8fbbce4e 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -31,6 +31,7 @@ import { CustomerService } from '@core/http/customer.service'; import { DashboardService } from '@core/http/dashboard.service'; import { UserService } from '@core/http/user.service'; import { AlarmService } from '@core/http/alarm.service'; +import { Router } from '@angular/router'; export const ServicesMap = new Map>( [ @@ -49,6 +50,7 @@ export const ServicesMap = new Map>( ['date', DatePipe], ['utils', UtilsService], ['translate', TranslateService], - ['http', HttpClient] + ['http', HttpClient], + ['router', Router] ] ); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index aa97d99876..3b434d805a 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -76,6 +76,7 @@ import { TranslateService } from '@ngx-translate/core'; import { PageLink } from '@shared/models/page/page-link'; import { SortOrder } from '@shared/models/page/sort-order'; import { DomSanitizer } from '@angular/platform-browser'; +import { Router } from '@angular/router'; export interface IWidgetAction { name: string; @@ -157,6 +158,7 @@ export class WidgetContext { translate: TranslateService; http: HttpClient; sanitizer: DomSanitizer; + router: Router; private changeDetectorValue: ChangeDetectorRef; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index 5751c88341..ee72b33453 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -96,8 +96,9 @@ export class DashboardFormComponent extends EntityComponent { } prepareFormValue(formValue: any): any { - formValue.configuration = {...(this.entity.configuration || {}), ...(formValue.configuration || {})}; - return formValue; + const preparedValue = super.prepareFormValue(formValue); + preparedValue.configuration = {...(this.entity.configuration || {}), ...(preparedValue.configuration || {})}; + return preparedValue; } onPublicLinkCopied($event) { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html index 9379dfa17c..dec6eedd0c 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html @@ -74,11 +74,11 @@ widgets-bundle.empty widget.select-widgets-bundle diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html index 8d8171a498..52734e6b4b 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html @@ -27,7 +27,7 @@ [ngStyle]="dashboardStyle">
    {{'dashboard.no-widgets' | translate}} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts new file mode 100644 index 0000000000..f0ffcd2c4d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { DeviceProfilesTableConfigResolver } from './device-profiles-table-config.resolver'; + +const routes: Routes = [ + { + path: 'deviceProfiles', + data: { + breadcrumb: { + label: 'device-profile.device-profiles', + icon: 'mdi:alpha-d-box' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'device-profile.device-profiles' + }, + resolve: { + entitiesTableConfig: DeviceProfilesTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + DeviceProfilesTableConfigResolver + ] +}) +export class DeviceProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html new file mode 100644 index 0000000000..40a9f55aa5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts new file mode 100644 index 0000000000..9cf18c498e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { DeviceProfile } from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-profile-tabs', + templateUrl: './device-profile-tabs.component.html', + styleUrls: [] +}) +export class DeviceProfileTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts new file mode 100644 index 0000000000..09207057ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; +import { DeviceProfileRoutingModule } from './device-profile-routing.module'; + +@NgModule({ + declarations: [ + DeviceProfileTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + DeviceProfileRoutingModule + ] +}) +export class DeviceProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts new file mode 100644 index 0000000000..c4927fbe99 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -0,0 +1,153 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { DialogService } from '@core/services/dialog.service'; +import { + DeviceProfile, + deviceProfileTypeTranslationMap, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; +import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; +import { Observable } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { + AddDeviceProfileDialogComponent, + AddDeviceProfileDialogData +} from '../../components/profile/add-device-profile-dialog.component'; + +@Injectable() +export class DeviceProfilesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private deviceProfileService: DeviceProfileService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialogService: DialogService, + private dialog: MatDialog) { + + this.config.entityType = EntityType.DEVICE_PROFILE; + this.config.entityComponent = DeviceProfileComponent; + this.config.entityTabsComponent = DeviceProfileTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE_PROFILE); + this.config.entityResources = entityTypeResources.get(EntityType.DEVICE_PROFILE); + + this.config.addDialogStyle = {width: '1000px'}; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'device-profile.name', '20%'), + new EntityTableColumn('type', 'device-profile.type', '20%', (deviceProfile) => { + return this.translate.instant(deviceProfileTypeTranslationMap.get(deviceProfile.type)); + }), + new EntityTableColumn('transportType', 'device-profile.transport-type', '20%', (deviceProfile) => { + return this.translate.instant(deviceTransportTypeTranslationMap.get(deviceProfile.transportType)); + }), + new EntityTableColumn('description', 'device-profile.description', '40%'), + new EntityTableColumn('isDefault', 'device-profile.default', '60px', + entity => { + return checkBoxCell(entity.default); + }) + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('device-profile.set-default'), + icon: 'flag', + isEnabled: (deviceProfile) => !deviceProfile.default, + onAction: ($event, entity) => this.setDefaultDeviceProfile($event, entity) + } + ); + + this.config.deleteEntityTitle = deviceProfile => this.translate.instant('device-profile.delete-device-profile-title', + { deviceProfileName: deviceProfile.name }); + this.config.deleteEntityContent = () => this.translate.instant('device-profile.delete-device-profile-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('device-profile.delete-device-profiles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('device-profile.delete-device-profiles-text'); + + this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink); + this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id); + this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile); + this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id); + this.config.onEntityAction = action => this.onDeviceProfileAction(action); + this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + this.config.addEntity = () => this.addDeviceProfile(); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('device-profile.device-profiles'); + + return this.config; + } + + addDeviceProfile(): Observable { + return this.dialog.open(AddDeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceProfileName: null + } + }).afterClosed(); + } + + setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device-profile.set-default-device-profile-title', {deviceProfileName: deviceProfile.name}), + this.translate.instant('device-profile.set-default-device-profile-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceProfileService.setDefaultDeviceProfile(deviceProfile.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onDeviceProfileAction(action: EntityAction): boolean { + switch (action.action) { + case 'setDefault': + this.setDefaultDeviceProfile(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html new file mode 100644 index 0000000000..a1d4919b5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html @@ -0,0 +1,24 @@ + +
    + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts new file mode 100644 index 0000000000..07d8d33b79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceConfiguration, + DeviceConfiguration, + DeviceProfileType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-configuration', + templateUrl: './default-device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceConfiguration | null): void { + this.defaultDeviceConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceConfiguration = null; + if (this.defaultDeviceConfigurationFormGroup.valid) { + configuration = this.defaultDeviceConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceProfileType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html new file mode 100644 index 0000000000..cd2d53aab0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
    + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts new file mode 100644 index 0000000000..111c632a74 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceTransportConfiguration, + DeviceTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-transport-configuration', + templateUrl: './default-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceTransportConfiguration | null): void { + this.defaultDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.defaultDeviceTransportConfigurationFormGroup.valid) { + configuration = this.defaultDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html new file mode 100644 index 0000000000..ffda7bac44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html @@ -0,0 +1,27 @@ + +
    +
    + + + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts new file mode 100644 index 0000000000..3e6abc1fd6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceConfiguration, DeviceProfileType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-configuration', + templateUrl: './device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceConfigurationComponent), + multi: true + }] +}) +export class DeviceConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceProfileType = DeviceProfileType; + + deviceConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + type: DeviceProfileType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceConfiguration = null; + if (this.deviceConfigurationFormGroup.valid) { + configuration = this.deviceConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html new file mode 100644 index 0000000000..0cff6b3693 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html @@ -0,0 +1,43 @@ + +
    + + + + +
    device.device-configuration
    +
    +
    + + +
    + + + +
    device.transport-configuration
    +
    +
    + + +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts new file mode 100644 index 0000000000..db9297f275 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts @@ -0,0 +1,107 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceData, + deviceProfileTypeConfigurationInfoMap, + deviceTransportTypeConfigurationInfoMap +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-data', + templateUrl: './device-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceDataComponent), + multi: true + }] +}) +export class DeviceDataComponent implements ControlValueAccessor, OnInit { + + deviceDataFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + displayDeviceConfiguration: boolean; + displayTransportConfiguration: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceDataFormGroup = this.fb.group({ + configuration: [null, Validators.required], + transportConfiguration: [null, Validators.required] + }); + this.deviceDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceDataFormGroup.disable({emitEvent: false}); + } else { + this.deviceDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceData | null): void { + const deviceProfileType = value?.configuration?.type; + this.displayDeviceConfiguration = deviceProfileType && + deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasDeviceConfiguration; + const deviceTransportType = value?.transportConfiguration?.type; + this.displayTransportConfiguration = deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasDeviceConfiguration; + this.deviceDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); + } + + private updateModel() { + let deviceData: DeviceData = null; + if (this.deviceDataFormGroup.valid) { + deviceData = this.deviceDataFormGroup.getRawValue(); + } + this.propagateChange(deviceData); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html new file mode 100644 index 0000000000..f109335edc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html @@ -0,0 +1,39 @@ + +
    +
    + + + + + + + + + + + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts new file mode 100644 index 0000000000..505f494551 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-transport-configuration', + templateUrl: './device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), + multi: true + }] +}) +export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceTransportType = DeviceTransportType; + + deviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + transportType: DeviceTransportType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceTransportConfiguration | null): void { + this.transportType = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.deviceTransportConfigurationFormGroup.valid) { + configuration = this.deviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.transportType; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html new file mode 100644 index 0000000000..3fdccba628 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
    + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts new file mode 100644 index 0000000000..05e1448bc3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType, Lwm2mDeviceTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-lwm2m-device-transport-configuration', + templateUrl: './lwm2m-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + lwm2mDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.lwm2mDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.lwm2mDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Lwm2mDeviceTransportConfiguration | null): void { + this.lwm2mDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.lwm2mDeviceTransportConfigurationFormGroup.valid) { + configuration = this.lwm2mDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.LWM2M; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html new file mode 100644 index 0000000000..e21bb3818a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
    + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts new file mode 100644 index 0000000000..68348b8017 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType, MqttDeviceTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-mqtt-device-transport-configuration', + templateUrl: './mqtt-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class MqttDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + mqttDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.mqttDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.mqttDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.mqttDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.mqttDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MqttDeviceTransportConfiguration | null): void { + this.mqttDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.mqttDeviceTransportConfigurationFormGroup.valid) { + configuration = this.mqttDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.MQTT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html index 1cf9911aa8..f1b5b442ce 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -58,6 +58,37 @@ {{ 'device.rsa-key-required' | translate }} +
    + + device.client-id + + + {{ 'device.client-id-pattern' | translate }} + + + + device.user-name + + + {{ 'device.user-name-required' | translate }} + + + + device.password + + + + + {{ 'device.client-id-or-user-name-necessary' | translate }} + +
    diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts index ccbe1affe8..73e5ae040f 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -19,9 +19,23 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; import { DeviceService } from '@core/http/device.service'; -import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; +import { + credentialTypeNames, + DeviceCredentialMQTTBasic, + DeviceCredentials, + DeviceCredentialsType +} from '@shared/models/device.models'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; @@ -53,6 +67,8 @@ export class DeviceCredentialsDialogComponent extends credentialTypeNamesMap = credentialTypeNames; + hidePassword = true; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData, @@ -69,7 +85,12 @@ export class DeviceCredentialsDialogComponent extends this.deviceCredentialsFormGroup = this.fb.group({ credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsId: [''], - credentialsValue: [''] + credentialsValue: [''], + credentialsBasic: this.fb.group({ + clientId: ['', [Validators.pattern(/^[A-Za-z0-9]+$/)]], + userName: [''], + password: [''] + }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])}) }); if (this.isReadOnly) { this.deviceCredentialsFormGroup.disable({emitEvent: false}); @@ -89,10 +110,17 @@ export class DeviceCredentialsDialogComponent extends this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe( (deviceCredentials) => { this.deviceCredentials = deviceCredentials; + let credentialsValue = deviceCredentials.credentialsValue; + let credentialsBasic = {clientId: null, userName: null, password: null}; + if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { + credentialsValue = null; + credentialsBasic = JSON.parse(deviceCredentials.credentialsValue) as DeviceCredentialMQTTBasic; + } this.deviceCredentialsFormGroup.patchValue({ credentialsType: deviceCredentials.credentialsType, credentialsId: deviceCredentials.credentialsId, - credentialsValue: deviceCredentials.credentialsValue + credentialsValue, + credentialsBasic }); this.updateValidators(); } @@ -100,12 +128,16 @@ export class DeviceCredentialsDialogComponent extends } credentialsTypeChanged(): void { - this.deviceCredentialsFormGroup.patchValue( - {credentialsId: null, credentialsValue: null}, {emitEvent: true}); + this.deviceCredentialsFormGroup.patchValue({ + credentialsId: null, + credentialsValue: null, + credentialsBasic: {clientId: '', userName: '', password: ''} + }, {emitEvent: true}); this.updateValidators(); } updateValidators(): void { + this.hidePassword = true; const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType; switch (crendetialsType) { case DeviceCredentialsType.ACCESS_TOKEN: @@ -113,27 +145,66 @@ export class DeviceCredentialsDialogComponent extends this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); break; case DeviceCredentialsType.X509_CERTIFICATE: this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); break; + case DeviceCredentialsType.MQTT_BASIC: + this.deviceCredentialsFormGroup.get('credentialsBasic').enable(); + this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); } } + private atLeastOne(validator: ValidatorFn, controls: string[] = null) { + return (group: FormGroup): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : {atLeastOne: true}; + }; + } + cancel(): void { this.dialogRef.close(null); } save(): void { this.submitted = true; - this.deviceCredentials = {...this.deviceCredentials, ...this.deviceCredentialsFormGroup.value}; + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; + if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) { + deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic); + } + delete deviceCredentialsValue.credentialsBasic; + this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue}; this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe( (deviceCredentials) => { this.dialogRef.close(deviceCredentials); } ); } + + passwordChanged() { + const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value; + if (value !== '') { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]); + if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true}); + } + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity(); + } else { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity(); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html index 7f6e0159fa..21a7469850 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html @@ -15,9 +15,9 @@ limitations under the License. --> - - + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss index bd3bc86b1c..7a61453421 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss @@ -23,7 +23,7 @@ } :host ::ng-deep { - tb-entity-subtype-select { + tb-device-profile-autocomplete { width: 100%; mat-form-field { diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts index 4137a6752d..e78a5f73db 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts @@ -20,6 +20,7 @@ import { AppState } from '@core/core.state'; import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; import { DeviceInfo } from '@app/shared/models/device.models'; import { EntityType } from '@shared/models/entity-type.models'; +import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id'; @Component({ selector: 'tb-device-table-header', @@ -34,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent - - + + device.label + +
    {{ 'device.is-gateway' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index d187c8bf0c..833919eb78 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -19,7 +19,16 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { EntityComponent } from '../../components/entity/entity.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { DeviceInfo } from '@shared/models/device.models'; +import { + createDeviceConfiguration, + createDeviceProfileConfiguration, createDeviceTransportConfiguration, + DeviceData, + DeviceInfo, + DeviceProfileData, + DeviceProfileInfo, + DeviceProfileType, + DeviceTransportType +} from '@shared/models/device.models'; import { EntityType } from '@shared/models/entity-type.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { ActionNotificationShow } from '@core/notification/notification.actions'; @@ -70,8 +79,9 @@ export class DeviceComponent extends EntityComponent { return this.fb.group( { name: [entity ? entity.name : '', [Validators.required]], - type: [entity ? entity.type : null, [Validators.required]], + deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], label: [entity ? entity.label : ''], + deviceData: [entity ? entity.deviceData : null, [Validators.required]], additionalInfo: this.fb.group( { gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false], @@ -84,8 +94,9 @@ export class DeviceComponent extends EntityComponent { updateForm(entity: DeviceInfo) { this.entityForm.patchValue({name: entity.name}); - this.entityForm.patchValue({type: entity.type}); + this.entityForm.patchValue({deviceProfileId: entity.deviceProfileId}); this.entityForm.patchValue({label: entity.label}); + this.entityForm.patchValue({deviceData: entity.deviceData}); this.entityForm.patchValue({additionalInfo: {gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false}}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); @@ -122,4 +133,38 @@ export class DeviceComponent extends EntityComponent { ); } } + + onDeviceProfileUpdated() { + this.entitiesTableConfig.table.updateData(false); + } + + onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) { + if (deviceProfile && this.isEdit) { + const deviceProfileType: DeviceProfileType = deviceProfile.type; + const deviceTransportType: DeviceTransportType = deviceProfile.transportType; + let deviceData: DeviceData = this.entityForm.getRawValue().deviceData; + if (!deviceData) { + deviceData = { + configuration: createDeviceConfiguration(deviceProfileType), + transportConfiguration: createDeviceTransportConfiguration(deviceTransportType) + }; + this.entityForm.patchValue({deviceData}); + this.entityForm.markAsDirty(); + } else { + let changed = false; + if (deviceData.configuration.type !== deviceProfileType) { + deviceData.configuration = createDeviceConfiguration(deviceProfileType); + changed = true; + } + if (deviceData.transportConfiguration.type !== deviceTransportType) { + deviceData.transportConfiguration = createDeviceTransportConfiguration(deviceTransportType); + changed = true; + } + if (changed) { + this.entityForm.patchValue({deviceData}); + this.entityForm.markAsDirty(); + } + } + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts index c6e7c3bcc4..53ee34d570 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -24,9 +24,23 @@ import { DeviceCredentialsDialogComponent } from '@modules/home/pages/device/dev import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; +import { DefaultDeviceConfigurationComponent } from './data/default-device-configuration.component'; +import { DeviceConfigurationComponent } from './data/device-configuration.component'; +import { DeviceDataComponent } from './data/device-data.component'; +import { DefaultDeviceTransportConfigurationComponent } from './data/default-device-transport-configuration.component'; +import { DeviceTransportConfigurationComponent } from './data/device-transport-configuration.component'; +import { MqttDeviceTransportConfigurationComponent } from './data/mqtt-device-transport-configuration.component'; +import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-transport-configuration.component'; @NgModule({ declarations: [ + DefaultDeviceConfigurationComponent, + DeviceConfigurationComponent, + DefaultDeviceTransportConfigurationComponent, + MqttDeviceTransportConfigurationComponent, + Lwm2mDeviceTransportConfigurationComponent, + DeviceTransportConfigurationComponent, + DeviceDataComponent, DeviceComponent, DeviceTabsComponent, DeviceTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index 3216e776dd..a3d9a73e8a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -86,6 +86,8 @@ export class DevicesTableConfigResolver implements Resolve this.translate.instant('device.delete-device-title', { deviceName: device.name }); this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); @@ -111,7 +113,7 @@ export class DevicesTableConfigResolver implements Resolve> = [ new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), new EntityTableColumn('name', 'device.name', '25%'), - new EntityTableColumn('type', 'device.device-type', '25%'), + new EntityTableColumn('deviceProfileName', 'device-profile.device-profile', '25%'), new EntityTableColumn('label', 'device.label', '25%') ]; if (deviceScope === 'tenant') { @@ -175,11 +177,15 @@ export class DevicesTableConfigResolver implements Resolve - this.deviceService.getTenantDeviceInfos(pageLink, this.config.componentsData.deviceType); + this.deviceService.getTenantDeviceInfosByDeviceProfileId(pageLink, + this.config.componentsData.deviceProfileId !== null ? + this.config.componentsData.deviceProfileId.id : ''); this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id); } else { this.config.entitiesFetchFunction = pageLink => - this.deviceService.getCustomerDeviceInfos(this.customerId, pageLink, this.config.componentsData.deviceType); + this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink, + this.config.componentsData.deviceProfileId !== null ? + this.config.componentsData.deviceProfileId.id : ''); this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); } } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss index 8a7118d6d9..83bdb6c118 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss @@ -41,7 +41,6 @@ width: 100%; height: 100%; max-width: 240px; - text-transform: uppercase; &:hover { border-bottom: none; } diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index a6578ed38e..497c6231f3 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -29,15 +29,19 @@ import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.mo import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; +import { TenantProfileModule } from './tenant-profile/tenant-profile.module'; import { MODULES_MAP } from '@shared/public-api'; import { modulesMap } from '../../common/modules-map'; +import { DeviceProfileModule } from './device-profile/device-profile.module'; @NgModule({ exports: [ AdminModule, HomeLinksModule, ProfileModule, + TenantProfileModule, TenantModule, + DeviceProfileModule, DeviceModule, AssetModule, EntityViewModule, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index e87f7158d0..fd4efddf5a 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -29,7 +29,8 @@ rulenode.name - + {{ 'rulenode.name-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index c19fe98348..b5630b52f8 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -72,8 +72,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O } if (this.ruleNode) { if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { + this.ruleNodeFormGroup = this.fb.group({ - name: [this.ruleNode.name, [Validators.required]], + name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], debugMode: [this.ruleNode.debugMode, []], configuration: [this.ruleNode.configuration, [Validators.required]], additionalInfo: this.fb.group( @@ -102,6 +103,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O private updateRuleNode() { const formValue = this.ruleNodeFormGroup.value || {}; + if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) { const targetRuleChainId: string = formValue.targetRuleChainId; if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) { @@ -115,6 +117,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O Object.assign(this.ruleNode, formValue); } } else { + formValue.name = formValue.name.trim(); Object.assign(this.ruleNode, formValue); } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 2c8ecc9419..7096045aed 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1550,6 +1550,7 @@ export class AddRuleNodeDialogComponent extends DialogComponent + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts new file mode 100644 index 0000000000..cab08974f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { TenantProfile } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-profile-tabs', + templateUrl: './tenant-profile-tabs.component.html', + styleUrls: [] +}) +export class TenantProfileTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts new file mode 100644 index 0000000000..66d55d37ed --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { TenantProfileRoutingModule } from './tenant-profile-routing.module'; +import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; + +@NgModule({ + declarations: [ + TenantProfileTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + TenantProfileRoutingModule + ] +}) +export class TenantProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts new file mode 100644 index 0000000000..a56fbf60da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; +import { TenantProfileComponent } from '../../components/profile/tenant-profile.component'; +import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; +import { DialogService } from '@core/services/dialog.service'; + +@Injectable() +export class TenantProfilesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private tenantProfileService: TenantProfileService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialogService: DialogService) { + + this.config.entityType = EntityType.TENANT_PROFILE; + this.config.entityComponent = TenantProfileComponent; + this.config.entityTabsComponent = TenantProfileTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT_PROFILE); + this.config.entityResources = entityTypeResources.get(EntityType.TENANT_PROFILE); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'tenant-profile.name', '40%'), + new EntityTableColumn('description', 'tenant-profile.description', '60%'), + new EntityTableColumn('isDefault', 'tenant-profile.default', '60px', + entity => { + return checkBoxCell(entity.default); + }) + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('tenant-profile.set-default'), + icon: 'flag', + isEnabled: (tenantProfile) => !tenantProfile.default, + onAction: ($event, entity) => this.setDefaultTenantProfile($event, entity) + } + ); + + this.config.deleteEntityTitle = tenantProfile => this.translate.instant('tenant-profile.delete-tenant-profile-title', + { tenantProfileName: tenantProfile.name }); + this.config.deleteEntityContent = () => this.translate.instant('tenant-profile.delete-tenant-profile-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('tenant-profile.delete-tenant-profiles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('tenant-profile.delete-tenant-profiles-text'); + + this.config.entitiesFetchFunction = pageLink => this.tenantProfileService.getTenantProfiles(pageLink); + this.config.loadEntity = id => this.tenantProfileService.getTenantProfile(id.id); + this.config.saveEntity = tenantProfile => this.tenantProfileService.saveTenantProfile(tenantProfile); + this.config.deleteEntity = id => this.tenantProfileService.deleteTenantProfile(id.id); + this.config.onEntityAction = action => this.onTenantProfileAction(action); + this.config.deleteEnabled = (tenantProfile) => tenantProfile && !tenantProfile.default; + this.config.entitySelectionEnabled = (tenantProfile) => tenantProfile && !tenantProfile.default; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('tenant-profile.tenant-profiles'); + + return this.config; + } + + setDefaultTenantProfile($event: Event, tenantProfile: TenantProfile) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('tenant-profile.set-default-tenant-profile-title', {tenantProfileName: tenantProfile.name}), + this.translate.instant('tenant-profile.set-default-tenant-profile-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.tenantProfileService.setDefaultTenantProfile(tenantProfile.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onTenantProfileAction(action: EntityAction): boolean { + switch (action.action) { + case 'setDefault': + this.setDefaultTenantProfile(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts index f6374330dc..388de81fda 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts @@ -18,14 +18,14 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; -import { Tenant } from '@shared/models/tenant.model'; +import { TenantInfo } from '@shared/models/tenant.model'; @Component({ selector: 'tb-tenant-tabs', templateUrl: './tenant-tabs.component.html', styleUrls: [] }) -export class TenantTabsComponent extends EntityTabsComponent { +export class TenantTabsComponent extends EntityTabsComponent { constructor(protected store: Store) { super(store); diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html index 335a7d3c9a..dd603d4a91 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html @@ -49,6 +49,12 @@ {{ 'tenant.title-required' | translate }} + +
    tenant.description @@ -56,16 +62,6 @@
    -
    - -
    {{ 'tenant.isolated-tb-core' | translate }}
    -
    {{'tenant.isolated-tb-core-details' | translate}}
    -
    - -
    {{ 'tenant.isolated-tb-rule-engine' | translate }}
    -
    {{'tenant.isolated-tb-rule-engine-details' | translate}}
    -
    -
    diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts index 8a76830093..f139deb1b5 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts @@ -18,7 +18,7 @@ import { Component, Inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Tenant } from '@app/shared/models/tenant.model'; +import { Tenant, TenantInfo } from '@app/shared/models/tenant.model'; import { ActionNotificationShow } from '@app/core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; import { ContactBasedComponent } from '../../components/entity/contact-based.component'; @@ -27,14 +27,14 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod @Component({ selector: 'tb-tenant', templateUrl: './tenant.component.html', - styleUrls: ['./tenant.component.scss'] + styleUrls: [] }) -export class TenantComponent extends ContactBasedComponent { +export class TenantComponent extends ContactBasedComponent { constructor(protected store: Store, protected translate: TranslateService, - @Inject('entity') protected entityValue: Tenant, - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + @Inject('entity') protected entityValue: TenantInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, protected fb: FormBuilder) { super(store, fb, entityValue, entitiesTableConfigValue); } @@ -47,12 +47,11 @@ export class TenantComponent extends ContactBasedComponent { } } - buildEntityForm(entity: Tenant): FormGroup { + buildEntityForm(entity: TenantInfo): FormGroup { return this.fb.group( { title: [entity ? entity.title : '', [Validators.required]], - isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], - isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], + tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] @@ -64,8 +63,7 @@ export class TenantComponent extends ContactBasedComponent { updateEntityForm(entity: Tenant) { this.entityForm.patchValue({title: entity.title}); - this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); - this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); + this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); } @@ -73,10 +71,6 @@ export class TenantComponent extends ContactBasedComponent { if (this.entityForm) { if (this.isEditValue) { this.entityForm.enable({emitEvent: false}); - if (!this.isAdd) { - this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); - this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); - } } else { this.entityForm.disable({emitEvent: false}); } @@ -94,4 +88,7 @@ export class TenantComponent extends ContactBasedComponent { })); } + onTenantProfileUpdated() { + this.entitiesTableConfig.table.updateData(false); + } } diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts index 11e1d1e466..c3c0c14e88 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts @@ -18,7 +18,7 @@ import { Injectable } from '@angular/core'; import { Resolve, Router } from '@angular/router'; -import { Tenant } from '@shared/models/tenant.model'; +import { TenantInfo } from '@shared/models/tenant.model'; import { DateEntityTableColumn, EntityTableColumn, @@ -31,11 +31,12 @@ import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { TenantTabsComponent } from '@home/pages/tenant/tenant-tabs.component'; +import { mergeMap } from 'rxjs/operators'; @Injectable() -export class TenantsTableConfigResolver implements Resolve> { +export class TenantsTableConfigResolver implements Resolve> { - private readonly config: EntityTableConfig = new EntityTableConfig(); + private readonly config: EntityTableConfig = new EntityTableConfig(); constructor(private tenantService: TenantService, private translate: TranslateService, @@ -49,11 +50,12 @@ export class TenantsTableConfigResolver implements Resolve('createdTime', 'common.created-time', this.datePipe, '150px'), - new EntityTableColumn('title', 'tenant.title', '25%'), - new EntityTableColumn('email', 'contact.email', '25%'), - new EntityTableColumn('country', 'contact.country', '25%'), - new EntityTableColumn('city', 'contact.city', '25%') + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'tenant.title', '20%'), + new EntityTableColumn('tenantProfileName', 'tenant-profile.tenant-profile', '20%'), + new EntityTableColumn('email', 'contact.email', '20%'), + new EntityTableColumn('country', 'contact.country', '20%'), + new EntityTableColumn('city', 'contact.city', '20%') ); this.config.cellActionDescriptors.push( @@ -70,27 +72,29 @@ export class TenantsTableConfigResolver implements Resolve this.translate.instant('tenant.delete-tenants-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('tenant.delete-tenants-text'); - this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenants(pageLink); - this.config.loadEntity = id => this.tenantService.getTenant(id.id); - this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant); + this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenantInfos(pageLink); + this.config.loadEntity = id => this.tenantService.getTenantInfo(id.id); + this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant).pipe( + mergeMap((savedTenant) => this.tenantService.getTenantInfo(savedTenant.id.id)) + ); this.config.deleteEntity = id => this.tenantService.deleteTenant(id.id); this.config.onEntityAction = action => this.onTenantAction(action); } - resolve(): EntityTableConfig { + resolve(): EntityTableConfig { this.config.tableTitle = this.translate.instant('tenant.tenants'); return this.config; } - manageTenantAdmins($event: Event, tenant: Tenant) { + manageTenantAdmins($event: Event, tenant: TenantInfo) { if ($event) { $event.stopPropagation(); } this.router.navigateByUrl(`tenants/${tenant.id.id}/users`); } - onTenantAction(action: EntityAction): boolean { + onTenantAction(action: EntityAction): boolean { switch (action.action) { case 'manageTenantAdmins': this.manageTenantAdmins(action.event, action.entity); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss index afb804612a..6ae687c691 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss @@ -120,7 +120,6 @@ div.tb-editor-area-title-panel { label { padding: 4px; color: #00acc1; - text-transform: uppercase; background: rgba(220, 220, 220, .35); border-radius: 5px; &:not(:last-child) { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html index d751799ccb..e1df8b4335 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html @@ -16,7 +16,7 @@ -->
    widgets-bundle.empty
    > { @@ -55,6 +56,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve widgetsBundle ? widgetsBundle.title : ''; diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index ddb24b3639..e8e9fc29f2 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -75,7 +75,6 @@ display: flex; justify-content: center; align-items: center; - text-transform: uppercase; } } } diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index 1264acc3a2..d7d4438566 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -19,7 +19,7 @@

    {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}

    - + diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index a934e29093..401d72859b 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -20,6 +20,7 @@ import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; +import { guid } from '@core/utils'; @Component({ selector: 'tb-breadcrumb', @@ -94,6 +95,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { const isMdiIcon = icon.startsWith('mdi:'); const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ]; const breadcrumb = { + id: guid(), label, labelFunction, ignoreTranslate, @@ -110,4 +112,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { } return newBreadcrumbs; } + + trackByBreadcrumbs(index: number, breadcrumb: BreadCrumb){ + return breadcrumb.id; + } } diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts index 2a1f3d5988..bf43f8ef5b 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -16,8 +16,9 @@ import { ActivatedRouteSnapshot, Params } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { HasUUID } from '@shared/models/id/has-uuid'; -export interface BreadCrumb { +export interface BreadCrumb extends HasUUID{ label: string; labelFunction?: () => string; ignoreTranslate: boolean; diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss index 01efd57308..2681932b3f 100644 --- a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss @@ -77,7 +77,6 @@ label { padding: 4px; color: #00acc1; - text-transform: uppercase; background: rgba(220, 220, 220, .35); border-radius: 5px; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 86a45c8885..27c156790b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -77,6 +77,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @Input() excludeEntityIds: Array; + @Input() + labelText: string; + + @Input() + requiredText: string; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -212,6 +218,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit break; } } + if (this.labelText && this.labelText.length) { + this.entityText = this.labelText; + } + if (this.requiredText && this.requiredText.length) { + this.entityRequiredText = this.requiredText; + } const currentEntity = this.getCurrentEntity(); if (currentEntity) { const currentEntityType = currentEntity.id.entityType; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss index a79b9d24d6..8b59ccd29a 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss @@ -178,7 +178,6 @@ $previewSize: 100px !default; label { padding: 4px; color: #00acc1; - text-transform: uppercase; background: rgba(220, 220, 220, .35); border-radius: 5px; } @@ -263,6 +262,10 @@ $previewSize: 100px !default; transform: translate(0%, -50%) !important; } + .MuiButton-root { + text-transform: none; + } + } .rc-select { diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts index e8ae19519d..503b12c1a8 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts @@ -91,6 +91,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va errorShowed = false; + ignoreChange = false; + private propagateChange = null; constructor(public elementRef: ElementRef, @@ -118,8 +120,10 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va this.jsonEditor.session.setUseWrapMode(false); this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); this.jsonEditor.on('change', () => { - this.cleanupJsonErrors(); - this.updateView(); + if (!this.ignoreChange) { + this.cleanupJsonErrors(); + this.updateView(); + } }); this.editorResize$ = new ResizeObserver(() => { this.onAceEditorResize(); @@ -225,7 +229,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va // } if (this.jsonEditor) { + this.ignoreChange = true; this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); + this.ignoreChange = false; } } diff --git a/ui-ngx/src/app/shared/components/kv-map.component.scss b/ui-ngx/src/app/shared/components/kv-map.component.scss index a0ef9affc7..203f234c7c 100644 --- a/ui-ngx/src/app/shared/components/kv-map.component.scss +++ b/ui-ngx/src/app/shared/components/kv-map.component.scss @@ -19,7 +19,6 @@ position: relative; display: flex; height: 40px; - text-transform: uppercase; &.disabled { color: rgba(0, 0, 0, .38); diff --git a/ui-ngx/src/app/shared/components/logo.component.ts b/ui-ngx/src/app/shared/components/logo.component.ts index 255348054b..2069778e4b 100644 --- a/ui-ngx/src/app/shared/components/logo.component.ts +++ b/ui-ngx/src/app/shared/components/logo.component.ts @@ -23,7 +23,7 @@ import { Component } from '@angular/core'; }) export class LogoComponent { - logo = require('../../../assets/logo_title_white.svg').default; + logo = 'assets/logo_title_white.svg'; gotoThingsboard(): void { window.open('https://thingsboard.io', '_blank'); diff --git a/ui-ngx/src/app/shared/components/nav-tree.component.ts b/ui-ngx/src/app/shared/components/nav-tree.component.ts index fb1efa4b9e..a06a33d5c5 100644 --- a/ui-ngx/src/app/shared/components/nav-tree.component.ts +++ b/ui-ngx/src/app/shared/components/nav-tree.component.ts @@ -150,13 +150,13 @@ export class NavTreeComponent implements OnInit { this.treeElement.on('changed.jstree', (e: any, data) => { const node: NavTreeNode = data.instance.get_selected(true)[0]; if (this.onNodeSelected) { - this.onNodeSelected(node, e as Event); + this.ngZone.run(() => this.onNodeSelected(node, e as Event)); } }); this.treeElement.on('model.jstree', (e: any, data) => { if (this.onNodesInserted) { - this.onNodesInserted(data.nodes, data.parent); + this.ngZone.run(() => this.onNodesInserted(data.nodes, data.parent)); } }); diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.html b/ui-ngx/src/app/shared/components/time/timezone-select.component.html new file mode 100644 index 0000000000..b5e063ac72 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.html @@ -0,0 +1,50 @@ + + + timezone.timezone + + + + + + + + + {{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} + + + + + {{ 'timezone.timezone-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts new file mode 100644 index 0000000000..7dc89df7da --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts @@ -0,0 +1,221 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import * as _moment from 'moment-timezone'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; + +interface TimezoneInfo { + id: string; + name: string; + offset: string; + nOffset: number; +} + +@Component({ + selector: 'tb-timezone-select', + templateUrl: './timezone-select.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimezoneSelectComponent), + multi: true + }] +}) +export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectTimezoneFormGroup: FormGroup; + + modelValue: string | null; + + defaultTimezoneId: string = null; + + defaultTimezoneInfo: TimezoneInfo = null; + + timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => { + const tz = _moment.tz(zoneName); + return { + id: zoneName, + name: zoneName.replace(/_/g, ' '), + offset: `UTC${tz.format('Z')}`, + nOffset: tz.utcOffset() + } + }); + + @Input() + set defaultTimezone(timezone: string) { + if (this.defaultTimezoneId !== timezone) { + this.defaultTimezoneId = timezone; + if (this.defaultTimezoneId) { + this.defaultTimezoneInfo = + this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId); + } else { + this.defaultTimezoneInfo = null; + } + } + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('timezoneInput', {static: true, read: MatAutocompleteTrigger}) timezoneInputTrigger: MatAutocompleteTrigger; + + filteredTimezones: Observable>; + + searchText = ''; + + ignoreClosePanel = false; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private ngZone: NgZone, + private fb: FormBuilder) { + this.selectTimezoneFormGroup = this.fb.group({ + timezone: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredTimezones = this.selectTimezoneFormGroup.get('timezone').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchTimezones(name) ), + share() + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectTimezoneFormGroup.disable({emitEvent: false}); + } else { + this.selectTimezoneFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + let foundTimezone: TimezoneInfo = null; + if (value !== null) { + foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value); + } + if (foundTimezone !== null) { + this.modelValue = value; + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); + } else { + if (this.defaultTimezoneInfo) { + this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false}); + setTimeout(() => { + this.updateView(this.defaultTimezoneInfo.id); + }, 0); + } else { + this.modelValue = null; + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); + } + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectTimezoneFormGroup.get('timezone').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + onPanelClosed() { + if (this.ignoreClosePanel) { + this.ignoreClosePanel = false; + } else { + if (!this.modelValue && this.defaultTimezoneInfo) { + this.ngZone.run(() => { + this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true}); + }); + } + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayTimezoneFn(timezone?: TimezoneInfo): string | undefined { + return timezone ? `${timezone.name} (${timezone.offset})` : undefined; + } + + fetchTimezones(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.timezones; + if (searchText && searchText.length) { + result = this.timezones.filter((timezoneInfo) => + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + clear() { + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.timezoneInputTrigger.openPanel(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts index 87301900c0..f9b42f12f5 100644 --- a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts @@ -1379,5 +1379,11 @@ export const serviceCompletions: TbEditorCompletions = { 'See DomSanitizer for API reference.', meta: 'service', type: 'DomSanitizer' + }, + router: { + description: 'Router Service
    ' + + 'See Router for API reference.', + meta: 'service', + type: 'Router' } }; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 5e8665c655..cff9b26c2b 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -107,9 +107,11 @@ export const HelpLinks = { ruleNodeRestApiCall: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node', ruleNodeSendEmail: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node', tenants: helpBaseUrl + '/docs/user-guide/ui/tenants', + tenantProfiles: helpBaseUrl + '/docs/user-guide/ui/tenant-profiles', customers: helpBaseUrl + '/docs/user-guide/ui/customers', users: helpBaseUrl + '/docs/user-guide/ui/users', devices: helpBaseUrl + '/docs/user-guide/ui/devices', + deviceProfiles: helpBaseUrl + '/docs/user-guide/ui/device-profiles', assets: helpBaseUrl + '/docs/user-guide/ui/assets', entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 780f60904e..addb726ed5 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -20,6 +20,323 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { CustomerId } from '@shared/models/id/customer-id'; import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id'; import { EntitySearchQuery } from '@shared/models/relation.models'; +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { EntityInfoData } from '@shared/models/entity.models'; +import { KeyFilter } from '@shared/models/query/query.models'; +import { TimeUnit } from '@shared/models/time/time.models'; + +export enum DeviceProfileType { + DEFAULT = 'DEFAULT' +} + +export enum DeviceTransportType { + DEFAULT = 'DEFAULT', + MQTT = 'MQTT', + LWM2M = 'LWM2M' +} + +export enum MqttTransportPayloadType { + JSON = 'JSON', + PROTOBUF = 'PROTOBUF' +} + +export interface DeviceConfigurationFormInfo { + hasProfileConfiguration: boolean; + hasDeviceConfiguration: boolean; +} + +export const deviceProfileTypeTranslationMap = new Map( + [ + [DeviceProfileType.DEFAULT, 'device-profile.type-default'] + ] +); + +export const deviceProfileTypeConfigurationInfoMap = new Map( + [ + [ + DeviceProfileType.DEFAULT, + { + hasProfileConfiguration: false, + hasDeviceConfiguration: false, + } + ] + ] +); + +export const deviceTransportTypeTranslationMap = new Map( + [ + [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default'], + [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt'], + [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m'] + ] +); + +export const mqttTransportPayloadTypeTranslationMap = new Map( + [ + [MqttTransportPayloadType.JSON, 'device-profile.mqtt-device-payload-type-json'], + [MqttTransportPayloadType.PROTOBUF, 'device-profile.mqtt-device-payload-type-proto'] + ] +); + + +export const deviceTransportTypeConfigurationInfoMap = new Map( + [ + [ + DeviceTransportType.DEFAULT, + { + hasProfileConfiguration: false, + hasDeviceConfiguration: false, + } + ], + [ + DeviceTransportType.MQTT, + { + hasProfileConfiguration: true, + hasDeviceConfiguration: true, + } + ], + [ + DeviceTransportType.LWM2M, + { + hasProfileConfiguration: true, + hasDeviceConfiguration: true, + } + ] + ] +); + +export interface DefaultDeviceProfileConfiguration { + [key: string]: any; +} + +export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration; + +export interface DeviceProfileConfiguration extends DeviceProfileConfigurations { + type: DeviceProfileType; +} + +export interface DefaultDeviceProfileTransportConfiguration { + [key: string]: any; +} + +export interface MqttDeviceProfileTransportConfiguration { + deviceTelemetryTopic?: string; + deviceAttributesTopic?: string; + [key: string]: any; +} + +export interface Lwm2mDeviceProfileTransportConfiguration { + [key: string]: any; +} + +export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration & + MqttDeviceProfileTransportConfiguration & + Lwm2mDeviceProfileTransportConfiguration; + +export interface DeviceProfileTransportConfiguration extends DeviceProfileTransportConfigurations { + type: DeviceTransportType; +} + +export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { + let configuration: DeviceProfileConfiguration = null; + if (type) { + switch (type) { + case DeviceProfileType.DEFAULT: + const defaultConfiguration: DefaultDeviceProfileConfiguration = {}; + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; + break; + } + } + return configuration; +} + +export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { + let configuration: DeviceConfiguration = null; + if (type) { + switch (type) { + case DeviceProfileType.DEFAULT: + const defaultConfiguration: DefaultDeviceConfiguration = {}; + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; + break; + } + } + return configuration; +} + +export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { + let transportConfiguration: DeviceProfileTransportConfiguration = null; + if (type) { + switch (type) { + case DeviceTransportType.DEFAULT: + const defaultTransportConfiguration: DefaultDeviceProfileTransportConfiguration = {}; + transportConfiguration = {...defaultTransportConfiguration, type: DeviceTransportType.DEFAULT}; + break; + case DeviceTransportType.MQTT: + const mqttTransportConfiguration: MqttDeviceProfileTransportConfiguration = { + deviceTelemetryTopic: 'v1/devices/me/telemetry', + deviceAttributesTopic: 'v1/devices/me/attributes', + transportPayloadType: MqttTransportPayloadType.JSON + }; + transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; + break; + case DeviceTransportType.LWM2M: + const lwm2mTransportConfiguration: Lwm2mDeviceProfileTransportConfiguration = {}; + transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; + break; + } + } + return transportConfiguration; +} + +export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { + let transportConfiguration: DeviceTransportConfiguration = null; + if (type) { + switch (type) { + case DeviceTransportType.DEFAULT: + const defaultTransportConfiguration: DefaultDeviceTransportConfiguration = {}; + transportConfiguration = {...defaultTransportConfiguration, type: DeviceTransportType.DEFAULT}; + break; + case DeviceTransportType.MQTT: + const mqttTransportConfiguration: MqttDeviceTransportConfiguration = {}; + transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; + break; + case DeviceTransportType.LWM2M: + const lwm2mTransportConfiguration: Lwm2mDeviceTransportConfiguration = {}; + transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; + break; + } + } + return transportConfiguration; +} + +export enum AlarmConditionType { + SIMPLE = 'SIMPLE', + DURATION = 'DURATION', + REPEATING = 'REPEATING' +} + +export const AlarmConditionTypeTranslationMap = new Map( + [ + [AlarmConditionType.SIMPLE, 'device-profile.condition-type-simple'], + [AlarmConditionType.DURATION, 'device-profile.condition-type-duration'], + [AlarmConditionType.REPEATING, 'device-profile.condition-type-repeating'] + ] +); + +export interface AlarmConditionSpec{ + type?: AlarmConditionType; + unit?: TimeUnit; + value?: number; + count?: number; +} + +export interface AlarmCondition { + condition: Array; + spec?: AlarmConditionSpec; +} + +export enum AlarmScheduleType { + ANY_TIME = 'ANY_TIME', + SPECIFIC_TIME = 'SPECIFIC_TIME', + CUSTOM = 'CUSTOM' +} + +export const AlarmScheduleTypeTranslationMap = new Map( + [ + [AlarmScheduleType.ANY_TIME, 'device-profile.schedule-any-time'], + [AlarmScheduleType.SPECIFIC_TIME, 'device-profile.schedule-specific-time'], + [AlarmScheduleType.CUSTOM, 'device-profile.schedule-custom'] + ] +); + +export interface AlarmSchedule{ + type: AlarmScheduleType; + timezone?: string; + daysOfWeek?: number[]; + startsOn?: number; + endsOn?: number; + items?: CustomTimeSchedulerItem[]; +} + +export interface CustomTimeSchedulerItem{ + enabled: boolean; + dayOfWeek: number; + startsOn: number; + endsOn: number; +} + +export interface AlarmRule { + condition: AlarmCondition; + alarmDetails?: string; + schedule?: AlarmSchedule; +} + +export interface DeviceProfileAlarm { + id: string; + alarmType: string; + createRules: {[severity: string]: AlarmRule}; + clearRule?: AlarmRule; + propagate?: boolean; + propagateRelationTypes?: Array; +} + +export interface DeviceProfileData { + configuration: DeviceProfileConfiguration; + transportConfiguration: DeviceProfileTransportConfiguration; + alarms?: Array; +} + +export interface DeviceProfile extends BaseData { + tenantId?: TenantId; + name: string; + description?: string; + default?: boolean; + type: DeviceProfileType; + transportType: DeviceTransportType; + defaultRuleChainId?: RuleChainId; + profileData: DeviceProfileData; +} + +export interface DeviceProfileInfo extends EntityInfoData { + type: DeviceProfileType; + transportType: DeviceTransportType; +} + +export interface DefaultDeviceConfiguration { + [key: string]: any; +} + +export type DeviceConfigurations = DefaultDeviceConfiguration; + +export interface DeviceConfiguration extends DeviceConfigurations { + type: DeviceProfileType; +} + +export interface DefaultDeviceTransportConfiguration { + [key: string]: any; +} + +export interface MqttDeviceTransportConfiguration { + [key: string]: any; +} + +export interface Lwm2mDeviceTransportConfiguration { + [key: string]: any; +} + +export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration & + MqttDeviceTransportConfiguration & + Lwm2mDeviceTransportConfiguration; + +export interface DeviceTransportConfiguration extends DeviceTransportConfigurations { + type: DeviceTransportType; +} + +export interface DeviceData { + configuration: DeviceConfiguration; + transportConfiguration: DeviceTransportConfiguration; +} export interface Device extends BaseData { tenantId?: TenantId; @@ -27,23 +344,28 @@ export interface Device extends BaseData { name: string; type: string; label: string; + deviceProfileId?: DeviceProfileId; + deviceData?: DeviceData; additionalInfo?: any; } export interface DeviceInfo extends Device { customerTitle: string; customerIsPublic: boolean; + deviceProfileName: string; } export enum DeviceCredentialsType { ACCESS_TOKEN = 'ACCESS_TOKEN', - X509_CERTIFICATE = 'X509_CERTIFICATE' + X509_CERTIFICATE = 'X509_CERTIFICATE', + MQTT_BASIC = 'MQTT_BASIC' } export const credentialTypeNames = new Map( [ [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], - [DeviceCredentialsType.X509_CERTIFICATE, 'X.509 Certificate'], + [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], + [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'] ] ); @@ -54,6 +376,12 @@ export interface DeviceCredentials extends BaseData { credentialsValue: string; } +export interface DeviceCredentialMQTTBasic { + clientId: string; + userName: string; + password: string; +} + export interface DeviceSearchQuery extends EntitySearchQuery { deviceTypes: Array; } @@ -69,6 +397,6 @@ export enum ClaimResponse { } export interface ClaimResult { - device: Device, - response: ClaimResponse + device: Device; + response: ClaimResponse; } diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 6841a911b7..c6a94ad8b5 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -35,11 +35,13 @@ import { BaseData, HasId } from '@shared/models/base-data'; export enum EntityType { TENANT = 'TENANT', + TENANT_PROFILE = 'TENANT_PROFILE', CUSTOMER = 'CUSTOMER', USER = 'USER', DASHBOARD = 'DASHBOARD', ASSET = 'ASSET', DEVICE = 'DEVICE', + DEVICE_PROFILE = 'DEVICE_PROFILE', ALARM = 'ALARM', RULE_CHAIN = 'RULE_CHAIN', RULE_NODE = 'RULE_NODE', @@ -88,6 +90,20 @@ export const entityTypeTranslations = new Map( export enum DynamicValueSourceType { CURRENT_TENANT = 'CURRENT_TENANT', CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', - CURRENT_USER = 'CURRENT_USER' + CURRENT_USER = 'CURRENT_USER', + CURRENT_DEVICE = 'CURRENT_DEVICE' } export const dynamicValueSourceTypeTranslationMap = new Map( [ [DynamicValueSourceType.CURRENT_TENANT, 'filter.current-tenant'], [DynamicValueSourceType.CURRENT_CUSTOMER, 'filter.current-customer'], - [DynamicValueSourceType.CURRENT_USER, 'filter.current-user'] + [DynamicValueSourceType.CURRENT_USER, 'filter.current-user'], + [DynamicValueSourceType.CURRENT_DEVICE, 'filter.current-device'] ] ); @@ -287,26 +294,26 @@ export interface FilterPredicateValue { } export interface StringFilterPredicate { - type: FilterPredicateType.STRING, + type: FilterPredicateType.STRING; operation: StringOperation; value: FilterPredicateValue; ignoreCase: boolean; } export interface NumericFilterPredicate { - type: FilterPredicateType.NUMERIC, + type: FilterPredicateType.NUMERIC; operation: NumericOperation; value: FilterPredicateValue; } export interface BooleanFilterPredicate { - type: FilterPredicateType.BOOLEAN, + type: FilterPredicateType.BOOLEAN; operation: BooleanOperation; value: FilterPredicateValue; } export interface BaseComplexFilterPredicate { - type: FilterPredicateType.COMPLEX, + type: FilterPredicateType.COMPLEX; operation: ComplexOperation; predicates: Array; } @@ -335,6 +342,7 @@ export interface KeyFilterPredicateInfo { export interface KeyFilter { key: EntityKey; + valueType: EntityKeyValueType; predicate: KeyFilterPredicate; } @@ -354,6 +362,140 @@ export interface FiltersInfo { datasourceFilters: {[datasourceIndex: number]: FilterInfo}; } +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array): string { + const filtersText = keyFilters.map(keyFilter => + keyFilterToText(translate, datePipe, keyFilter, + keyFilters.length > 1 ? ComplexOperation.AND : undefined)); + let result: string; + if (filtersText.length > 1) { + const andText = translate.instant('filter.operation.and'); + result = filtersText.join(' ' + andText + ' '); + } else { + result = filtersText[0]; + } + return result; +} + +export function keyFilterToText(translate: TranslateService, datePipe: DatePipe, keyFilter: KeyFilter, + parentComplexOperation?: ComplexOperation): string { + const keyFilterPredicate = keyFilter.predicate; + return keyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate, parentComplexOperation); +} + +export function keyFilterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: KeyFilter, + keyFilterPredicate: KeyFilterPredicate, + parentComplexOperation?: ComplexOperation): string { + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate; + const complexOperation = complexPredicate.operation; + const complexPredicatesText = + complexPredicate.predicates.map(predicate => keyFilterPredicateToText(translate, datePipe, keyFilter, predicate, complexOperation)); + if (complexPredicatesText.length > 1) { + const operationText = translate.instant(complexOperationTranslationMap.get(complexOperation)); + let result = complexPredicatesText.join(' ' + operationText + ' '); + if (complexOperation === ComplexOperation.OR && parentComplexOperation && ComplexOperation.OR !== parentComplexOperation) { + result = `(${result})`; + } + return result; + } else { + return complexPredicatesText[0]; + } + } else { + return simpleKeyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate); + } +} + +function simpleKeyFilterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: KeyFilter, + keyFilterPredicate: StringFilterPredicate | + NumericFilterPredicate | + BooleanFilterPredicate): string { + const key = keyFilter.key.key; + let operation: string; + let value: string; + const val = keyFilterPredicate.value; + const dynamicValue = !!val.dynamicValue && !!val.dynamicValue.sourceType; + if (dynamicValue) { + value = '' + + translate.instant(dynamicValueSourceTypeTranslationMap.get(val.dynamicValue.sourceType)) + ''; + value += '.' + val.dynamicValue.sourceAttribute + ''; + } + switch (keyFilterPredicate.type) { + case FilterPredicateType.STRING: + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); + if (keyFilterPredicate.ignoreCase) { + operation += ' ' + translate.instant('filter.ignore-case'); + } + if (!dynamicValue) { + value = `'${keyFilterPredicate.value.defaultValue}'`; + } + break; + case FilterPredicateType.NUMERIC: + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { + value = datePipe.transform(keyFilterPredicate.value.defaultValue, 'yyyy-MM-dd HH:mm'); + } else { + value = keyFilterPredicate.value.defaultValue + ''; + } + } + break; + case FilterPredicateType.BOOLEAN: + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); + value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false'); + break; + } + if (!dynamicValue) { + value = `${value}`; + } + return `${key} ${operation} ${value}`; +} + +export function keyFilterInfosToKeyFilters(keyFilterInfos: Array): Array { + if (!keyFilterInfos) { + return []; + } + const keyFilters: Array = []; + for (const keyFilterInfo of keyFilterInfos) { + const key = keyFilterInfo.key; + for (const predicate of keyFilterInfo.predicates) { + const keyFilter: KeyFilter = { + key, + valueType: keyFilterInfo.valueType, + predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) + }; + keyFilters.push(keyFilter); + } + } + return keyFilters; +} + +export function keyFiltersToKeyFilterInfos(keyFilters: Array): Array { + const keyFilterInfos: Array = []; + const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; + for (const keyFilter of keyFilters) { + const key = keyFilter.key; + const infoKey = key.key + key.type + keyFilter.valueType; + let keyFilterInfo = keyFilterInfoMap[infoKey]; + if (!keyFilterInfo) { + keyFilterInfo = { + key, + valueType: keyFilter.valueType, + predicates: [] + }; + keyFilterInfoMap[infoKey] = keyFilterInfo; + keyFilterInfos.push(keyFilterInfo); + } + if (keyFilter.predicate) { + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); + } + } + return keyFilterInfos; +} + export function filterInfoToKeyFilters(filter: FilterInfo): Array { const keyFilterInfos = filter.keyFilters; const keyFilters: Array = []; @@ -362,6 +504,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array { for (const predicate of keyFilterInfo.predicates) { const keyFilter: KeyFilter = { key, + valueType: keyFilterInfo.valueType, predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) }; keyFilters.push(keyFilter); @@ -384,6 +527,26 @@ export function keyFilterPredicateInfoToKeyFilterPredicate(keyFilterPredicateInf return keyFilterPredicate; } +export function keyFilterPredicateToKeyFilterPredicateInfo(keyFilterPredicate: KeyFilterPredicate): KeyFilterPredicateInfo { + const keyFilterPredicateInfo: KeyFilterPredicateInfo = { + keyFilterPredicate: null, + userInfo: null + }; + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate; + const predicateInfos = complexPredicate.predicates.map( + predicate => keyFilterPredicateToKeyFilterPredicateInfo(predicate)); + keyFilterPredicateInfo.keyFilterPredicate = { + predicates: predicateInfos, + operation: complexPredicate.operation, + type: FilterPredicateType.COMPLEX + } as ComplexFilterPredicateInfo; + } else { + keyFilterPredicateInfo.keyFilterPredicate = keyFilterPredicate; + } + return keyFilterPredicateInfo; +} + export function isFilterEditable(filter: FilterInfo): boolean { if (filter.editable) { return filter.keyFilters.some(value => isKeyFilterInfoEditable(value)); @@ -481,7 +644,7 @@ export interface Filter extends FilterInfo { } export interface Filters { - [id: string]: Filter + [id: string]: Filter; } export interface EntityFilter extends EntityFilters { @@ -535,7 +698,7 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag }, direction: Direction.DESC } - } + }; } export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 65a176de0d..378dca7c25 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -16,11 +16,29 @@ import { ContactBased } from '@shared/models/contact-based.model'; import { TenantId } from './id/tenant-id'; +import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; +import { BaseData } from '@shared/models/base-data'; + +export interface TenantProfileData { + [key: string]: string; +} + +export interface TenantProfile extends BaseData { + name: string; + description?: string; + default?: boolean; + isolatedTbCore?: boolean; + isolatedTbRuleEngine?: boolean; + profileData?: TenantProfileData; +} export interface Tenant extends ContactBased { title: string; region: string; - isolatedTbCore: boolean; - isolatedTbRuleEngine: boolean; + tenantProfileId: TenantProfileId; additionalInfo?: any; } + +export interface TenantInfo extends Tenant { + tenantProfileName: string; +} diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 1437719947..b81952dbde 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -465,3 +465,19 @@ export const defaultTimeIntervals = new Array( value: 30 * DAY } ); + +export enum TimeUnit { + SECONDS = 'SECONDS', + MINUTES = 'MINUTES', + HOURS = 'HOURS', + DAYS = 'DAYS' +} + +export const timeUnitTranslationMap = new Map( + [ + [TimeUnit.SECONDS, 'timeunit.seconds'], + [TimeUnit.MINUTES, 'timeunit.minutes'], + [TimeUnit.HOURS, 'timeunit.hours'], + [TimeUnit.DAYS, 'timeunit.days'] + ] +); diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 38551578c4..d071869808 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -134,6 +134,7 @@ import { HistorySelectorComponent } from './components/time/history-selector/his import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { ContactComponent } from '@shared/components/contact.component'; +import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @NgModule({ providers: [ @@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component'; DashboardSelectPanelComponent, DatetimePeriodComponent, DatetimeComponent, + TimezoneSelectComponent, ValueInputComponent, DashboardAutocompleteComponent, EntitySubTypeAutocompleteComponent, @@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component'; DashboardSelectComponent, DatetimePeriodComponent, DatetimeComponent, + TimezoneSelectComponent, DashboardAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, 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 0258408d15..52bc785588 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -718,6 +718,12 @@ "access-token-invalid": "Access token length must be from 1 to 20 characters.", "rsa-key": "RSA public key", "rsa-key-required": "RSA public key is required.", + "client-id": "Client ID", + "client-id-pattern": "Contains invalid character.", + "user-name": "User Name", + "user-name-required": "User Name is required.", + "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary", + "password": "Password", "secret": "Secret", "secret-required": "Secret is required.", "device-type": "Device type", @@ -748,7 +754,127 @@ "import": "Import device", "device-file": "Device file", "search": "Search devices", - "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected" + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected", + "device-configuration": "Device configuration", + "transport-configuration": "Transport configuration" + }, + "device-profile": { + "device-profile": "Device profile", + "device-profiles": "Device profiles", + "all-device-profiles": "All", + "add": "Add device profile", + "edit": "Edit device profile", + "device-profile-details": "Device profile details", + "no-device-profiles-text": "No device profiles found", + "search": "Search device profiles", + "selected-device-profiles": "{ count, plural, 1 {1 device profile} other {# device profiles} } selected", + "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", + "device-profile-required": "Device profile is required", + "idCopiedMessage": "Device profile Id has been copied to clipboard", + "set-default": "Make device profile default", + "delete": "Delete device profile", + "copyId": "Copy device profile Id", + "name": "Name", + "name-required": "Name is required.", + "type": "Profile type", + "type-required": "Profile type is required.", + "type-default": "Default", + "transport-type": "Transport type", + "transport-type-required": "Transport type is required.", + "transport-type-default": "Default", + "transport-type-mqtt": "MQTT", + "transport-type-lwm2m": "LWM2M", + "description": "Description", + "default": "Default", + "profile-configuration": "Profile configuration", + "transport-configuration": "Transport configuration", + "default-rule-chain": "Default rule chain", + "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", + "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.", + "delete-device-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 device profile} other {# device profiles} }?", + "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data will become unrecoverable.", + "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", + "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", + "no-device-profiles-found": "No device profiles found.", + "create-new-device-profile": "Create a new one!", + "mqtt-device-topic-filters": "MQTT device topic filters", + "mqtt-device-payload-type": "MQTT device payload", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Payload type is required.", + "support-level-wildcards": "Single [+] and multi-level [#] wildcards supported.", + "telemetry-topic-filter": "Telemetry topic filter", + "telemetry-topic-filter-required": "Telemetry topic filter is required.", + "attributes-topic-filter": "Attributes topic filter", + "attributes-topic-filter-required": "Attributes topic filter is required.", + "rpc-response-topic-filter": "RPC response topic filter", + "rpc-response-topic-filter-required": "RPC response topic filter is required.", + "not-valid-pattern-topic-filter": "Not valid pattern topic filter", + "not-valid-single-character": "Invalid use of a single-level wildcard character", + "not-valid-multi-character": "Invalid use of a multi-level wildcard character", + "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", + "alarm-rules": "Alarm rules ({{count}})", + "no-alarm-rules": "No alarm rules configured", + "add-alarm-rule": "Add alarm rule", + "edit-alarm-rule": "Edit alarm rule", + "alarm-type": "Alarm type", + "alarm-type-required": "Alarm type is required.", + "alarm-type-pattern-hint": "Alarm type pattern, use ${metaKeyName} to substitute variables from metadata", + "create-alarm-pattern": "Create {{alarmType}} alarm", + "create-alarm-rules": "Create alarm rules", + "no-create-alarm-rules": "No create conditions configured", + "clear-alarm-rule": "Clear alarm rule", + "no-clear-alarm-rule": "No clear condition configured", + "add-create-alarm-rule": "Add create condition", + "add-clear-alarm-rule": "Add clear condition", + "select-alarm-severity": "Select alarm severity", + "alarm-severity-required": "Alarm severity is required.", + "condition-duration": "Condition duration", + "condition-duration-value": "Duration value", + "condition-duration-time-unit": "Time unit", + "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", + "condition-duration-value-pattern": "Duration value should be integers.", + "condition-duration-value-required": "Duration value is required.", + "condition-duration-time-unit-required": "Time unit is required.", + "advanced-settings": "Advanced settings", + "alarm-rule-details": "Details", + "propagate-alarm": "Propagate alarm", + "alarm-rule-relation-types-list": "Relation types to propagate", + "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", + "alarm-details": "Alarm details", + "alarm-rule-condition": "Alarm rule condition", + "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", + "edit-alarm-rule-condition": "Edit alarm rule condition", + "condition": "Condition", + "condition-type": "Condition type", + "condition-type-simple": "Simple", + "condition-type-duration": "Duration", + "condition-type-repeating": "Repeating", + "condition-type-required": "Condition type is required.", + "condition-repeating-value": "Count of events", + "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", + "condition-repeating-value-pattern": "Count of events should be integers.", + "condition-repeating-value-required": "Count of events is required.", + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": "Schedule", + "schedule-any-time": "Active all the time", + "schedule-specific-time": "Active at a specific time", + "schedule-custom": "Custom", + "schedule-day": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "schedule-days": "Days", + "schedule-time": "Time", + "schedule-time-from": "From", + "schedule-time-to": "To" }, "dialog": { "close": "Close dialog" @@ -806,6 +932,10 @@ "type-devices": "Devices", "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", "device-name-starts-with": "Devices whose names start with '{{prefix}}'", + "type-device-profile": "Device profile", + "type-device-profiles": "Device profiles", + "list-of-device-profiles": "{ count, plural, 1 {One device profile} other {List of # device profiles} }", + "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", "type-asset": "Asset", "type-assets": "Assets", "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", @@ -826,6 +956,10 @@ "type-tenants": "Tenants", "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", + "type-tenant-profile": "Tenant profile", + "type-tenant-profiles": "Tenant profiles", + "list-of-tenant-profiles": "{ count, plural, 1 {One tenant profile} other {List of # tenant profiles} }", + "tenant-profile-name-starts-with": "Tenant profiles whose names start with '{{prefix}}'", "type-customer": "Customer", "type-customers": "Customers", "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", @@ -1182,6 +1316,8 @@ "filter": "Filter", "editable": "Editable", "no-filters-found": "No filters found.", + "no-filter-text": "No filter specified", + "add-filter-prompt": "Please add filter", "no-filter-matching": "'{{filter}}' not found.", "create-new-filter": "Create a new one!", "filter-required": "Filter is required.", @@ -1200,9 +1336,10 @@ "and": "and", "or": "or" }, - "ignore-case": "Ignore case", + "ignore-case": "ignore case", "value": "Value", "remove-filter": "Remove filter", + "preview": "Filter preview", "no-filters": "No filters configured", "add-filter": "Add filter", "add-complex-filter": "Add complex filter", @@ -1210,6 +1347,7 @@ "complex-filter": "Complex filter", "edit-complex-filter": "Edit complex filter", "edit-filter-user-params": "Edit filter predicate user parameters", + "filter-user-params": "Filter predicate user parameters", "user-parameters": "User parameters", "display-label": "Label to display", "autogenerated-label": "Auto generate label", @@ -1243,6 +1381,7 @@ "current-tenant": "Current tenant", "current-customer": "Current customer", "current-user": "Current user", + "current-device": "Current device", "default-value": "Default value", "dynamic-source-type": "Dynamic source type", "no-dynamic-value": "No dynamic value", @@ -1621,6 +1760,12 @@ "help": "Help", "reset-debug-mode": "Reset debug mode in all nodes" }, + "timezone": { + "timezone": "Timezone", + "select-timezone": "Select timezone", + "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", + "timezone-required": "Timezone is required." + }, "queue": { "select_name": "Select queue name", "name": "Queue Name", @@ -1659,6 +1804,35 @@ "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" }, + "tenant-profile": { + "tenant-profile": "Tenant profile", + "tenant-profiles": "Tenant profiles", + "add": "Add tenant profile", + "edit": "Edit tenant profile", + "tenant-profile-details": "Tenant profile details", + "no-tenant-profiles-text": "No tenant profiles found", + "search": "Search tenant profiles", + "selected-tenant-profiles": "{ count, plural, 1 {1 tenant profile} other {# tenant profiles} } selected", + "no-tenant-profiles-matching": "No tenant profile matching '{{entity}}' were found.", + "tenant-profile-required": "Tenant profile is required", + "idCopiedMessage": "Tenant profile Id has been copied to clipboard", + "set-default": "Make tenant profile default", + "delete": "Delete tenant profile", + "copyId": "Copy tenant profile Id", + "name": "Name", + "name-required": "Name is required.", + "data": "Profile data", + "description": "Description", + "default": "Default", + "delete-tenant-profile-title": "Are you sure you want to delete the tenant profile '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Be careful, after the confirmation the tenant profile and all related data will become unrecoverable.", + "delete-tenant-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 tenant profile} other {# tenant profiles} }?", + "delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.", + "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' default?", + "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified.", + "no-tenant-profiles-found": "No tenant profiles found.", + "create-new-tenant-profile": "Create a new one!" + }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", @@ -1670,6 +1844,12 @@ "seconds": "Seconds", "advanced": "Advanced" }, + "timeunit": { + "seconds": "Seconds", + "minutes": "Minutes", + "hours": "Hours", + "days": "Days" + }, "timewindow": { "days": "{ days, plural, 1 { day } other {# days } }", "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index e5f09184b9..c255440ba7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -1,12 +1,14 @@ { "access": { - "unauthorized": "권한 없음.", - "unauthorized-access": "허가되지 않은 접근", + "unauthorized": "승인되지 않음", + "unauthorized-access": "승인되지 않은 접근", "unauthorized-access-text": "이 리소스에 접근하려면 로그인해야 합니다!", "access-forbidden": "접근 금지", - "access-forbidden-text": "접근 권한이 없습니다.!
    만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", - "refresh-token-expired": "세션이 만료되었습니다.", - "refresh-token-failed": "세션을 새로 고칠 수 없습니다." + "access-forbidden-text": "접근 권한이 없습니다!
    만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", + "refresh-token-expired": "세션이 만료되었습니다", + "refresh-token-failed": "세션을 새로 고칠 수 없습니다.", + "permission-denied": "권한이 없습니다", + "permission-denied-text": "이 작업을 수행할 권한이 없습니다!" }, "action": { "activate": "활설화", @@ -22,11 +24,11 @@ "update": "업데이트", "remove": "제거", "search": "검색", - "clear-search": "Clear search", + "clear-search": "검색 초기화", "assign": "할당", "unassign": "비할당", "share": "Share", - "make-private": "Make private", + "make-private": "비공개로 설정", "apply": "적용", "apply-changes": "변경사항 적용", "edit-mode": "수정 모드", @@ -44,8 +46,8 @@ "undo": "취소", "copy": "복사", "paste": "붙여넣기", - "copy-reference": "Copy reference", - "paste-reference": "Paste reference", + "copy-reference": "참조 복사", + "paste-reference": "참조 붙여넣기", "import": "가져오기", "export": "내보내기", "share-via": "Share via {{provider}}" @@ -79,26 +81,26 @@ "smtp-port": "SMTP 포트", "smtp-port-required": "SMTP 포트를 입력해야 합니다.", "smtp-port-invalid": "올바른 SMTP 포트가 아닙니다.", - "timeout-msec": "제한시간 (msec)", - "timeout-required": "제한시간을 입력해야 합니다.", - "timeout-invalid": "올바른 제한시간이 아닙니다.", + "timeout-msec": "제한시간 (ms)", + "timeout-required": "제한시이 입력되지 않았습니다.", + "timeout-invalid": "제한시간이 올바르게 입력되지 않았습니다.", "enable-tls": "TLS 사용", "tls-version" : "TLS 버전", "send-test-mail": "테스트 메일 보내기" }, "alarm": { - "alarm": "Alarm", - "alarms": "Alarms", - "select-alarm": "Select alarm", - "no-alarms-matching": "No alarms matching '{{entity}}' were found.", - "alarm-required": "Alarm is required", - "alarm-status": "Alarm status", + "alarm": "알람", + "alarms": "알람", + "select-alarm": "알람 선택", + "no-alarms-matching": "'{{entity}}'에 대한 알람이 존재하지 않습니다.", + "alarm-required": "알람이 필요합니다", + "alarm-status": "알람 상태", "search-status": { "ANY": "Any", - "ACTIVE": "Active", + "ACTIVE": "활성", "CLEARED": "Cleared", - "ACK": "Acknowledged", - "UNACK": "Unacknowledged" + "ACK": "수용", + "UNACK": "불수용" }, "display-status": { "ACTIVE_UNACK": "Active Unacknowledged", @@ -107,28 +109,28 @@ "CLEARED_ACK": "Cleared Acknowledged" }, "no-alarms-prompt": "No alarms found", - "created-time": "Created time", - "type": "Type", - "severity": "Severity", - "originator": "Originator", - "originator-type": "Originator type", - "details": "Details", - "status": "Status", + "created-time": "생성된 시간", + "type": "종류", + "severity": "심각도", + "originator": "창시자", + "originator-type": "창시자 종류", + "details": "자세히", + "status": "상태", "alarm-details": "Alarm details", - "start-time": "Start time", - "end-time": "End time", + "start-time": "시작 시각", + "end-time": "마지막 시각", "ack-time": "Acknowledged time", "clear-time": "Cleared time", - "severity-critical": "Critical", - "severity-major": "Major", - "severity-minor": "Minor", - "severity-warning": "Warning", - "severity-indeterminate": "Indeterminate", - "acknowledge": "Acknowledge", - "clear": "Clear", - "search": "Search alarms", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected", - "no-data": "No data to display", + "severity-critical": "심각한", + "severity-major": "주요한", + "severity-minor": "작은", + "severity-warning": "경고", + "severity-indeterminate": "중간", + "acknowledge": "수용", + "clear": "지우기", + "search": "알람 검색", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } 선택됨", + "no-data": "표시할 데이터가 없습니다", "polling-interval": "Alarms polling interval (sec)", "polling-interval-required": "Alarms polling interval is required.", "min-polling-interval-message": "At least 1 sec polling interval is allowed.", @@ -178,46 +180,46 @@ "any-relation": "any" }, "asset": { - "asset": "Asset", - "assets": "Assets", - "management": "Asset management", - "view-assets": "View Assets", - "add": "Add Asset", - "assign-to-customer": "Assign to customer", - "assign-asset-to-customer": "Assign Asset(s) To Customer", - "assign-asset-to-customer-text": "Please select the assets to assign to the customer", - "no-assets-text": "No assets found", - "assign-to-customer-text": "Please select the customer to assign the asset(s)", - "public": "Public", - "assignedToCustomer": "Assigned to customer", - "make-public": "Make asset public", - "make-private": "Make asset private", - "unassign-from-customer": "Unassign from customer", - "delete": "Delete asset", - "asset-public": "Asset is public", - "asset-type": "Asset type", - "asset-type-required": "Asset type is required.", - "select-asset-type": "Select asset type", - "enter-asset-type": "Enter asset type", - "any-asset": "Any asset", - "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", - "asset-type-list-empty": "No asset types selected.", - "asset-types": "Asset types", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "type": "Type", - "type-required": "Type is required.", - "details": "Details", - "events": "Events", - "add-asset-text": "Add new asset", - "asset-details": "Asset details", - "assign-assets": "Assign assets", - "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", - "delete-assets": "Delete assets", - "unassign-assets": "Unassign assets", + "asset": "자산", + "assets": "자산", + "management": "자산 관리", + "view-assets": "자산 보기", + "add": "자산 추가", + "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": "이름을 입력하세요.", + "description": "설명", + "type": "종류", + "type-required": "종류를 입력하세요.", + "details": "자세히", + "events": "이벤트", + "add-asset-text": "새로운 자산 추가", + "asset-details": "자산 자세히", + "assign-assets": "자산 지정", + "assign-assets-text": "자산 { count, plural, 1 {1 asset} other {# assets} }을 고객에게 지정", + "delete-assets": "자산 삭제", + "unassign-assets": "자산 지정 해제", "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", - "assign-new-asset": "Assign new asset", + "assign-new-asset": "새로운 자산 지정", "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?", @@ -248,10 +250,11 @@ "scope-server": "서버 속성", "scope-shared": "공유 속성", "add": "속성 추가", - "key": "Key", - "key-required": "속성 key를 입력하세요.", + "key": "키", + "last-update-time": "마지막 수정된 시간", + "key-required": "속성 키를 입력하세요.", "value": "Value", - "value-required": "속성 value를 입력하세요.", + "value-required": "속성 값을 입력하세요.", "delete-attributes-title": "{ count, plural, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", "delete-attributes": "속성 삭제", @@ -264,38 +267,40 @@ "add-widget-to-dashboard": "대시보드에 위젯 추가", "selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨", "selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" + "no-attributes-text": "아무 속성도 찾을 수 없습니다", + "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다." }, "audit-log": { - "audit": "Audit", - "audit-logs": "Audit Logs", - "timestamp": "Timestamp", - "entity-type": "Entity Type", - "entity-name": "Entity Name", - "user": "User", - "type": "Type", - "status": "Status", - "details": "Details", - "type-added": "Added", - "type-deleted": "Deleted", - "type-updated": "Updated", - "type-attributes-updated": "Attributes updated", - "type-attributes-deleted": "Attributes deleted", + "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 call", - "type-credentials-updated": "Credentials updated", - "type-assigned-to-customer": "Assigned to Customer", - "type-unassigned-from-customer": "Unassigned from Customer", - "type-activated": "Activated", - "type-suspended": "Suspended", - "type-credentials-read": "Credentials read", - "type-attributes-read": "Attributes read", - "status-success": "Success", - "status-failure": "Failure", - "audit-log-details": "Audit log details", - "no-audit-logs-prompt": "No logs found", - "action-data": "Action data", - "failure-details": "Failure details", - "search": "Search audit logs", - "clear-search": "Clear search" + "type-credentials-updated": "자격 증명이 갱신되었습니다", + "type-assigned-to-customer": "고객에게 지정", + "type-unassigned-from-customer": "지정된 고객 해제", + "type-activated": "활성", + "type-suspended": "일시 중지", + "type-credentials-read": "자격 증명 읽기", + "type-attributes-read": "속성 읽기", + "status-success": "성공", + "status-failure": "실패", + "audit-log-details": "감사 로그 세부 사항", + "no-audit-logs-prompt": "아무 로그도 없습니다.", + "action-data": "액션 데이터", + "failure-details": "실패 세부 사항", + "search": "감사 로그 검색", + "clear-search": "검색 초기화" }, "confirm-on-exit": { "message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?", @@ -323,8 +328,8 @@ }, "content-type": { "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" + "text": "텍스트", + "binary": "바이너리 (Base64)" }, "customer": { "customers": "커스터머", @@ -337,10 +342,10 @@ "manage-customer-users": "커스터머 사용자 관리", "manage-customer-devices": "커스터머 디바이스 관리", "manage-customer-dashboards": "커스터머 대시보드 관리", - "manage-public-devices": "Manage public devices", - "manage-public-dashboards": "Manage public dashboards", - "manage-customer-assets": "Manage customer assets", - "manage-public-assets": "Manage public assets", + "manage-public-devices": "공개된 디바이스 관리", + "manage-public-dashboards": "공개된 대시보드 관리", + "manage-customer-assets": "고객 자산 관리", + "manage-public-assets": "공개된 자산 관리", "add-customer-text": "커스터머 추가", "no-customers-text": "커스터머가 없습니다.", "customer-details": "커스터머 상세정보", @@ -355,16 +360,16 @@ "title": "타이틀", "title-required": "타이틀을 입력하세요.", "description": "설명", - "details": "Details", - "events": "Events", - "copyId": "Copy customer Id", - "idCopiedMessage": "Customer Id has been copied to clipboard", - "select-customer": "Select customer", - "no-customers-matching": "No customers matching '{{entity}}' were found.", - "customer-required": "Customer is required", - "select-default-customer": "Select default customer", - "default-customer": "Default customer", - "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" + "details": "자세히", + "events": "이벤트", + "copyId": "고객 ID 복사", + "idCopiedMessage": "고객 ID가 클립 보드에 복사되었습니다.", + "select-customer": "선택된 고객", + "no-customers-matching": "'{{entity}}'에 해당하는 고객을 찾을 수 없습니다.", + "customer-required": "고객을 입력하세요.", + "select-default-customer": "기본 고객 선택", + "default-customer": "기본 고객", + "default-customer-required": "테넌트 수준에서 대시보드를 디버그 하기 위해서는 기본 고객이 필요합니다." }, "datetime": { "date-from": "시작 날짜", @@ -522,8 +527,8 @@ "assign-to-customer-text": "디바이스를 할당할 커스터머를 선택하세요.", "device-details": "디바이스 상세정보", "add-device-text": "디바이스 추가", - "credentials": "크리덴셜", - "manage-credentials": "크리덴셜 관리", + "credentials": "자격 증명", + "manage-credentials": "자격 증명 관리", "delete": "디바이스 삭제", "assign-devices": "디바이스 할당", "assign-devices-text": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커서터머에 할당", @@ -575,8 +580,8 @@ "unknown-error": "알 수 없는 오류" }, "entity": { - "entity": "Entity", - "entities": "Entities", + "entity": "개체", + "entities": "개체", "aliases": "Entity aliases", "entity-alias": "Entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias", @@ -588,70 +593,70 @@ "alias-required": "Entity alias is required.", "remove-alias": "Remove entity alias", "add-alias": "Add entity alias", - "entity-list": "Entity list", - "entity-type": "Entity type", - "entity-types": "Entity types", - "entity-type-list": "Entity type list", - "any-entity": "Any entity", - "enter-entity-type": "Enter entity type", + "entity-list": "개체 목록", + "entity-type": "개체 종류", + "entity-types": "개체 종류", + "entity-type-list": "개체 종류 목록", + "any-entity": "모든 개체", + "enter-entity-type": "개체 종류 입력", "no-entities-matching": "No entities matching '{{entity}}' were found.", "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", - "name-starts-with": "Name starts with", + "name-starts-with": "다음으로 시작하는 이름", "use-entity-name-filter": "Use filter", - "entity-list-empty": "No entities selected.", - "entity-type-list-empty": "No entity types selected.", + "entity-list-empty": "아무 개체도 선택되지 않았습니다.", + "entity-type-list-empty": "개체 종류가 선택되지 않았습니다.", "entity-name-filter-required": "Entity name filter is required.", "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", - "all-subtypes": "All", - "select-entities": "Select entities", + "all-subtypes": "모두", + "select-entities": "선택된 개체", "no-aliases-found": "No aliases found.", "no-alias-matching": "'{{alias}}' not found.", - "create-new-alias": "Create a new one!", - "key": "Key", - "key-name": "Key name", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' not found.", - "create-new-key": "Create a new one!", - "type": "Type", - "type-required": "Entity type is required.", - "type-device": "Device", - "type-devices": "Devices", + "create-new-alias": "생성 완료!", + "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 {One device} other {List of # devices} }", "device-name-starts-with": "Devices whose names start with '{{prefix}}'", - "type-asset": "Asset", - "type-assets": "Assets", + "type-asset": "자산", + "type-assets": "자산", "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", - "type-rule": "Rule", - "type-rules": "Rules", + "type-rule": "규칙", + "type-rules": "규칙", "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugins", + "type-plugin": "플러그인", + "type-plugins": "플러그인", "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", - "type-tenant": "Tenant", - "type-tenants": "Tenants", + "type-tenant": "테넌트", + "type-tenants": "테넌트", "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", - "type-customer": "Customer", - "type-customers": "Customers", + "type-customer": "고객", + "type-customers": "고객", "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", - "type-user": "User", - "type-users": "Users", + "type-user": "사용자", + "type-users": "사용자", "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", "user-name-starts-with": "Users whose names start with '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboards", + "type-dashboard": "대시보드", + "type-dashboards": "대시보드", "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarms", + "type-alarm": "알람", + "type-alarms": "알람", "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", - "type-rulechain": "Rule chain", - "type-rulechains": "Rule chains", + "type-rulechain": "규칙 사슬", + "type-rulechains": "규칙 사슬", "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", "type-current-customer": "Current Customer", @@ -667,23 +672,23 @@ "type-error": "에러", "type-lc-event": "주기적 이벤트", "type-stats": "통계", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", + "type-debug-rule-node": "디버그", + "type-debug-rule-chain": "디버그", "no-events-prompt": "이벤트 없음", "error": "에러", "alarm": "알람", "event-time": "이벤트 발생 시간", "server": "서버", "body": "Body", - "method": "Method", - "type": "Type", - "entity": "Entity", - "message-id": "Message Id", - "message-type": "Message Type", - "data-type": "Data Type", - "relation-type": "Relation Type", - "metadata": "Metadata", - "data": "Data", + "method": "방법", + "type": "종류", + "entity": "개체", + "message-id": "메시지 ID", + "message-type": "메시지 종류", + "data-type": "데이터 종류", + "relation-type": "관계 종류", + "metadata": "메타데이터", + "data": "데이터", "event": "이벤트", "status": "상태", "success": "성공", @@ -692,11 +697,11 @@ "errors-occurred": "오류가 발생했습니다" }, "extension": { - "extensions": "Extensions", + "extensions": "확장", "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", - "type": "Type", - "key": "Key", - "value": "Value", + "type": "종류", + "key": "키", + "value": "값", "id": "Id", "extension-id": "Extension id", "extension-type": "Extension type", @@ -992,14 +997,14 @@ "invalid-additional-info": "Unable to parse additional info json." }, "rulechain": { - "rulechain": "Rule chain", - "rulechains": "Rule chains", + "rulechain": "규칙 사슬", + "rulechains": "규칙 사슬", "root": "Root", "delete": "Delete rule chain", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "add": "Add Rule Chain", + "name": "이름", + "name-required": "이름을 입력하세요.", + "description": "설명", + "add": "규칙 사슬 추가", "set-root": "Make rule chain root", "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", @@ -1008,14 +1013,14 @@ "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?", "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }", "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", - "add-rulechain-text": "Add new rule chain", - "no-rulechains-text": "No rule chains found", - "rulechain-details": "Rule chain details", - "details": "Details", - "events": "Events", - "system": "System", - "import": "Import rule chain", - "export": "Export rule chain", + "add-rulechain-text": "새로운 규칙 사슬 추가", + "no-rulechains-text": "아무 규칙 사슬도 없습니다.", + "rulechain-details": "규칙 사슬 상세 정보", + "details": "자세히", + "events": "이벤트", + "system": "시스템", + "import": "규칙 사슬 불러오기", + "export": "규칙 사슬 내보내기", "export-failed-error": "Unable to export rule chain: {{error}}", "create-new-rulechain": "Create new rule chain", "rulechain-file": "Rule chain file", @@ -1029,70 +1034,70 @@ "debug-mode": "Debug mode" }, "rulenode": { - "details": "Details", - "events": "Events", - "search": "Search nodes", - "open-node-library": "Open node library", - "add": "Add rule node", - "name": "Name", - "name-required": "Name is required.", - "type": "Type", - "description": "Description", - "delete": "Delete rule node", - "select-all-objects": "Select all nodes and connections", - "deselect-all-objects": "Deselect all nodes and connections", - "delete-selected-objects": "Delete selected nodes and connections", - "delete-selected": "Delete selected", - "select-all": "Select all", - "copy-selected": "Copy selected", - "deselect-all": "Deselect all", - "rulenode-details": "Rule node details", - "debug-mode": "Debug mode", - "configuration": "Configuration", - "link": "Link", - "link-details": "Rule node link details", - "add-link": "Add link", - "link-label": "Link label", - "link-label-required": "Link label is required.", - "custom-link-label": "Custom link label", - "custom-link-label-required": "Custom link label is required.", - "type-filter": "Filter", + "details": "자세히", + "events": "이벤트", + "search": "노드 검색", + "open-node-library": "노드 라이브러리 열기", + "add": "규칙 노드 추가", + "name": "이름", + "name-required": "이름을 입력하세요.", + "type": "종류", + "description": "설명", + "delete": "규칙 노드 삭제", + "select-all-objects": "모든 노드와 연결을 선택", + "deselect-all-objects": "모든 노드와 연결을 선택 해제", + "delete-selected-objects": "선택된 노드와 연결을 삭제", + "delete-selected": "선택 삭제", + "select-all": "모두 선택", + "copy-selected": "선택 복사", + "deselect-all": "선택 해제", + "rulenode-details": "규칙 노드 상세 정보", + "debug-mode": "디버그 모드", + "configuration": "설정", + "link": "링크", + "link-details": "규칙 노드 링크 상세 정보", + "add-link": "링크 추가", + "link-label": "링크 라벨", + "link-label-required": "링크 라벨을 입력하세요.", + "custom-link-label": "링크 라벨 사용자 정의", + "custom-link-label-required": "링크 라벨 사용자 정의를 입력하세요.", + "type-filter": "필터", "type-filter-details": "Filter incoming messages with configured conditions", "type-enrichment": "Enrichment", "type-enrichment-details": "Add additional information into Message Metadata", "type-transformation": "Transformation", "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Action", + "type-action": "", "type-action-details": "Perform special action", - "type-external": "External", + "type-external": "외부", "type-external-details": "Interacts with external system", "type-rule-chain": "Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-input": "Input", + "type-input": "입력", "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", "ui-resources-load-error": "Failed to load configuration ui resources.", "invalid-target-rulechain": "Unable to resolve target rule chain!", "test-script-function": "Test script function", - "message": "Message", - "message-type": "Message type", - "message-type-required": "Message type is required", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Help" + "message": "메시지", + "message-type": "메시지 종류", + "message-type-required": "메시지 종류를 입력하세요.", + "metadata": "메타데이터", + "metadata-required": "메타데이터 엔트리를 입력하세요.", + "output": "출력", + "test": "테스트", + "help": "도움말" }, "tenant": { "tenants": "테넌트", "management": "테넌트 관리", "add": "테넌트 추가", - "admins": "Admins", + "admins": "관리자", "manage-tenant-admins": "테넌트 관리자 관리", "delete": "테넌트 삭제", "add-tenant-text": "테넌트 추가", "no-tenants-text": "테넌트가 없습니다.", - "tenant-details": "테넌트 상세정보", + "tenant-details": "테넌트 상세 정보", "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", "delete-tenant-text": "테넌트와 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", "delete-tenants-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} }를 삭제하시겠습니까?", @@ -1101,23 +1106,23 @@ "title": "타이틀", "title-required": "타이틀을 입력하세요.", "description": "설명", - "details": "Details", - "events": "Events", - "copyId": "Copy tenant Id", - "idCopiedMessage": "Tenant Id has been copied to clipboard", - "select-tenant": "Select tenant", + "details": "자세히", + "events": "이벤트", + "copyId": "테넌트 ID 복사", + "idCopiedMessage": "테넌트 ID를 클립보드로 복사", + "select-tenant": "테넌트 선택", "no-tenants-matching": "No tenants matching '{{entity}}' were found.", - "tenant-required": "Tenant is required" + "tenant-required": "테넌트가 필요합니다." }, "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": "Days", - "hours": "Hours", - "minutes": "Minutes", - "seconds": "Seconds", + "days": "일", + "hours": "시간", + "minutes": "분", + "seconds": "초", "advanced": "고급" }, "timewindow": { @@ -1125,13 +1130,13 @@ "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", - "realtime": "Realtime", - "history": "History", - "last-prefix": "last", - "period": "from {{ startTime }} to {{ endTime }}", + "realtime": "실시간", + "history": "기록", + "last-prefix": "과거", + "period": "{{ startTime }}부터 {{ endTime }}까지", "edit": "타임윈도우 편집", "date-range": "날짜 범위", - "last": "Last", + "last": "과거", "time-period": "기간" }, "user": { @@ -1146,7 +1151,7 @@ "delete": "사용자 삭제", "add-user-text": "새로운 사용자 추가", "no-users-text": "사용자가 없습니다.", - "user-details": "사용자 상세정보", + "user-details": "사용자 상세 정보", "delete-user-title": "'{{userEmail}}' 사용자를 삭제하시겠습니까?", "delete-user-text": "사용자와 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-users-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} }을 삭제하시겠니까?", @@ -1242,10 +1247,10 @@ "update-dashboard-state": "Update current dashboard state", "open-dashboard": "Navigate to other dashboard", "custom": "Custom action", - "target-dashboard-state": "Target dashboard state", - "target-dashboard-state-required": "Target dashboard state is required", - "set-entity-from-widget": "Set entity from widget", - "target-dashboard": "Target dashboard", + "target-dashboard-state": "대상 대시보드 상태", + "target-dashboard-state-required": "대상 대시보드 상태가 필요합니다.", + "set-entity-from-widget": "위젯으로 부터 객체 설정", + "target-dashboard": "대상 대시보드", "open-right-layout": "Open right dashboard layout (mobile view)" }, "widgets-bundle": { @@ -1253,13 +1258,13 @@ "widgets-bundles": "위젯 번들", "add": "위젯 번들 추가", "delete": "위젯 번들 삭제", - "title": "타이틀", - "title-required": "타이틀을 입력하세요.", + "title": "제목", + "title-required": "제목을 입력하세요.", "add-widgets-bundle-text": "위젯 번들 추가", "no-widgets-bundles-text": "위젯 번들이 없습니다.", "empty": "위젯 번들이 비어있습니다.", "details": "상세", - "widgets-bundle-details": "위젯 번들 상세정보", + "widgets-bundle-details": "위젯 번들 상세 정보", "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' 위젯 번들을 삭제하시겠습니까?", "delete-widgets-bundle-text": "위젯 번들과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-widgets-bundles-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} }를 삭제하시겠습니까?", @@ -1279,15 +1284,15 @@ "data": "데이터", "settings": "설정", "advanced": "고급", - "title": "타이틀", + "title": "제목", "general-settings": "일반 설정", - "display-title": "타이틀 표시", + "display-title": "제목 표시", "drop-shadow": "그림자", "enable-fullscreen": "전체화면 사용 ", "background-color": "배경 색", "text-color": "글자 색", "padding": "패딩", - "title-style": "타이틀 스타일", + "title-style": "제목 스타일", "mobile-mode-settings": "모바일 모드 설정", "order": "순서", "height": "높이", @@ -1333,18 +1338,18 @@ "Oct": "10월", "Nov": "11월", "Dec": "12월", - "January": "일월", - "February": "이월", - "March": "행진", - "April": "4 월", - "June": "유월", - "July": "칠월", - "August": "팔월", - "September": "구월", - "October": "십월", - "November": "십일월", - "December": "12 월", - "Custom Date Range": "맞춤 기간", + "January": "1월", + "February": "2월", + "March": "3월", + "April": "4월", + "June": "6월", + "July": "7월", + "August": "8월", + "September": "9월", + "October": "10월", + "November": "11월", + "December": "12월", + "Custom Date Range": "임의 기간 범위", "Date Range Template": "기간 템플릿", "Today": "오늘", "Yesterday": "어제", @@ -1359,22 +1364,22 @@ "Hour": "시간", "Day": "일", "Week": "주", - "2 weeks": "이주", + "2 weeks": "2 주", "Month": "달", "3 months": "3 개월", "6 months": "6 개월", "Custom interval": "사용자 지정 간격", "Interval": "간격", "Step size": "단계 크기", - "Ok": "Ok" + "Ok": "확인" } } }, "icon": { - "icon": "Icon", - "select-icon": "Select icon", + "icon": "아이콘", + "select-icon": "선택된 아이콘", "material-icons": "Material icons", - "show-all": "Show all icons" + "show-all": "모든 아이콘 보기" }, "custom": { "widget-action": { diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 1721196b2b..c6c3b3af3a 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -331,7 +331,6 @@ pre.tb-highlight { font-weight: 400; line-height: 18px; color: rgba(0, 0, 0, .38); - text-transform: uppercase; } .tb-fullscreen { @@ -482,10 +481,6 @@ mat-label { pointer-events: all; } - button:not(.mat-menu-item):not(.mat-sort-header-button) { - text-transform: uppercase; - } - button.mat-menu-item { font-size: 15px; } @@ -563,7 +558,7 @@ mat-label { } } - mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell { + mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell, .mat-expansion-panel-header { button.mat-icon-button { mat-icon { color: rgba(0, 0, 0, .54); @@ -965,7 +960,6 @@ mat-label { position: relative; display: flex; height: calc(100% - 60px); - text-transform: uppercase; text-align: center; } @@ -974,10 +968,6 @@ mat-label { margin-top: -50px; } - .mat-tab-label { - text-transform: uppercase; - } - .tb-primary-background { background-color: $primary; } diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json index 2bd6ffd811..bcbbe5480d 100644 --- a/ui-ngx/src/tsconfig.app.json +++ b/ui-ngx/src/tsconfig.app.json @@ -1,9 +1,9 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", - "react", "react-dom", "jstree", "raphael", "canvas-gauges", "leaflet", "leaflet-markercluster"] + "react", "react-dom", "jstree", "raphael", "canvas-gauges", "leaflet", "leaflet.markercluster"] }, "angularCompilerOptions": { "fullTemplateTypeCheck": true diff --git a/ui-ngx/src/tsconfig.spec.json b/ui-ngx/src/tsconfig.spec.json index 0c06d472bf..de7733630e 100644 --- a/ui-ngx/src/tsconfig.spec.json +++ b/ui-ngx/src/tsconfig.spec.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/spec", "types": [ diff --git a/ui-ngx/tsconfig.base.json b/ui-ngx/tsconfig.base.json deleted file mode 100644 index b814f9ff19..0000000000 --- a/ui-ngx/tsconfig.base.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "sourceMap": true, - "declaration": false, - "module": "es2020", - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "importHelpers": true, - "target": "es5", - "jsx": "react", - "typeRoots": [ - "node_modules/@types", - "src/typings/rawloader.typings.d.ts", - "src/typings/jquery.typings.d.ts", - "src/typings/jquery.flot.typings.d.ts", - "src/typings/jquery.jstree.typings.d.ts", - "src/typings/split.js.typings.d.ts", - "src/typings/add-marker.d.ts", - "src/typings/leaflet-editable.d.ts" - ], - "paths": { - "@app/*": ["src/app/*"], - "@env/*": [ - "src/environments/*" - ], - "@core/*": ["src/app/core/*"], - "@modules/*": ["src/app/modules/*"], - "@shared/*": ["src/app/shared/*"], - "@home/*": ["src/app/modules/home/*"], - "jszip": [ - "node_modules/jszip/dist/jszip.min.js" - ] - }, - "lib": [ - "es2018", - "es2019", - "dom" - ] - } -} diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index e8d90a7a1b..b814f9ff19 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -1,20 +1,45 @@ -/* - This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. - It is not intended to be used to perform a compilation. - - To learn more about this file see: https://angular.io/config/solution-tsconfig. -*/ { - "files": [], - "references": [ - { - "path": "./src/tsconfig.app.json" + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "module": "es2020", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "importHelpers": true, + "target": "es5", + "jsx": "react", + "typeRoots": [ + "node_modules/@types", + "src/typings/rawloader.typings.d.ts", + "src/typings/jquery.typings.d.ts", + "src/typings/jquery.flot.typings.d.ts", + "src/typings/jquery.jstree.typings.d.ts", + "src/typings/split.js.typings.d.ts", + "src/typings/add-marker.d.ts", + "src/typings/leaflet-editable.d.ts" + ], + "paths": { + "@app/*": ["src/app/*"], + "@env/*": [ + "src/environments/*" + ], + "@core/*": ["src/app/core/*"], + "@modules/*": ["src/app/modules/*"], + "@shared/*": ["src/app/shared/*"], + "@home/*": ["src/app/modules/home/*"], + "jszip": [ + "node_modules/jszip/dist/jszip.min.js" + ] }, - { - "path": "./src/tsconfig.spec.json" - }, - { - "path": "./e2e/tsconfig.e2e.json" - } - ] -} \ No newline at end of file + "lib": [ + "es2018", + "es2019", + "dom" + ] + } +} diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 4aaffa98dd..58d275db1b 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -2,189 +2,190 @@ # yarn lockfile v1 -"@angular-builders/custom-webpack@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@angular-builders/custom-webpack/-/custom-webpack-10.0.0.tgz#9dac43cf627692de1d2c44967252e4d9567fc297" - integrity sha512-5uUUN+Mbg+RUTs4XCADuNC1k/tmRcWWrDbZFkn8QfqopWPHA2qDZIChA3oLeU0yH3t0Q4ugemcb51XAU0/eO7Q== +"@angular-builders/custom-webpack@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@angular-builders/custom-webpack/-/custom-webpack-10.0.1.tgz#9126c260ecfeb88c3ba6865e51b486bbe301e504" + integrity sha512-YDy5zEKVwXdoXLjmbsY6kGaEbmunQxaPipxrwLUc9hIjRLU2WcrX9vopf1R9Pgj4POad73IPBNGu+ibqNRFIEQ== dependencies: "@angular-devkit/architect" ">=0.1000.0 < 0.1100.0" "@angular-devkit/build-angular" ">=0.1000.0 < 0.1100.0" "@angular-devkit/core" "^10.0.0" lodash "^4.17.15" - ts-node "^8.10.2" + ts-node "^9.0.0" webpack-merge "^4.2.2" -"@angular-devkit/architect@0.1000.6", "@angular-devkit/architect@>=0.1000.0 < 0.1100.0": - version "0.1000.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1000.6.tgz#d8143abbf1a1cef8e0ea9c80690821e8ca4cd54c" - integrity sha512-IZ8yiiW+LQ5mI3VbNHzisTIn0j6D1inQZgcZtc5W2A7fFNvBlIh6vGU3mB6Qvg678Gt6tlvnNT6/R9A9Ct7VnA== - dependencies: - "@angular-devkit/core" "10.0.6" - rxjs "6.5.5" - -"@angular-devkit/build-angular@>=0.1000.0 < 0.1100.0", "@angular-devkit/build-angular@^0.1000.5": - version "0.1000.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1000.6.tgz#7c4a8a4792f7252fe8d0bd57b46e86f44c93d4b3" - integrity sha512-tKyVD8Wqfo2wFdfWmc7OMzFn30Zl37heEusnMrQD5/zZ3Hw4Nqt2kf3pf3hbWl1GExUVFYyRNoCOCh9DaIfh0w== - dependencies: - "@angular-devkit/architect" "0.1000.6" - "@angular-devkit/build-optimizer" "0.1000.6" - "@angular-devkit/build-webpack" "0.1000.6" - "@angular-devkit/core" "10.0.6" - "@babel/core" "7.9.6" - "@babel/generator" "7.9.6" - "@babel/plugin-transform-runtime" "7.9.6" - "@babel/preset-env" "7.9.6" - "@babel/runtime" "7.9.6" - "@babel/template" "7.8.6" - "@jsdevtools/coverage-istanbul-loader" "3.0.3" - "@ngtools/webpack" "10.0.6" - ajv "6.12.3" - autoprefixer "9.8.0" +"@angular-devkit/architect@0.1001.5", "@angular-devkit/architect@>=0.1000.0 < 0.1100.0": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1001.5.tgz#27bdac3c1ee1d3f179e57ce7cfcc1daa4bacdcee" + integrity sha512-W8ZqtbxwDtHnzPoqVyeyDEq24i+H0/i0fjIBuJ+XAMtd3U9JtPALIRLdhnunLXO7OLxjtxjzh0qLxKgiXGEd3g== + dependencies: + "@angular-devkit/core" "10.1.5" + rxjs "6.6.2" + +"@angular-devkit/build-angular@>=0.1000.0 < 0.1100.0", "@angular-devkit/build-angular@^0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1001.5.tgz#3b03138c41441c26f18f7fe0af03712a9ef8cb8b" + integrity sha512-+KPlwHN2glkXg/H/dlMDWPfY+Io4QqEv4cBRgJjDsW42Di49woNUot8VpGrgnDhVeLaIDmLpD6GUj2DNRgTgcg== + dependencies: + "@angular-devkit/architect" "0.1001.5" + "@angular-devkit/build-optimizer" "0.1001.5" + "@angular-devkit/build-webpack" "0.1001.5" + "@angular-devkit/core" "10.1.5" + "@babel/core" "7.11.1" + "@babel/generator" "7.11.0" + "@babel/plugin-transform-runtime" "7.11.0" + "@babel/preset-env" "7.11.0" + "@babel/runtime" "7.11.2" + "@babel/template" "7.10.4" + "@jsdevtools/coverage-istanbul-loader" "3.0.5" + "@ngtools/webpack" "10.1.5" + autoprefixer "9.8.6" babel-loader "8.1.0" browserslist "^4.9.1" - cacache "15.0.3" + cacache "15.0.5" caniuse-lite "^1.0.30001032" circular-dependency-plugin "5.2.0" copy-webpack-plugin "6.0.3" core-js "3.6.4" - css-loader "3.5.3" + css-loader "4.2.2" cssnano "4.1.10" file-loader "6.0.0" find-cache-dir "3.3.1" glob "7.1.6" - jest-worker "26.0.0" + jest-worker "26.3.0" karma-source-map-support "1.4.0" - less-loader "6.1.0" - license-webpack-plugin "2.2.0" + less-loader "6.2.0" + license-webpack-plugin "2.3.0" loader-utils "2.0.0" - mini-css-extract-plugin "0.9.0" + mini-css-extract-plugin "0.10.0" minimatch "3.0.4" - open "7.0.4" - parse5 "4.0.0" + open "7.2.0" + parse5 "6.0.1" + parse5-htmlparser2-tree-adapter "6.0.1" pnp-webpack-plugin "1.6.4" - postcss "7.0.31" + postcss "7.0.32" postcss-import "12.0.1" postcss-loader "3.0.0" raw-loader "4.0.1" - regenerator-runtime "0.13.5" + regenerator-runtime "0.13.7" resolve-url-loader "3.1.1" rimraf "3.0.2" - rollup "2.10.9" - rxjs "6.5.5" - sass "1.26.5" - sass-loader "8.0.2" + rollup "2.26.5" + rxjs "6.6.2" + sass "1.26.10" + sass-loader "10.0.1" semver "7.3.2" source-map "0.7.3" - source-map-loader "1.0.0" + source-map-loader "1.0.2" source-map-support "0.5.19" speed-measure-webpack-plugin "1.3.3" style-loader "1.2.1" - stylus "0.54.7" + stylus "0.54.8" stylus-loader "3.0.2" - terser "4.7.0" - terser-webpack-plugin "3.0.1" + terser "5.3.0" + terser-webpack-plugin "4.1.0" tree-kill "1.2.2" - webpack "4.43.0" + webpack "4.44.1" webpack-dev-middleware "3.7.2" webpack-dev-server "3.11.0" webpack-merge "4.2.2" webpack-sources "1.4.3" webpack-subresource-integrity "1.4.1" - worker-plugin "4.0.3" + worker-plugin "5.0.0" -"@angular-devkit/build-optimizer@0.1000.6": - version "0.1000.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1000.6.tgz#f5b208be155b0ffb37d7380fc1a0e12a3765319d" - integrity sha512-R8zDEAvd9PeUKvOKh6I7xp3w+MViCwjGKoOZcznjH/i/9PQjOHCMwU5S48RQloQjMGu96eDMUGOVnd9qkzXUEw== +"@angular-devkit/build-optimizer@0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1001.5.tgz#99532fcaa953a251ab519961f9517b390f10bf9f" + integrity sha512-N5zXJMs9JwFtbuDyEnNk1UX6clC/RFiTaHb/ofaTYbq39xEKGbZRVCFP8bGM4JEI5trF05m7JTD3wo3nHtZLqw== dependencies: loader-utils "2.0.0" source-map "0.7.3" - tslib "2.0.0" + tslib "2.0.1" + typescript "4.0.2" webpack-sources "1.4.3" -"@angular-devkit/build-webpack@0.1000.6": - version "0.1000.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1000.6.tgz#1ec6942b4079b6cb6704b5d39af7df14102d562e" - integrity sha512-R01bJWuvckU5IdjcqoCeikLBpHRqt5fgfD0a4Hsg3evqW6xxXcSgc+YhWfeEmyU/nF/kVel8G2bFyPzhZP4QdQ== +"@angular-devkit/build-webpack@0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1001.5.tgz#bb7d76b7b5a0565a1694ee6a7fc89ef51acf7c33" + integrity sha512-Z6U0jz6FOIC3Faiq642smQEjVZ8ZaLpxNd/QCnFWLkNAhySP8TVcRWvF8AzHJNXBLDcjm3uDy+3OX+lYALJibg== dependencies: - "@angular-devkit/architect" "0.1000.6" - "@angular-devkit/core" "10.0.6" - rxjs "6.5.5" + "@angular-devkit/architect" "0.1001.5" + "@angular-devkit/core" "10.1.5" + rxjs "6.6.2" -"@angular-devkit/core@10.0.6", "@angular-devkit/core@^10.0.0": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-10.0.6.tgz#cbb40a34f976f9496270efc4fbdb3ad836b9723e" - integrity sha512-mVvqSEoeErZ7bAModk95EAa6R9Nl23rvX+/TXuKVTK2dziMFBOrwHjb1DYhnZxFIH4xfUftCx+BWHjXBXCPYlA== +"@angular-devkit/core@10.1.5", "@angular-devkit/core@^10.0.0": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-10.1.5.tgz#3eb4321cd929a4a92a887f6a4810bdc1c7eb593e" + integrity sha512-Ly97h90Z6ZLhSnTkk2baUDNLeOrKgj/bUPkcBEKWranx6IRx8FMzin/+ysIQasBlEXWPIc8QbBmCz7xXkO4p7g== dependencies: - ajv "6.12.3" + ajv "6.12.4" fast-json-stable-stringify "2.1.0" magic-string "0.25.7" - rxjs "6.5.5" + rxjs "6.6.2" source-map "0.7.3" -"@angular-devkit/schematics@10.0.6": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-10.0.6.tgz#dc3486448cc34544f7076f7fe0a67b75137ae840" - integrity sha512-V3T4cf+jVKiPYyBrSVHf3ZSnk4wIc1WEaaeFta56HccEGQCQpvAFKqDurmtMHer50Hhaxhn7IC3Oi5kPnvkNyQ== +"@angular-devkit/schematics@10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-10.1.5.tgz#a0f13f084c0d84e7718d8bdd7a367775660afb5b" + integrity sha512-5bhQX/PC548wIPcgCx9Q0Oewe8/i8+0eZvD9qLVWzJvUEKqgbjgoA7r7KJIJx2WINbESJGTIjwbXSZ6JmAJNhA== dependencies: - "@angular-devkit/core" "10.0.6" - ora "4.0.4" - rxjs "6.5.5" + "@angular-devkit/core" "10.1.5" + ora "5.0.0" + rxjs "6.6.2" -"@angular/animations@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-10.0.9.tgz#ba6c064fd12da139caf35a26a13b8cef5f1d5958" - integrity sha512-NcQOlLQNR50qIvEjR/yo28P6Nh0hf9GRuy+4iCxHiRNZyBL8AwX/y+8/NpgSRoL/U+Iq4bXz5DSOkmrtZfFHXg== +"@angular/animations@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-10.1.5.tgz#b89540ba81fc09fdb1b0ed8ec13773232bdc14d3" + integrity sha512-RbUIluxgE5pSWWdODlcEAQuRqc/D1A2v275zBsMFjwJg3/cZl/z+RWcFJedHpJHEtbz7Aay1UWHu9jhXfA8elg== dependencies: tslib "^2.0.0" -"@angular/cdk@^10.1.3": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-10.1.3.tgz#4a8d7dcdbe344b4d7e8e0ed5c25c19b78313b494" - integrity sha512-xMV1M41mfuaQod4rtAG/duYiWffGIC2C87E1YuyHTh8SEcHopGVRQd2C8PWH+iwinPbes7AjU1uzCEvmOYikrA== +"@angular/cdk@^10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-10.2.4.tgz#656095648af005e7fa02c4cc68865be4bf59fc10" + integrity sha512-Ccm/iRb6zELWwMem6qTnFCalMVX/aS17hhN65efpNKrH3ovhyQSPWtF4p9IaEJ3rZpfXqXMPBneJ9ZXAA/iKog== dependencies: tslib "^2.0.0" optionalDependencies: parse5 "^5.0.0" -"@angular/cli@^10.0.5": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-10.0.6.tgz#366114e36a155611b553be535bec166bd28b4119" - integrity sha512-gQbQA/CsCyMf9RKEv1hJBCdBebV2BHeT4lGi56Eii0IkvZD5WIH0dNfQzR+6ErqGDgE1EI+9YCuX3psMEvCRUA== +"@angular/cli@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-10.1.5.tgz#1e2eee9cdb54889e40144c64f80a0122d3fab594" + integrity sha512-HlJVDxuTfrmxp8CvABV1pn7Ffeo0q0PuAR7gNCDcVi2vN7EDmBRRnyxBvATO4KzE5DHiSIqF0xLIsokSS7JC6w== dependencies: - "@angular-devkit/architect" "0.1000.6" - "@angular-devkit/core" "10.0.6" - "@angular-devkit/schematics" "10.0.6" - "@schematics/angular" "10.0.6" - "@schematics/update" "0.1000.6" + "@angular-devkit/architect" "0.1001.5" + "@angular-devkit/core" "10.1.5" + "@angular-devkit/schematics" "10.1.5" + "@schematics/angular" "10.1.5" + "@schematics/update" "0.1001.5" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.1" debug "4.1.1" ini "1.3.5" - inquirer "7.1.0" + inquirer "7.3.3" npm-package-arg "8.0.1" npm-pick-manifest "6.1.0" - open "7.0.4" + open "7.2.0" pacote "9.5.12" read-package-tree "5.3.1" rimraf "3.0.2" semver "7.3.2" symbol-observable "1.2.0" - universal-analytics "0.4.20" - uuid "8.1.0" + universal-analytics "0.4.23" + uuid "8.3.0" -"@angular/common@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-10.0.9.tgz#47a7eaad30c5f7fd49f2ef927a6fbfd0bd0dcf9c" - integrity sha512-zg1xtTR8LwLXfTcXX0rL30ur1GKaf3Iu8FCQpfh0804Ezn5TeovuGg2zvf/O3igwmYzrym+a0JRMe4TZsAuJxQ== +"@angular/common@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-10.1.5.tgz#8a2cca7ea70091f4d5db8736292ec60ff143137a" + integrity sha512-xo10mSQYuf6x1XrnTfwt3Rs7JtSMkSyrJtAS/vNQKdBP/8zmn6pP9zRpp7vhQ5qF+W3HN8rPLb+YI2F6uaGjBg== dependencies: tslib "^2.0.0" -"@angular/compiler-cli@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-10.0.9.tgz#6533b7f1eba9468bfdf6765c0fd0265aa0ca5085" - integrity sha512-E4WxTYO4zY7ytMna5Y8Yoh58KPJKjDn3tzefLNrBEX3t69XueYmh5JTRHmNOiWcFX2QIbgqzX9mXZ9uPLDslmg== +"@angular/compiler-cli@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-10.1.5.tgz#65e586d8650ed6ac70034472be043127d040ad7a" + integrity sha512-AJ4eOHUxgDdfq/EagUlhJ6HaNlHajtmPkhXp2HmNMNN1nPN55VZSvN43Co2gdAHiFENqsTNlnQH630aXaDyVbQ== dependencies: canonical-path "1.0.0" chokidar "^3.0.0" @@ -205,10 +206,10 @@ resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== -"@angular/compiler@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-10.0.9.tgz#968dd654927209579b77bbe9421e73b6d9002dd3" - integrity sha512-KUIRkOoZ0C7IlJ514ahyA1VDmlLrh9wBF/hm6e7fKMUAzPTwch6WQczjw/Bdt8ArL3ovYasbFungBvJ7apENtw== +"@angular/compiler@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-10.1.5.tgz#0721759b2589faf172be7a6ddde4544dca2679f1" + integrity sha512-3LyFkEzs6P6YYKkE/6E4PasMd58EBddOt9kR9kPmj9Atv/BLY3nc5RSWkOe4rK4GnBVP+ByzQiT9Fn5CiQnG/g== dependencies: tslib "^2.0.0" @@ -217,10 +218,10 @@ resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== -"@angular/core@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-10.0.9.tgz#4e03ae6f1eaf6191783176c4d24af183a20f572d" - integrity sha512-sRod8RnARYmMy/uBmZyLxt5F3vYIvAkTEv48DFvCKJ6FN7zJPS7C7/dnnDv0JG7hNsX3jVXCsIRWJiCk0ycDGg== +"@angular/core@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-10.1.5.tgz#2a855edc013237db93d18620ad3d4d74ef4a11b4" + integrity sha512-B8j1B5vkBmzyan78kMJhw7dfhe7znmujbeDU7qRgRcIllc9pVJv7D133Yze6JFiLVg21PfyFYs8FBJNeq39hxQ== dependencies: tslib "^2.0.0" @@ -231,43 +232,43 @@ dependencies: tslib "^2.0.0" -"@angular/forms@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-10.0.9.tgz#1472c18995fffb80027c5399b2c587348914fce1" - integrity sha512-t/ZjPFHz5LLj5osvNTkBibcF5Y1XrOwmgX8e6FPl2OTM/NcEyqUPWQmcGhy1Wc7jxP6JVWSWLrqI5QKkATHSBA== +"@angular/forms@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-10.1.5.tgz#2cde5e119c6f1fe9d7afceb034b6a62e231223d9" + integrity sha512-fkXKCwXL0XeFMUkmzJpm+FHYrv1CCfFGxYEBQ/bzfd3Op+dFJqEPiOwK3wG943Y09THday6H509RwwEIyF/4yw== dependencies: tslib "^2.0.0" -"@angular/language-service@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-10.0.9.tgz#9360635be990ad854c4e37ed8f4aad827cde9c1e" - integrity sha512-8MvLqIjaYqrkLOa1zhq6ocp0SYnVb8AVQ5maGB9aTmW+SU4YtZFdBd0mdRWr2XKNAWTeQN98V0GsjvYe5W34DQ== +"@angular/language-service@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-10.1.5.tgz#bf5658075f7114364e2f266f2d0cc61d93d955a1" + integrity sha512-D3y97MciUx8txpwkRnMPOhPI1fyPJCGL0JwNOO0jq1qNKMzwRRetaacKUkv1apCZWU7r2PuL2GlJM6tIX5Ml3Q== -"@angular/material@^10.1.3": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-10.1.3.tgz#016fafae127a6c3ac9594790074418bad3f69d4c" - integrity sha512-6ygbCVcejFydmZUlOcNreiWQTvL4kOrEp/M51DV70hqffTnxajCzaRe2MQhxisENB/bR8mtMvf8YY3Rsys/HCw== +"@angular/material@^10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-10.2.4.tgz#9a3e4958ed72ae77c1638075efe5bff5158b2ba4" + integrity sha512-m5pRzCZQlpb7BZrc+LV+eeMU9M76obWVbNy8or6gvBPa6awfbwOz8uBryIvVngdkhoIieqAu1iV4bG7b7Xp3sg== dependencies: tslib "^2.0.0" -"@angular/platform-browser-dynamic@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-10.0.9.tgz#e30975983ca3ea3be2ce99e5462e6968c6401d1b" - integrity sha512-iRkBAErsYNG5lPglA8bZfsKQTuVBAl6IVc0TNlGNCePQM1blZ0f2BjMoqBWrbTi5xGgGu2cmJcy9M6hob2sgzw== +"@angular/platform-browser-dynamic@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-10.1.5.tgz#05e58c1a3468371a553fad0291756d4f2d7d8b8e" + integrity sha512-wxHm1UFCtB+oU+IJ6pACGmjO9H8KVzJOLYL5hp2w0k8s7k7Zg73f6BdRgWWEEYv6uYIfF77qtKwgbH0X5H9S+w== dependencies: tslib "^2.0.0" -"@angular/platform-browser@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-10.0.9.tgz#0a2964f8dc41edb915c808f80979fda02c3dd01e" - integrity sha512-y6d2CLj6ZM0NpJGH5yfNBTVyMiOwUXFfVRw52MWQ4Kt2Auk0EROG6dWAVL0XcE9OGXBI/U/4wemmQ9LQy7wLSA== +"@angular/platform-browser@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-10.1.5.tgz#b166b6f520e34012c91e2586022d00c5e2be8f49" + integrity sha512-qMAoPHt6dgXMtieI4zx/s5yX7FFRRUDp1R4GMBCZHPN3p66WdEVxBJo4p5RWhZJioXpUwKz8Xvc+Rrh7r0KDBA== dependencies: tslib "^2.0.0" -"@angular/router@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-10.0.9.tgz#63ad68848d5653c7b87d821c5243f88d539b5c9f" - integrity sha512-B3xnO5JFFw/cP8vHxujgnznMo4P/9W1G1pvXEOmKje4CRxf+3089ikgyHxbm45OYoN4H/c6QtBUZ9KN9u3rYKg== +"@angular/router@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-10.1.5.tgz#8cadac2d200c237522db6d99b60846d08c789304" + integrity sha512-tY88ZzoBrc9K67wi5V1NLnurd3r9bYR2csZ6/zJeOE+Vdxz9ChSaglgh9T0vQdbVEAjVGPP5QtYaFO2Xv4qOIg== dependencies: tslib "^2.0.0" @@ -278,14 +279,14 @@ dependencies: tslib "^2.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.9.6": +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== @@ -294,29 +295,7 @@ invariant "^2.2.4" semver "^5.5.0" -"@babel/core@7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.6.tgz#d9aa1f580abf3b2286ef40b6904d390904c63376" - integrity sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.6" - "@babel/parser" "^7.9.6" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.7.5": +"@babel/core@7.11.1": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== @@ -338,17 +317,29 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43" - integrity sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ== +"@babel/core@^7.7.5": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" + integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== dependencies: - "@babel/types" "^7.9.6" - jsesc "^2.5.1" - lodash "^4.17.13" + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.6" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.5" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.5" + "@babel/types" "^7.11.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.11.0", "@babel/generator@^7.9.6": +"@babel/generator@7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== @@ -357,6 +348,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.11.0", "@babel/generator@^7.11.5", "@babel/generator@^7.11.6": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" + integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== + dependencies: + "@babel/types" "^7.11.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -372,7 +372,7 @@ "@babel/helper-explode-assignable-expression" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.9.6": +"@babel/helper-compilation-targets@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== @@ -383,6 +383,18 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/helper-create-class-features-plugin@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -402,11 +414,10 @@ lodash "^4.17.19" "@babel/helper-explode-assignable-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" - integrity sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A== + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b" + integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ== dependencies: - "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" "@babel/helper-function-name@^7.10.4": @@ -432,21 +443,21 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4": +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== dependencies: "@babel/types" "^7.11.0" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3": +"@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0", "@babel/helper-module-transforms@^7.9.0": +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== @@ -479,14 +490,13 @@ lodash "^4.17.19" "@babel/helper-remap-async-to-generator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" - integrity sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg== + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d" + integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-wrap-function" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" "@babel/helper-replace-supers@^7.10.4": @@ -536,7 +546,7 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.10.4", "@babel/helpers@^7.9.6": +"@babel/helpers@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== @@ -554,12 +564,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.8.6", "@babel/parser@^7.9.6": - version "7.11.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" - integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== +"@babel/parser@^7.10.4", "@babel/parser@^7.11.1", "@babel/parser@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== -"@babel/plugin-proposal-async-generator-functions@^7.8.3": +"@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== @@ -568,7 +578,15 @@ "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-dynamic-import@^7.8.3": +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-dynamic-import@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== @@ -576,7 +594,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.8.3": +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" + integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== @@ -584,7 +610,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" + integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== @@ -592,7 +626,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.8.3": +"@babel/plugin-proposal-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== @@ -600,7 +634,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.9.6": +"@babel/plugin-proposal-object-rest-spread@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== @@ -609,7 +643,7 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-optional-catch-binding@^7.8.3": +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== @@ -617,7 +651,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.9.0": +"@babel/plugin-proposal-optional-chaining@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== @@ -626,7 +660,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" + integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -641,6 +683,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" + integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -648,6 +697,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-json-strings@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" @@ -655,6 +711,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -662,7 +725,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.0": +"@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -690,21 +753,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.8.3": +"@babel/plugin-syntax-top-level-await@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.8.3": +"@babel/plugin-transform-arrow-functions@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.8.3": +"@babel/plugin-transform-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== @@ -713,21 +776,21 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.10.4" -"@babel/plugin-transform-block-scoped-functions@^7.8.3": +"@babel/plugin-transform-block-scoped-functions@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.8.3": +"@babel/plugin-transform-block-scoping@^7.10.4": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-classes@^7.9.5": +"@babel/plugin-transform-classes@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== @@ -741,21 +804,21 @@ "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.8.3": +"@babel/plugin-transform-computed-properties@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-destructuring@^7.9.5": +"@babel/plugin-transform-destructuring@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -763,14 +826,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-duplicate-keys@^7.8.3": +"@babel/plugin-transform-duplicate-keys@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-exponentiation-operator@^7.8.3": +"@babel/plugin-transform-exponentiation-operator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== @@ -778,14 +841,14 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-for-of@^7.9.0": +"@babel/plugin-transform-for-of@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-function-name@^7.8.3": +"@babel/plugin-transform-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== @@ -793,21 +856,21 @@ "@babel/helper-function-name" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-literals@^7.8.3": +"@babel/plugin-transform-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.8.3": +"@babel/plugin-transform-member-expression-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-modules-amd@^7.9.6": +"@babel/plugin-transform-modules-amd@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== @@ -816,7 +879,7 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.6": +"@babel/plugin-transform-modules-commonjs@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== @@ -826,7 +889,7 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.6": +"@babel/plugin-transform-modules-systemjs@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== @@ -836,7 +899,7 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.9.0": +"@babel/plugin-transform-modules-umd@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== @@ -844,21 +907,21 @@ "@babel/helper-module-transforms" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.10.4" -"@babel/plugin-transform-new-target@^7.8.3": +"@babel/plugin-transform-new-target@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-object-super@^7.8.3": +"@babel/plugin-transform-object-super@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== @@ -866,7 +929,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" -"@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.9.5": +"@babel/plugin-transform-parameters@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== @@ -874,45 +937,45 @@ "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-property-literals@^7.8.3": +"@babel/plugin-transform-property-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-regenerator@^7.8.7": +"@babel/plugin-transform-regenerator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.8.3": +"@babel/plugin-transform-reserved-words@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-runtime@7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.6.tgz#3ba804438ad0d880a17bca5eaa0cdf1edeedb2fd" - integrity sha512-qcmiECD0mYOjOIt8YHNsAP1SxPooC/rDmfmiSK9BNY72EitdSc7l44WTEklaWuFtbOEBjNhWWyph/kOImbNJ4w== +"@babel/plugin-transform-runtime@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" + integrity sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw== dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-shorthand-properties@^7.8.3": +"@babel/plugin-transform-shorthand-properties@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-spread@^7.8.3": +"@babel/plugin-transform-spread@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== @@ -920,7 +983,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" -"@babel/plugin-transform-sticky-regex@^7.8.3": +"@babel/plugin-transform-sticky-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== @@ -928,7 +991,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-regex" "^7.10.4" -"@babel/plugin-transform-template-literals@^7.8.3": +"@babel/plugin-transform-template-literals@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== @@ -936,14 +999,21 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.8.4": +"@babel/plugin-transform-typeof-symbol@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.8.3": +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" + integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-unicode-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== @@ -951,76 +1021,84 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-env@7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.6.tgz#df063b276c6455ec6fcfc6e53aacc38da9b0aea6" - integrity sha512-0gQJ9RTzO0heXOhzftog+a/WyOuqMrAIugVYxMYf83gh1CQaQDjMtsOpqOwXyDL/5JcWsrCm8l4ju8QC97O7EQ== +"@babel/preset-env@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" + integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== dependencies: - "@babel/compat-data" "^7.9.6" - "@babel/helper-compilation-targets" "^7.9.6" - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-async-generator-functions" "^7.8.3" - "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.6" - "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.9.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" + "@babel/compat-data" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - "@babel/plugin-transform-arrow-functions" "^7.8.3" - "@babel/plugin-transform-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.9.5" - "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.9.5" - "@babel/plugin-transform-dotall-regex" "^7.8.3" - "@babel/plugin-transform-duplicate-keys" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.9.0" - "@babel/plugin-transform-function-name" "^7.8.3" - "@babel/plugin-transform-literals" "^7.8.3" - "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.6" - "@babel/plugin-transform-modules-commonjs" "^7.9.6" - "@babel/plugin-transform-modules-systemjs" "^7.9.6" - "@babel/plugin-transform-modules-umd" "^7.9.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.8.3" - "@babel/plugin-transform-object-super" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.9.5" - "@babel/plugin-transform-property-literals" "^7.8.3" - "@babel/plugin-transform-regenerator" "^7.8.7" - "@babel/plugin-transform-reserved-words" "^7.8.3" - "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" - "@babel/plugin-transform-sticky-regex" "^7.8.3" - "@babel/plugin-transform-template-literals" "^7.8.3" - "@babel/plugin-transform-typeof-symbol" "^7.8.4" - "@babel/plugin-transform-unicode-regex" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.6" - browserslist "^4.11.1" + "@babel/types" "^7.11.0" + browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" levenary "^1.1.1" semver "^5.5.0" "@babel/preset-modules@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" - integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -1028,30 +1106,14 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/runtime@7.9.6": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" - integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@7.11.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" - -"@babel/template@^7.10.4", "@babel/template@^7.8.6": +"@babel/template@7.10.4", "@babel/template@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== @@ -1060,25 +1122,25 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.9.6": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" - integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" + integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" + "@babel/generator" "^7.11.5" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/parser" "^7.11.5" + "@babel/types" "^7.11.5" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.6": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" - integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" + integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== dependencies: "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.19" @@ -1089,17 +1151,17 @@ resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== -"@date-io/core@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.8.0.tgz#bbfdd5d09c6757e456478e0bcc0764ecc85b5d40" - integrity sha512-MIL74B3O08gjjm5fcDSWME5MfdsvyQBX58zlWHIzM+m2h3+M5rP6P+T3qym3FWnpc8EKK5E8kF97nLqNiOwgkQ== +"@date-io/core@^2.10.6": + version "2.10.6" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.10.6.tgz#1a6e671b590a08af8bd0784f3a93670e5d2d5bd7" + integrity sha512-MGYt4GEB/4ZMdSbj6FS7/gPBvuhHUwnn5O6t8PlkSqGF1310qxypVyK4CZg5RQgev25L3R5eLVdNTyYrJOL8Rw== "@date-io/date-fns@^2.6.1": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.8.0.tgz#33cadd0aeeea23afa95c246d35672f4783744129" - integrity sha512-7j2RtmXWbDDBROJNL/WMrC7dK9RgV8BBW1aQtO3JB3NU52ZP5DBgJ+M5xEoEWJ+50vaKxLPYq2QYkrWsqlbzxg== + version "2.10.6" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.10.6.tgz#d0afee6452d80112017f42af4912ba22d95b11b6" + integrity sha512-jUiIbs4VCmACy2Ml2xu3tqf0AUSZu4qQ3cRz8SoG4YPzeg1fqII8y/gTa7GJkXiH0bUKUWaf/G2dfJa9tUnmJA== dependencies: - "@date-io/core" "^2.8.0" + "@date-io/core" "^2.10.6" "@emotion/hash@^0.8.0": version "0.8.0" @@ -1124,26 +1186,26 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jsdevtools/coverage-istanbul-loader@3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.3.tgz#102e414b02ae2f0b3c7fd45a705601e1fd4867c5" - integrity sha512-TAdNkeGB5Fe4Og+ZkAr1Kvn9by2sfL44IAHFtxlh1BA1XJ5cLpO9iSNki5opWESv3l3vSHsZ9BNKuqFKbEbFaA== +"@jsdevtools/coverage-istanbul-loader@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" + integrity sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA== dependencies: convert-source-map "^1.7.0" - istanbul-lib-instrument "^4.0.1" - loader-utils "^1.4.0" + istanbul-lib-instrument "^4.0.3" + loader-utils "^2.0.0" merge-source-map "^1.1.0" - schema-utils "^2.6.4" + schema-utils "^2.7.0" "@juggle/resize-observer@^3.1.3": version "3.2.0" resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.2.0.tgz#5e0b448d27fe3091bae6216456512c5904d05661" integrity sha512-fsLxt0CHx2HCV9EL8lDoVkwHffsA0snUpddYjdLyXcG5E41xaamn9ZyQqOE9TUJdrRlH8/hjIf+UdOdDeKCUgg== -"@mat-datetimepicker/core@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@mat-datetimepicker/core/-/core-5.0.1.tgz#70f56661a84c5ba56ba2c00ea8c8873c48f0ae43" - integrity sha512-jZyImHMGDSkCuOVI4rC4lvTtnSroicrsaPpWX2R07gS5oI47/ATuYsoQu4/tmRG6dt4FbXD3+BrWl5Uqs0/6Hw== +"@mat-datetimepicker/core@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@mat-datetimepicker/core/-/core-5.1.0.tgz#62f3648ca316c621d12166c8db562e1da8d8bcae" + integrity sha512-behTHJFcgKOyC3fAViwVryQpQAG3Pz4X4GnTJuCA0UiA3iFvkafZghldaJF7QL3KCE5DpWDhMvqOp8RR1sDh+w== dependencies: tslib "^2.0.0" @@ -1230,35 +1292,34 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@ngrx/effects@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-10.0.0.tgz#d58151b1d7f5731ea42de7ed239bbab3d5866542" - integrity sha512-HHcQQ6mj1Cd0rQgnX5Wp3f7G8PKhh+Rk+jofsOsE6aHQPuuNhmnDwSA1U4PT4sXNv6JmFi5GjUqBz+tuw83oFQ== +"@ngrx/effects@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-10.0.1.tgz#66011516735dd59955910a9790d33fbcb8d50400" + integrity sha512-pw0hRQNlyBBRHH1NRWl3TF+RtEAS4XOSnoTHPtQ84Ib/bEribvexsdEq3k6yLWvR3tLTudb5J6SYwYawcM6omA== dependencies: tslib "^2.0.0" -"@ngrx/store-devtools@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-10.0.0.tgz#018716afd3df89bd2fa1f603966878f82e80d727" - integrity sha512-+7SSPW9H+IdGX04QYmfgqYOeFM++PLD6CxGRUkIIc+6jFovanMS6CVKw6V+WeerPwoZaRn43cMIDj9FChMQ4oA== +"@ngrx/store-devtools@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-10.0.1.tgz#6f3529e7708870e44cf407733bf06389ee657c26" + integrity sha512-kwgF1yjjVn0FER+AG83OLCYSMuX4/E3L+DN4doSoZs4BNO9FdkYIIA4ul1nXT5d6SLiFFTmlufmbgc6HCF3pjQ== dependencies: tslib "^2.0.0" -"@ngrx/store@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-10.0.0.tgz#a355f0fdf6129b78b82fb57cacdb277bde0de066" - integrity sha512-+mhTGJXjc+55KI1pWV5SSuP+JBAr35U1AbnBYJqqXuwJVXnJ8+n6gAr06qpPN+YMf+zRQDFwAIrqyFOfMqeJHg== +"@ngrx/store@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-10.0.1.tgz#74c3bb383cc507f927ba63710cc6622f2f2859db" + integrity sha512-ZbPvhp/tRYnS3jZ28mDOX2LH3jfySXT0uv8ffIboM/o9QxBGHpAJyBct2zkpy4duYBc3i/sIbRn+CEpAjLXjHw== dependencies: tslib "^2.0.0" -"@ngtools/webpack@10.0.6": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.0.6.tgz#6fe392ecad9f2869fc68eda7f1344ad0b68a8898" - integrity sha512-AbSDhPmsljkZO2jHFpge/5AHLQIrbscWgo4brrhF7NQ5TvPgE0Xn0wU7gxB9++hVUKQLGnnbAvewJyB/uYb9Nw== +"@ngtools/webpack@10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.1.5.tgz#08fe0c8cc9defb156f3b01e9f8a32994ed66fa0b" + integrity sha512-oebpaFwYk42DCYL3CTVeDUAhh6OrqkZxLJypVuRtb1iNZltwEQKRykoYCr4yQuNByzn4+i21CvlSuBwm0afXHg== dependencies: - "@angular-devkit/core" "10.0.6" - enhanced-resolve "4.1.1" - rxjs "6.5.5" + "@angular-devkit/core" "10.1.5" + enhanced-resolve "4.3.0" webpack-sources "1.4.3" "@ngx-translate/core@^13.0.0": @@ -1303,26 +1364,26 @@ dependencies: mkdirp "^1.0.4" -"@schematics/angular@10.0.6": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-10.0.6.tgz#568590574dca1556280a0a04f625f79bee34ee4a" - integrity sha512-TPBpo0GnMJLvKE6rYZDkSy9pnkMH55rSJ6nfLDpQ5zzmhoD/QnASUr8trfTFs3+MqmPlX61xI00+HmStmI8sJQ== +"@schematics/angular@10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-10.1.5.tgz#964a50c070920f9bb42e0f26cedc9c86c8f48241" + integrity sha512-3VRcMB9WpjcMvlZ1y+78WGuZ4Ehp9pGw/T+zAR1VG9/16XHDQyfObsMuaU2EnEoufiHbTe3UpvVpYOu6tOCJrA== dependencies: - "@angular-devkit/core" "10.0.6" - "@angular-devkit/schematics" "10.0.6" + "@angular-devkit/core" "10.1.5" + "@angular-devkit/schematics" "10.1.5" + jsonc-parser "2.3.0" -"@schematics/update@0.1000.6": - version "0.1000.6" - resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1000.6.tgz#f0131d4b3ed879ae8f19c032d310bece79691738" - integrity sha512-GGfPGPjRF/MA4EeJ+h1ebzoYDzChF4BV7SaTfpT107LPCD3McRjKS39Jw2qH/ArGNSbrbJ8fYNOIj3g/uh1GoA== +"@schematics/update@0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1001.5.tgz#367ea5696fea300a81469994632985f30b41b40b" + integrity sha512-DSomJ5IMs/5HUPx0RdPYubPWXh7kToxXUZbJywe0Q+TWTd+1xFfg8++O1DG4iW7E/Boqojx5VenAOzWY9jDWjA== dependencies: - "@angular-devkit/core" "10.0.6" - "@angular-devkit/schematics" "10.0.6" + "@angular-devkit/core" "10.1.5" + "@angular-devkit/schematics" "10.1.5" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" npm-package-arg "^8.0.0" pacote "9.5.12" - rxjs "6.5.5" semver "7.3.2" semver-intersect "1.4.0" @@ -1331,11 +1392,6 @@ resolved "https://registry.yarnpkg.com/@types/canvas-gauges/-/canvas-gauges-2.1.2.tgz#fb9ece324cb15ae137791ad21eb2db70e11a7210" integrity sha512-oWCq0XjsTBXPtMKXoW23ORbMWguC2Fa8o5NiZVYiUoQMMrpNLKj1E+LDznlMpcib3iyWVIy+TEpc/ea6LMbW3Q== -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/flot@^0.0.31": version "0.0.31" resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.31.tgz#0daca37c6c855b69a0a7e2e37dd0f84b3db8c8c1" @@ -1362,9 +1418,9 @@ "@types/node" "*" "@types/jasmine@*", "@types/jasmine@^3.5.12": - version "3.5.12" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.12.tgz#5c378c1545cdc7cb339cff5578f854b6d1e0a17d" - integrity sha512-vJaQ58oceFao+NzpKNqLOWwHPsqA7YEhKv+mOXvYU4/qh+BfVWIxaBtL0Ck5iCS67yOkNwGkDCrzepnzIWF+7g== + version "3.5.14" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.14.tgz#f41a14e8ffa939062a71cf9722e5ee7d4e1f94af" + integrity sha512-Fkgk536sHPqcOtd+Ow+WiUNuk0TSo/BntKkF8wSvcd6M2FvPjeXcUE6Oz/bwDZiUZEaXLslAgw00Q94Pnx6T4w== "@types/jasminewd2@^2.0.8": version "2.0.8" @@ -1373,10 +1429,10 @@ dependencies: "@types/jasmine" "*" -"@types/jquery@*", "@types/jquery@^3.3.29", "@types/jquery@^3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.1.tgz#cebb057acf5071c40e439f30e840c57a30d406c3" - integrity sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg== +"@types/jquery@*", "@types/jquery@^3.3.29", "@types/jquery@^3.5.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.2.tgz#e17c1756ecf7bbb431766c6761674a5d1de16579" + integrity sha512-+MFOdKF5Zr41t3y2wfzJvK1PrUK0KtPLAFwYownp/0nCoMIANDDu5aFSpWfb8S0ZajCSNeaBnMrBGxksXK5yeg== dependencies: "@types/sizzle" "*" @@ -1385,10 +1441,10 @@ resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.11.0.tgz#f1311fe280a5f83b1e6517cab1116aad63465cd0" integrity sha512-RqTqKEenGBSa/vS3qHQuhudWE1d1NbollRDoArx85k1vUg4rugc+odFQW13c6O5re7hjf6zaRnWz9St/j8h15w== -"@types/json-schema@^7.0.4": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== +"@types/json-schema@^7.0.5": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/jstree@^3.3.40": version "3.3.40" @@ -1397,20 +1453,6 @@ dependencies: "@types/jquery" "*" -"@types/jszip@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.4.1.tgz#e7a4059486e494c949ef750933d009684227846f" - integrity sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A== - dependencies: - jszip "*" - -"@types/leaflet-markercluster@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/leaflet-markercluster/-/leaflet-markercluster-1.0.3.tgz#64151be453f6490e8751500482deb961064e782c" - integrity sha1-ZBUb5FP2SQ6HUVAEgt65YQZOeCw= - dependencies: - "@types/leaflet" "*" - "@types/leaflet-polylinedecorator@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#1572131ffedb3154c6e18e682d2fb700e203af19" @@ -1418,6 +1460,13 @@ dependencies: "@types/leaflet" "*" +"@types/leaflet.markercluster@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.3.tgz#5824d9be3dd5c0864a22a1fca36664550a96a76c" + integrity sha512-X/b/Enz84PzmcA9z7pxsHEBEUNghmvznEBcRQeuxyYL/QU6jAR7LIb/ot03ATNPO56wSFzbCnsOf7yJ+7FzS1Q== + dependencies: + "@types/leaflet" "*" + "@types/leaflet@*", "@types/leaflet@^1.5.17": version "1.5.17" resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b" @@ -1425,25 +1474,37 @@ dependencies: "@types/geojson" "*" -"@types/lodash@^4.14.159": - version "4.14.159" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" - integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== +"@types/lodash@^4.14.161": + version "4.14.161" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" + integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/mousetrap@^1.6.0": +"@types/moment-timezone@^0.5.30": + version "0.5.30" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7" + integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg== + dependencies: + moment-timezone "*" + +"@types/mousetrap@1.6.3": version "1.6.3" resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== +"@types/mousetrap@^1.6.0": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.4.tgz#32503197fca4168b10bf251c1d677a9b5b1c2415" + integrity sha512-+Y900DGhe+f+4lRwHm9krsKfsiXcbdOhzTsLbytU4MiG8wE9xOw7CFKtgYKfqEAcUdWEGZRyuTxoyFl2Gx6Rdg== + "@types/node@*": - version "14.0.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" - integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== + version "14.11.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305" + integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ== "@types/prop-types@*": version "15.7.3" @@ -1479,10 +1540,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.46": - version "16.9.46" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e" - integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg== +"@types/react@*", "@types/react@^16.9.51": + version "16.9.51" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.51.tgz#f8aa51ffa9996f1387f63686696d9b59713d2b60" + integrity sha512-lQa12IyO+DMlnSZ3+AGHRUiUcpK47aakMMoBG8f7HGxJT8Yfe+WE128HIXaHOHVPReAW0oDS3KAI0JI2DDe1PQ== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -1699,9 +1760,9 @@ JSONStream@^1.3.4: through ">=2.2.7 <3" abab@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c" - integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== abbrev@1: version "1.1.1" @@ -1722,9 +1783,9 @@ ace-builds@^1.4.12, ace-builds@^1.4.6: integrity sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg== acorn@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== adjust-sourcemap-loader@2.0.0: version "2.0.0" @@ -1769,9 +1830,9 @@ agentkeepalive@^3.4.1: humanize-ms "^1.2.1" aggregate-error@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" - integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== dependencies: clean-stack "^2.0.0" indent-string "^4.0.0" @@ -1781,25 +1842,25 @@ ajv-errors@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@6.12.3: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== +ajv@6.12.4: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3: - version "6.12.4" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1811,10 +1872,10 @@ alphanum-sort@^1.0.0: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -angular-gridster2@^10.1.3: - version "10.1.4" - resolved "https://registry.yarnpkg.com/angular-gridster2/-/angular-gridster2-10.1.4.tgz#a5c5c3e4541b73b64cab156ad7b726e0e3d28405" - integrity sha512-jK5govAUQ4zKnVuNepsR3Qs8K9fUn5lLg4A4AKe9wsoRMEMdkSuCHh65ij4YePAxr44hky9Zy6EaCmVorBd8LA== +angular-gridster2@^10.1.6: + version "10.1.6" + resolved "https://registry.yarnpkg.com/angular-gridster2/-/angular-gridster2-10.1.6.tgz#7fb3f93e35c566be220ba6107550b581d952af78" + integrity sha512-k0aWhX2N8E3cux4goPVs4za3FiekH++NvfsxruKG/gI5jXUOYrz1qsK66oYgrJM0zy9zGcBUU0jKIZiC8pklag== dependencies: tslib "^2.0.0" @@ -1876,11 +1937,10 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" anymatch@^2.0.0: @@ -2071,21 +2131,21 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== attr-accept@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.1.tgz#89b48de019ed4342f1865626b4389c666b3ed231" - integrity sha512-GpefLMsbH5ojNgfTW+OBin2xKzuHfyeNA+qCktzZojBhbA/lPZdCFMWdwk5ajb989Ok7ZT+EADqvW3TAFNMjhA== + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== -autoprefixer@9.8.0: - version "9.8.0" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.0.tgz#68e2d2bef7ba4c3a65436f662d0a56a741e56511" - integrity sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A== +autoprefixer@9.8.6: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== dependencies: browserslist "^4.12.0" - caniuse-lite "^1.0.30001061" - chalk "^2.4.2" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.30" + postcss "^7.0.32" postcss-value-parser "^4.1.0" aws-sign2@~0.7.0: @@ -2133,6 +2193,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-arraybuffer@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" + integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= + base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -2359,15 +2424,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.12.0, browserslist@^4.8.5, browserslist@^4.9.1: - version "4.14.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.0.tgz#2908951abfe4ec98737b72f34c3bcedc8d43b000" - integrity sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ== +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5, browserslist@^4.9.1: + version "4.14.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== dependencies: - caniuse-lite "^1.0.30001111" - electron-to-chromium "^1.3.523" - escalade "^3.0.2" - node-releases "^1.1.60" + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" browserstack@^1.5.1: version "1.6.0" @@ -2425,22 +2490,22 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@15.0.3: - version "15.0.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.3.tgz#2225c2d1dd8e872339950d6a39c051e0e9334392" - integrity sha512-bc3jKYjqv7k4pWh7I/ixIjfcjPul4V4jme/WbjvwGS5LzoPL/GzXr4C5EgPNLO/QEZl9Oi61iGitYEdwcrwLCQ== +cacache@15.0.5, cacache@^15.0.4, cacache@^15.0.5: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== dependencies: + "@npmcli/move-file" "^1.0.1" chownr "^2.0.0" fs-minipass "^2.0.0" glob "^7.1.4" infer-owner "^1.0.4" - lru-cache "^5.1.1" + lru-cache "^6.0.0" minipass "^3.1.1" minipass-collect "^1.0.2" minipass-flush "^1.0.5" minipass-pipeline "^1.2.2" mkdirp "^1.0.3" - move-file "^2.0.0" p-map "^4.0.0" promise-inflight "^1.0.1" rimraf "^3.0.2" @@ -2469,29 +2534,6 @@ cacache@^12.0.0, cacache@^12.0.2: unique-filename "^1.1.1" y18n "^4.0.0" -cacache@^15.0.3, cacache@^15.0.4, cacache@^15.0.5: - version "15.0.5" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" - integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== - dependencies: - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.0" - tar "^6.0.2" - unique-filename "^1.1.1" - cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2536,11 +2578,16 @@ camelcase@5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -camelcase@5.3.1, camelcase@^5.0.0, camelcase@^5.3.1: +camelcase@5.3.1, camelcase@^5.0.0: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== + caniuse-api@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" @@ -2551,10 +2598,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001061, caniuse-lite@^1.0.30001111: - version "1.0.30001114" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001114.tgz#2e88119afb332ead5eaa330e332e951b1c4bfea9" - integrity sha512-ml/zTsfNBM+T1+mjglWRPgVsu2L76GAaADKX5f4t0pbhttEp0WMawJsHDYlFkVZkoA+89uvBRrVrEE4oqenzXQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: + version "1.0.30001146" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c" + integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug== canonical-path@1.0.0: version "1.0.0" @@ -2591,10 +2638,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -2695,15 +2742,15 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-spinners@^2.2.0: +cli-spinners@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== clipboard@^2.0.0: version "2.0.6" @@ -2732,15 +2779,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -2765,10 +2803,10 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" -codelyzer@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-6.0.0.tgz#50c98581cc2890e0e9a9f93878dc317115d836ed" - integrity sha512-edJIQCIcxD9DhVSyBEdJ38AbLikm515Wl91t5RDGNT88uA6uQdTm4phTWfn9JhzAI8kXNUcfYyAE90lJElpGtA== +codelyzer@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-6.0.1.tgz#c0e9668e847255b37c759e68fb2700b11e277d0f" + integrity sha512-cOyGQgMdhnRYtW2xrJUNrNYDjEgwQ+BrE2y93Bwz3h4DJ6vJRLfupemU5N3pbYsUlBHJf0u1j1UGk+NLW4d97g== dependencies: "@angular/compiler" "9.0.0" "@angular/core" "9.0.0" @@ -2833,6 +2871,11 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" +colorette@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + colors@1.4.0, colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -2894,15 +2937,15 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.43.0 < 2" -compression-webpack-plugin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-4.0.1.tgz#33eda97f1170dd38c5556771de10f34245aa0274" - integrity sha512-0mg6PgwTsUe5LEcUrOu3ob32vraDx2VdbMGAT1PARcOV+UJWDYZFdkSo6RbHoGQ061mmmkC7XpRKOlvwm/gzJQ== +compression-webpack-plugin@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-6.0.2.tgz#13482bfa81e0472e5d6af1165b6ee9f29f98178b" + integrity sha512-WUv7fTy2uCZKJ4iFMKJG42GDepCEocS5eqsEi8uIJZy97k/WvzxGz9dwE4+pIAkcrK4B7k+teKo71IrLu+tbqw== dependencies: cacache "^15.0.5" find-cache-dir "^3.3.1" - schema-utils "^2.7.0" - serialize-javascript "^4.0.0" + schema-utils "^2.7.1" + serialize-javascript "^5.0.1" webpack-sources "^1.4.3" compression@^1.7.4: @@ -3144,24 +3187,23 @@ css-declaration-sorter@^4.0.1: postcss "^7.0.1" timsort "^0.3.0" -css-loader@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" - integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== +css-loader@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" + integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg== dependencies: - camelcase "^5.3.1" + camelcase "^6.0.0" cssesc "^3.0.0" icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.27" + loader-utils "^2.0.0" + postcss "^7.0.32" postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" + postcss-modules-local-by-default "^3.0.3" postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.3" - schema-utils "^2.6.6" - semver "^6.3.0" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" + semver "^7.3.2" css-parse@~2.0.0: version "2.0.0" @@ -3218,9 +3260,9 @@ css-vendor@^2.0.8: is-in-browser "^1.0.2" css-what@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" - integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== + version "3.4.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e" + integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g== css@^2.0.0: version "2.2.4" @@ -3325,9 +3367,9 @@ csstype@^2.5.2: integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A== csstype@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7" - integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" + integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== custom-event@~1.0.0: version "1.0.1" @@ -3369,9 +3411,9 @@ data-urls@^2.0.0: whatwg-url "^8.0.0" date-fns@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" - integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ== + version "2.16.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" + integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== date-format@^2.1.0: version "2.1.0" @@ -3397,20 +3439,27 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: +debug@4.1.1, debug@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" -debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" +debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -3458,7 +3507,7 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-properties@^1.1.2, define-properties@^1.1.3: +define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -3657,9 +3706,9 @@ domelementtype@1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + version "2.0.2" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" + integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== domutils@^1.7.0: version "1.7.0" @@ -3670,9 +3719,9 @@ domutils@^1.7.0: domelementtype "1" dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: is-obj "^2.0.0" @@ -3709,10 +3758,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.523: - version "1.3.533" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.533.tgz#d7e5ca4d57e9bc99af87efbe13e7be5dde729b0f" - integrity sha512-YqAL+NXOzjBnpY+dcOKDlZybJDCOzgsq4koW3fvyty/ldTmsb4QazZpOWmVvZ2m0t5jbBf7L0lIGU3BUipwG+A== +electron-to-chromium@^1.3.571: + version "1.3.578" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" + integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== elliptic@^6.5.3: version "6.5.3" @@ -3767,30 +3816,30 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: once "^1.4.0" engine.io-client@~3.4.0: - version "3.4.3" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c" - integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw== + version "3.4.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967" + integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ== dependencies: component-emitter "~1.3.0" component-inherit "0.0.3" - debug "~4.1.0" + debug "~3.1.0" engine.io-parser "~2.2.0" has-cors "1.1.0" indexof "0.0.1" - parseqs "0.0.5" - parseuri "0.0.5" + parseqs "0.0.6" + parseuri "0.0.6" ws "~6.1.0" xmlhttprequest-ssl "~1.5.4" yeast "0.1.2" engine.io-parser@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" - integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + version "2.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7" + integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg== dependencies: after "0.8.2" arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.5" + base64-arraybuffer "0.1.4" blob "0.0.5" has-binary2 "~1.0.2" @@ -3806,16 +3855,7 @@ engine.io@~3.4.0: engine.io-parser "~2.2.0" ws "^7.1.2" -enhanced-resolve@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" - integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0, enhanced-resolve@^4.3.0: +enhanced-resolve@4.3.0, enhanced-resolve@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== @@ -3854,19 +3894,37 @@ error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" object-keys "^1.1.1" - object.assign "^4.1.0" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" @@ -3917,10 +3975,10 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3: d "^1.0.1" ext "^1.1.2" -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== +escalade@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== escape-html@~1.0.3: version "1.0.3" @@ -3946,17 +4004,22 @@ esprima@^4.0.0: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3973,9 +4036,9 @@ eve-raphael@0.5.0: integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA= eventemitter3@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" - integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.0.0: version "3.2.0" @@ -4193,11 +4256,11 @@ file-loader@6.0.0: schema-utils "^2.6.5" file-selector@^0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0" - integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ== + version "0.1.13" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.13.tgz#5efd977ca2bca1700992df1b10e254f4e73d2df4" + integrity sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ== dependencies: - tslib "^1.9.0" + tslib "^2.0.1" file-uri-to-path@1.0.0: version "1.0.0" @@ -4278,7 +4341,7 @@ flatted@^2.0.1, flatted@^2.0.2: "flot@git://github.com/thingsboard/flot.git#0.9-work": version "0.9.0-alpha" - resolved "git://github.com/thingsboard/flot.git#6e1a37095868f174d31d5c627c3659b70f9b92dd" + resolved "git://github.com/thingsboard/flot.git#0ff0c775db7c74e705f6c3c2bba92080a202ccd4" flush-write-stream@^1.0.0: version "1.1.1" @@ -4572,7 +4635,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1: +has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== @@ -4815,13 +4878,6 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8" - integrity sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" @@ -4951,21 +5007,21 @@ ini@1.3.5, ini@^1.3.4: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" - integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== +inquirer@7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== dependencies: ansi-escapes "^4.2.1" - chalk "^3.0.0" + chalk "^4.1.0" cli-cursor "^3.1.0" - cli-width "^2.0.0" + cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.15" + lodash "^4.17.19" mute-stream "0.0.8" run-async "^2.4.0" - rxjs "^6.5.3" + rxjs "^6.6.0" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" @@ -5058,10 +5114,10 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== is-color-stop@^1.0.0: version "1.1.0" @@ -5173,6 +5229,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -5240,7 +5301,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.0.4, is-regex@^1.1.0: +is-regex@^1.0.4, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -5340,7 +5401,7 @@ istanbul-lib-coverage@^3.0.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== -istanbul-lib-instrument@^4.0.1: +istanbul-lib-instrument@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== @@ -5409,31 +5470,32 @@ jasminewd2@^2.1.0: resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= -jest-worker@26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.0.0.tgz#4920c7714f0a96c6412464718d0c58a3df3fb066" - integrity sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw== +jest-worker@26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" + integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== dependencies: + "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.0.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" - integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== +jest-worker@^26.3.0: + version "26.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" + integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" -jquery.terminal@^2.16.0: - version "2.17.6" - resolved "https://registry.yarnpkg.com/jquery.terminal/-/jquery.terminal-2.17.6.tgz#72dc5828138cd9cbca5cf92ffee7e0d90e71a92c" - integrity sha512-NPAxHodxrs6hLXNW9VAfijYkBFtoL/pyzpDDu2vX2slUyLekkUD9JBM4V0NcAuOvhB2eW4hLFChoYD5B2uu9Sg== +jquery.terminal@^2.18.3: + version "2.18.3" + resolved "https://registry.yarnpkg.com/jquery.terminal/-/jquery.terminal-2.18.3.tgz#4392fcbc5b2c0d187ea80fe3ecfed03112a5a107" + integrity sha512-zzMVGYlAC+luF7Omm9UY1/nuvp00mozSgcGImObWSS3uDRtcxnxlwxQLC8tvlTT+koyfOvCBaWgB6AD4DvWVpQ== dependencies: "@types/jquery" "^3.3.29" jquery "^3.5.0" - prismjs "^1.19.0" + prismjs "^1.21.0" wcwidth "^1.0.1" jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.5.1: @@ -5441,16 +5503,16 @@ jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.5.1: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== -js-beautify@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.12.0.tgz#6c7e6a47a6075a7c8e60c861e850440a5479d36e" - integrity sha512-hZCm93+sWHqrsB2ac38cPX4A9t6mfReq13ZUr/0dk6rCXNLIq0R4lu0EiuJc0Ip6RiWNtE0vECjXOhcy/jMt9Q== +js-beautify@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.0.tgz#a056d5d3acfd4918549aae3ab039f9f3c51eebb2" + integrity sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA== dependencies: config-chain "^1.1.12" editorconfig "^0.15.3" glob "^7.1.3" mkdirp "^1.0.4" - nopt "^4.0.3" + nopt "^5.0.0" "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -5485,6 +5547,11 @@ json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-bet resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-defaults@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json-schema-defaults/-/json-schema-defaults-0.4.0.tgz#b63ee7e7aa83f29b54cb31d31ecddeb056c3306c" @@ -5526,6 +5593,11 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonc-parser@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" + integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -5632,7 +5704,7 @@ jstree@^3.3.10: dependencies: jquery ">=1.9.1" -jszip@*, jszip@^3.1.3, jszip@^3.5.0: +jszip@^3.1.3, jszip@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== @@ -5738,6 +5810,11 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klona@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + leaflet-editable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/leaflet-editable/-/leaflet-editable-1.2.0.tgz#a3a01001764ba58ea923381ee6a1c814708a0b84" @@ -5770,22 +5847,22 @@ leaflet.markercluster@^1.4.1: resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5" integrity sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw== -leaflet@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" - integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ== +leaflet@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" + integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== -less-loader@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-6.1.0.tgz#59fd591df408ced89a40fce11a2aea449b005631" - integrity sha512-/jLzOwLyqJ7Kt3xg5sHHkXtOyShWwFj410K9Si9WO+/h8rmYxxkSR0A3/hFEntWudE20zZnWMtpMYnLzqTVdUA== +less-loader@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-6.2.0.tgz#8b26f621c155b342eefc24f5bd6e9dc40c42a719" + integrity sha512-Cl5h95/Pz/PWub/tCBgT1oNMFeH1WTD33piG80jn5jr12T4XbxZcjThwNXDQ7AG649WEynuIzO4b0+2Tn9Qolg== dependencies: clone "^2.1.2" - less "^3.11.1" + less "^3.11.3" loader-utils "^2.0.0" - schema-utils "^2.6.6" + schema-utils "^2.7.0" -less@^3.11.1: +less@^3.11.3: version "3.12.2" resolved "https://registry.yarnpkg.com/less/-/less-3.12.2.tgz#157e6dd32a68869df8859314ad38e70211af3ab4" integrity sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q== @@ -5812,10 +5889,10 @@ levenary@^1.1.1: dependencies: leven "^3.1.0" -license-webpack-plugin@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.2.0.tgz#5c964380d7d0e0c27c349d86a6f856c82924590e" - integrity sha512-XPsdL/0brSHf+7dXIlRqotnCQ58RX2au6otkOg4U3dm8uH+Ka/fW4iukEs95uXm+qKe/SBs+s1Ll/aQddKG+tg== +license-webpack-plugin@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.0.tgz#c00f70d5725ba0408de208acb9e66612cc2eceda" + integrity sha512-JK/DXrtN6UeYQSgkg5q1+pgJ8aiKPL9tnz9Wzw+Ikkf+8mJxG56x6t8O+OH/tAeF/5NREnelTEMyFtbJNkjH4w== dependencies: "@types/webpack-sources" "^0.1.5" webpack-sources "^1.2.0" @@ -5904,17 +5981,17 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== dependencies: - chalk "^2.4.2" + chalk "^4.0.0" log4js@^6.2.1: version "6.3.0" @@ -5928,9 +6005,9 @@ log4js@^6.2.1: streamroller "^2.2.4" loglevel@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" - integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== + version "1.7.0" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" + integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" @@ -6150,11 +6227,16 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +"mime-db@>= 1.43.0 < 2": + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" @@ -6177,10 +6259,10 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mini-css-extract-plugin@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" - integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== +mini-css-extract-plugin@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.10.0.tgz#a0e6bfcad22a9c73f6c882a3c7557a98e2d3d27d" + integrity sha512-QgKgJBjaJhxVPwrLNqqwNS0AGkuQQ31Hp4xGXEK/P7wehEg6qmNtReHKai3zRXqY60wGVWLYcOMJK2b98aGc3A== dependencies: loader-utils "^1.1.0" normalize-url "1.9.1" @@ -6284,22 +6366,34 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1, mkdirp@~0.5.x: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" -mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.27.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== +moment-timezone@*, moment-timezone@^0.5.31: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +mousetrap@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" + integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA== mousetrap@^1.6.0: version "1.6.5" @@ -6318,13 +6412,6 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -move-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/move-file/-/move-file-2.0.0.tgz#83ffa309b5d7f69d518b28e1333e2ffadf331e3e" - integrity sha512-cdkdhNCgbP5dvS4tlGxZbD+nloio9GIimP57EjqFhwLcMjnU+XJKAZzlmg/TN/AK1LuNAdTSvm3CPPP4Xkv0iQ== - dependencies: - path-exists "^4.0.0" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -6335,7 +6422,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@^2.0.0, ms@^2.1.1: +ms@2.1.2, ms@^2.0.0, ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -6390,7 +6477,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0, neo-async@^2.6.1: +neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -6414,17 +6501,19 @@ ngx-clipboard@^13.0.1: dependencies: ngx-window-token ">=3.0.0" -ngx-color-picker@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/ngx-color-picker/-/ngx-color-picker-10.0.1.tgz#b6f10b2a4bc625c5430bfd5d0f18eb49140e5ca6" - integrity sha512-HlF+pWCmIEEaqs8n0pjTI0u1gLcCcT4Icay1JUwJRD2WJ4vyZiyDDzpnSRk6nkfdRAZp3ch40pEuZa+VlYOUTg== +ngx-color-picker@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ngx-color-picker/-/ngx-color-picker-10.1.0.tgz#19a6993a74bb3553024623b20ca6ebffd2c50f9c" + integrity sha512-Q3BILkQP+l+dcX0joe7+xuHDKydhGnG09sUG1FmlLZFYIEX4+AQqHULh+hUAci8kZlLZuOG+mB2Uq54QYadItw== dependencies: tslib "^2.0.0" -ngx-daterangepicker-material@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-3.0.4.tgz#af759e52fd587fcc9bce1fbcfc8cde828df6a471" - integrity sha512-pDg8kdXx/h8es8dpjBI+xbsxQbS0dV3uSPgfsx39t9LIw3Dv50h8T1achT5jUWSzSU7855ywTk+NlNBDTgkeNg== +ngx-daterangepicker-material@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-4.0.1.tgz#788c2e32eb4717629d4a0e60a60bf8d6430d8c13" + integrity sha512-0gY6DGU+dgYdmoAKrIJSB9xnDqBvj91Yis3II/ZJxxMfZVTG4qMMatck6w8FzdU+CYT64ArCq+Uwa6hJRHX6Nw== + dependencies: + tslib "^1.10.0" "ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": version "0.0.0" @@ -6473,10 +6562,10 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-forge@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" - integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-libs-browser@^2.2.1: version "2.2.1" @@ -6507,18 +6596,17 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.61: + version "1.1.61" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" + integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== -nopt@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== dependencies: abbrev "1" - osenv "^0.1.4" normalize-package-data@^2.0.0, normalize-package-data@^2.4.0: version "2.5.0" @@ -6628,9 +6716,9 @@ npm-pick-manifest@^3.0.0: semver "^5.4.1" npm-registry-fetch@^4.0.0: - version "4.0.6" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-4.0.6.tgz#3bd895ff52cbe7ac117fb9778f0e83aa9b54bfd5" - integrity sha512-SEp9m7fPe8FIKzhg2JS+xs+w4YY9mEwRlMcEZRELph7rdoygIQGY6B+g0+wdZqokHE8f57ZkexxYWqFO0FkfCw== + version "4.0.7" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz#57951bf6541e0246b34c9f9a38ab73607c9449d7" + integrity sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ== dependencies: JSONStream "^1.3.4" bluebird "^3.5.1" @@ -6683,20 +6771,20 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.7.0: +object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== object-is@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" - integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" + integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== dependencies: define-properties "^1.1.3" - es-abstract "^1.17.5" + es-abstract "^1.18.0-next.1" -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -6713,15 +6801,15 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: version "2.1.0" @@ -6784,10 +6872,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -open@7.0.4: - version "7.0.4" - resolved "https://registry.yarnpkg.com/open/-/open-7.0.4.tgz#c28a9d315e5c98340bf979fdcb2e58664aa10d83" - integrity sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ== +open@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.2.0.tgz#212959bd7b0ce2e8e3676adc76e3cf2f0a2498b4" + integrity sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ== dependencies: is-docker "^2.0.0" is-wsl "^2.1.1" @@ -6799,16 +6887,16 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -ora@4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" - integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww== +ora@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.0.0.tgz#4f0b34f2994877b49b452a707245ab1e9f6afccb" + integrity sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw== dependencies: - chalk "^3.0.0" + chalk "^4.1.0" cli-cursor "^3.1.0" - cli-spinners "^2.2.0" + cli-spinners "^2.4.0" is-interactive "^1.0.0" - log-symbols "^3.0.0" + log-symbols "^4.0.0" mute-stream "0.0.8" strip-ansi "^6.0.0" wcwidth "^1.0.1" @@ -6835,7 +6923,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@^0.1.4, osenv@^0.1.5: +osenv@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -6848,14 +6936,14 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" -p-limit@^3.0.1: +p-limit@^3.0.1, p-limit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== @@ -6969,10 +7057,17 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== +parse5-htmlparser2-tree-adapter@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + +parse5@6.0.1, parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parse5@^5.0.0: version "5.1.1" @@ -6986,6 +7081,11 @@ parseqs@0.0.5: dependencies: better-assert "~1.0.0" +parseqs@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" + integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== + parseuri@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" @@ -6993,6 +7093,11 @@ parseuri@0.0.5: dependencies: better-assert "~1.0.0" +parseuri@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" + integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -7137,9 +7242,9 @@ posix-character-classes@^0.1.0: integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= postcss-calc@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.3.tgz#d65cca92a3c52bf27ad37a5f732e0587b74f1623" - integrity sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA== + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== dependencies: postcss "^7.0.27" postcss-selector-parser "^6.0.2" @@ -7203,9 +7308,9 @@ postcss-import@12.0.1: resolve "^1.1.7" postcss-load-config@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" - integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q== + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" + integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== dependencies: cosmiconfig "^5.0.0" import-cwd "^2.0.0" @@ -7289,7 +7394,7 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" -postcss-modules-local-by-default@^3.0.2: +postcss-modules-local-by-default@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== @@ -7435,13 +7540,14 @@ postcss-selector-parser@^3.0.0: uniq "^1.0.1" postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" - integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== dependencies: cssesc "^3.0.0" indexes-of "^1.0.1" uniq "^1.0.1" + util-deprecate "^1.0.2" postcss-svgo@^4.0.2: version "4.0.2" @@ -7467,7 +7573,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.2, postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0: +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== @@ -7481,19 +7587,19 @@ postcss@7.0.21: source-map "^0.6.1" supports-color "^6.1.0" -postcss@7.0.31: - version "7.0.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.31.tgz#332af45cb73e26c0ee2614d7c7fb02dfcc2bd6dd" - integrity sha512-a937VDHE1ftkjk+8/7nj/mrjtmkn69xxzJgRETXdAUU+IgOYPQNJF17haGWbeDxSyk++HA14UA98FurvPyBJOA== +postcss@7.0.32: + version "7.0.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== dependencies: chalk "^2.4.2" source-map "^0.6.1" supports-color "^6.1.0" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.30, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.32" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" - integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -7504,12 +7610,12 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -prettier@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== +prettier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== -prismjs@^1.19.0: +prismjs@^1.21.0: version "1.21.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== @@ -7700,16 +7806,9 @@ querystring@0.2.0: integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - -raf@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" @@ -7757,82 +7856,80 @@ raw-loader@4.0.1: schema-utils "^2.6.5" rc-align@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.2.tgz#5097b239f96a8ad5a44b6ba7ce7e4dbc195d6654" - integrity sha512-HoTOCLXQehTOxy+Iiy6z0cDRssTSq+0UJuttMLoxtWpn6yorJO7k59ru74HZ7Pad3p0HDOD8v0m/4FQ+bANnsg== + version "4.0.8" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.8.tgz#276c3f5dfadf0de4bb95392cb81568c9e947a668" + integrity sha512-2sRUkmB8z4UEXzaS+lDHzXMoR8HrtKH9nn2yHlHVNyUTnaucjMFbdEoCk+hO1g7cpIgW0MphG8i0EH2scSesfw== dependencies: "@babel/runtime" "^7.10.1" classnames "2.x" dom-align "^1.7.0" - rc-util "^5.0.1" + rc-util "^5.3.0" resize-observer-polyfill "^1.5.1" -rc-motion@^1.0.0, rc-motion@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-1.0.2.tgz#b8aec288642298d74ddc9ac1773e1b600aaa1c25" - integrity sha512-FDmC9ZdzsXerlTZ+YLu+l5erjkMU98s85SFHdQac+pMy6zQ10RuON6Ntv3ZwP0+qY/YlIsK+0uMXIWOJ9LaLIg== +rc-motion@^2.0.0, rc-motion@^2.0.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.3.1.tgz#a0c9f402c267bd03543ef80a970297a6ba77c503" + integrity sha512-UAB2gwS9c1DuCFKVCimAkL2JUEGCwzgCbb+VslhIMFg6vY7oyMxYIQ6hkoji1PzgDEw0tHIhP+a17R6Y5DGMrQ== dependencies: "@babel/runtime" "^7.11.1" classnames "^2.2.1" - raf "^3.4.1" - rc-util "^5.0.6" + rc-util "^5.2.1" rc-resize-observer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-0.2.3.tgz#8268284d1766d163240b1682661ae7b59bc4523d" - integrity sha512-dEPCGX15eRRnu+TNBIGyEghpzE24fTDW8pHdJPJS/kCR3lafFqBLqKzBgZW6pMUuM70/ZDyFQ0Kynx9kWsXRNw== + version "0.2.5" + resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-0.2.5.tgz#03e3a5c3dfccd6c996a547e4f82721e4f20f6156" + integrity sha512-cc4sOI722MVoCkGf/ZZybDVsjxvnH0giyDdA7wBJLTiMSFJ0eyxBMnr0JLYoClxftjnr75Xzl/VUB3HDrAx04Q== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.1" rc-util "^5.0.0" resize-observer-polyfill "^1.5.1" -rc-select@^11.1.3: - version "11.1.5" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-11.1.5.tgz#df73e9b7c56da547ba012d4eb95c46e5fb158216" - integrity sha512-PQ502UZo/bGecEv3yyK7GqDhS/kN3EUwGLeBOG6M8A7lODwPJBEYJho8TGtS1YhJFBETzKQ85Ya7wsC73Bc+2w== +rc-select@^11.3.3: + version "11.3.3" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-11.3.3.tgz#ba445ac4d2d933dd1f80b796c1de28ce6c81bbf8" + integrity sha512-YMsGVEZxXctj15nIZKlFCkiOxMe0PNBeACN6nHqDozDYKR/aqP8J3XZqZ5Gw/fcgS4bI50zPVMieJKlY8/6Wfw== dependencies: "@babel/runtime" "^7.10.1" classnames "2.x" - rc-motion "^1.0.1" - rc-trigger "^4.3.0" + rc-motion "^2.0.1" + rc-trigger "^5.0.4" rc-util "^5.0.1" - rc-virtual-list "^2.1.5" + rc-virtual-list "^3.0.3" warning "^4.0.3" -rc-trigger@^4.3.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.4.0.tgz#52be45c7b40327b297ebacff84d69ce9285606bc" - integrity sha512-09562wc5I1JUbCdWohcFYJeLTpjKjEqH+0lY7plDtyI9yFXRngrvmqsrSJyT6Nat+C35ymD7fhwCCPq3cfUI4g== +rc-trigger@^5.0.4: + version "5.0.6" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.0.6.tgz#7e84717525871a7923a671edb5a290e9e616525b" + integrity sha512-L/xIX5OO7a/bdTH0yYT9mwAsV6oM1inAqAbLjoUzpcIW+UUE3jjVOjm5VaKDfHd41rzDzOfN05URmhet5QzXKQ== dependencies: - "@babel/runtime" "^7.10.1" + "@babel/runtime" "^7.11.2" classnames "^2.2.6" - raf "^3.4.1" rc-align "^4.0.0" - rc-motion "^1.0.0" - rc-util "^5.0.1" + rc-motion "^2.0.0" + rc-util "^5.3.4" -rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.0.7.tgz#125c3a2fd917803afbb685f9eadc789b085dc813" - integrity sha512-nr98b5aMqqvIqxm16nF+zC3K3f81r+HsflT5E9Encr5itRwV7Vo/5GjJMNds/WiFV33rilq7vzb3xeAbCycmwg== +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.7, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.3.4: + version "5.4.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004" + integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg== dependencies: react-is "^16.12.0" shallowequal "^1.1.0" -rc-virtual-list@^2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-2.1.5.tgz#f065e9835e16c612525e884662a886a25e4ff793" - integrity sha512-4jcNyR74cHfEEk25xjVfjYh6RsgK22XRApz0VpMPyygUnEpm0+44w3OP8S9vsFq3QAWUpOmW4kwp3ESJ917aaw== +rc-virtual-list@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.1.0.tgz#ca7ddbb291dace89c00cc4198ca7ef6e5e2034f7" + integrity sha512-DYU3wOjVuQo4hzYvmmpnoNtxrd8OIcutazA90x374ZFGGm4xYoSjCdh6UhBLi47IJI2BRry4l583nuoi7+06GA== dependencies: classnames "^2.2.6" rc-resize-observer "^0.2.3" rc-util "^5.0.7" -react-ace@^9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.1.3.tgz#848dc3741d5460f3ac73468b6c39879aab9238bc" - integrity sha512-1TZBs/9hFGgPuzu6DUiBogyhRA5Z1Po2wzPfZslbrTFGQtbNe+JXHuPoJNlUu/uerElzOLLsuJEDTO9FfLnZJA== +react-ace@^9.1.4: + version "9.1.4" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.1.4.tgz#7c45c361aa5fe1efa3313fa876bce30aa64a244f" + integrity sha512-4DBWvElbVR3WhsA2HhQ524K9Yoa/J/sjuBV9NUZ+yar3Q4BGJRTnhY6pM0INffH1IkBZHKIOyz34XHjc7RNTpw== dependencies: ace-builds "^1.4.6" diff-match-patch "^1.0.4" @@ -7850,10 +7947,10 @@ react-dom@^16.13.1: prop-types "^15.6.2" scheduler "^0.19.1" -react-dropzone@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.0.3.tgz#59c396a1482454fa78466f8565336f40ce7f7c84" - integrity sha512-+MoMOoKZfkZ9i1+qEFl2ZU29AB/c9K2bFxyACqGynguJunmqO+k2PJ2AcuiH51xVNl9R7q/x5QdBaIWb6RtoSw== +react-dropzone@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.2.0.tgz#4e54fa3479e6b6bb93f67914e4a27f1260807fdb" + integrity sha512-S/qaXQHCCg7MVlcrhqd05MLC6DupITLUB0CFn3iCLs6OTjzxdGDF1WTktTe5Jyq8jZdxYfMHNUZOHL0mg+K0Dw== dependencies: attr-accept "^2.0.0" file-selector "^0.1.12" @@ -7898,16 +7995,14 @@ read-cache@^1.0.0: pify "^2.3.0" read-package-json@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" - integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A== + version "2.1.2" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" + integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== dependencies: glob "^7.1.1" - json-parse-better-errors "^1.0.1" + json-parse-even-better-errors "^2.3.0" normalize-package-data "^2.0.0" npm-normalize-package-bin "^1.0.0" - optionalDependencies: - graceful-fs "^4.1.2" read-package-tree@5.3.1: version "5.3.1" @@ -7983,12 +8078,7 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== -regenerator-runtime@0.13.5: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== - -regenerator-runtime@^0.13.4: +regenerator-runtime@0.13.7, regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== @@ -8022,9 +8112,9 @@ regexp.prototype.flags@^1.2.0: es-abstract "^1.17.0-next.1" regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.2.0" @@ -8060,7 +8150,7 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -request@^2.87.0, request@^2.88.0: +request@^2.87.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -8231,10 +8321,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup@2.10.9: - version "2.10.9" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.10.9.tgz#17dcc6753c619efcc1be2cf61d73a87827eebdf9" - integrity sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg== +rollup@2.26.5: + version "2.26.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.5.tgz#5562ec36fcba3eed65cfd630bd78e037ad0e0307" + integrity sha512-rCyFG3ZtQdnn9YwfuAVH0l/Om34BdO5lwCA0W6Hq+bNB21dVEBbCRxhaHOmu1G7OBFDWytbzAC104u7rxHwGjA== optionalDependencies: fsevents "~2.1.2" @@ -8255,20 +8345,20 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@6.5.5: - version "6.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" - integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== - dependencies: - tslib "^1.9.0" - -rxjs@^6.5.3, rxjs@^6.6.2: +rxjs@6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== dependencies: tslib "^1.9.0" +rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.3: + version "6.6.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" + integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -8291,21 +8381,21 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" - integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== +sass-loader@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.1.tgz#10c0364d8034f22fee25ddcc9eded20f99bbe3b4" + integrity sha512-b2PSldKVTS3JcFPHSrEXh3BeAfR7XknGiGCAO5aHruR3Pf3kqLP3Gb2ypXLglRrAzgZkloNxLZ7GXEGDX0hBUQ== dependencies: - clone-deep "^4.0.1" - loader-utils "^1.2.3" - neo-async "^2.6.1" - schema-utils "^2.6.1" - semver "^6.3.0" + klona "^2.0.3" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^2.7.0" + semver "^7.3.2" -sass@1.26.5: - version "1.26.5" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.5.tgz#2d7aecfbbabfa298567c8f06615b6e24d2d68099" - integrity sha512-FG2swzaZUiX53YzZSjSakzvGtlds0lcbF+URuU9mxOv7WBh7NhXEVDa4kPKN4hN6fC2TkOTOKqiqp6d53N9X5Q== +sass@1.26.10: + version "1.26.10" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.10.tgz#851d126021cdc93decbf201d1eca2a20ee434760" + integrity sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw== dependencies: chokidar ">=2.0.0 <4.0.0" @@ -8345,14 +8435,14 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.1, schema-utils@^2.6.4, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== +schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0, schema-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" screenfull@^5.0.2: version "5.0.2" @@ -8380,11 +8470,11 @@ selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: xml2js "^0.4.17" selfsigned@^1.10.7: - version "1.10.7" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" - integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== + version "1.10.8" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== dependencies: - node-forge "0.9.0" + node-forge "^0.10.0" semver-dsl@^1.0.1: version "1.0.1" @@ -8410,7 +8500,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.3.2, semver@^7.0.0, semver@^7.1.1: +semver@7.3.2, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== @@ -8439,13 +8529,6 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" - integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== - dependencies: - randombytes "^2.1.0" - serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -8453,6 +8536,13 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -8519,13 +8609,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" @@ -8626,11 +8709,11 @@ socket.io-client@2.3.0: to-array "0.1.4" socket.io-parser@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" - integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + version "3.3.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199" + integrity sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ== dependencies: - component-emitter "1.2.1" + component-emitter "~1.3.0" debug "~3.1.0" isarray "2.0.1" @@ -8704,16 +8787,16 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-loader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.0.0.tgz#240b88575a9b0d27214aeecbd4e7686af95cfa56" - integrity sha512-ZayyQCSCrQazN50aCvuS84lJT4xc1ZAcykH5blHaBdVveSwjiFK8UGMPvao0ho54DTb0Jf7m57uRRG/YYUZ2Fg== +source-map-loader@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.0.2.tgz#b0a6582b2eaa387ede1ecf8061ae0b93c23f9eb0" + integrity sha512-oX8d6ndRjN+tVyjj6PlXSyFPhDdVAPsZA30nD3/II8g4uOv8fCz0DMn5sy8KtVbDfKQxOpGwGJnK3xIW3tauDw== dependencies: data-urls "^2.0.0" - iconv-lite "^0.5.1" + iconv-lite "^0.6.2" loader-utils "^2.0.0" - schema-utils "^2.6.6" - source-map "^0.6.0" + schema-utils "^2.7.0" + source-map "^0.6.1" source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: version "0.5.3" @@ -8726,7 +8809,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@^0.5.5, source-map-support@~0.5.12: +source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@^0.5.5, source-map-support@~0.5.12, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -8751,7 +8834,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@0.7.3, source-map@^0.7.3: +source-map@0.7.3, source-map@^0.7.3, source-map@~0.7.2: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -8788,9 +8871,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== spdy-transport@^3.0.0: version "3.0.0" @@ -9037,18 +9120,18 @@ stylus-loader@3.0.2: lodash.clonedeep "^4.5.0" when "~3.6.x" -stylus@0.54.7: - version "0.54.7" - resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.7.tgz#c6ce4793965ee538bcebe50f31537bfc04d88cd2" - integrity sha512-Yw3WMTzVwevT6ZTrLCYNHAFmanMxdylelL3hkWNgPMeTCpMwpV3nXjpOHuBXtFv7aiO2xRuQS6OoAdgkNcSNug== +stylus@0.54.8: + version "0.54.8" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.8.tgz#3da3e65966bc567a7b044bfe0eece653e099d147" + integrity sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== dependencies: css-parse "~2.0.0" debug "~3.1.0" - glob "^7.1.3" - mkdirp "~0.5.x" + glob "^7.1.6" + mkdirp "~1.0.4" safer-buffer "^2.1.2" sax "~1.2.4" - semver "^6.0.0" + semver "^6.3.0" source-map "^0.7.3" supports-color@^2.0.0: @@ -9071,9 +9154,9 @@ supports-color@^6.1.0: has-flag "^3.0.0" supports-color@^7.0.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" @@ -9136,19 +9219,19 @@ tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" -terser-webpack-plugin@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-3.0.1.tgz#31928c9330a582fb5ec6f90805337289b85cb8fe" - integrity sha512-eFDtq8qPUEa9hXcUzTwKXTnugIVtlqc1Z/ZVhG8LmRT3lgRY13+pQTnFLY2N7ATB6TKCHuW/IGjoAnZz9wOIqw== +terser-webpack-plugin@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.1.0.tgz#6e9d6ae4e1a900d88ddce8da6a47507ea61f44bc" + integrity sha512-0ZWDPIP8BtEDZdChbufcXUigOYk6dOX/P/X0hWxqDDcVAQLb8Yy/0FAaemSfax3PAA67+DJR778oz8qVbmy4hA== dependencies: - cacache "^15.0.3" + cacache "^15.0.5" find-cache-dir "^3.3.1" - jest-worker "^26.0.0" - p-limit "^2.3.0" + jest-worker "^26.3.0" + p-limit "^3.0.2" schema-utils "^2.6.6" - serialize-javascript "^3.0.0" + serialize-javascript "^4.0.0" source-map "^0.6.1" - terser "^4.6.13" + terser "^5.0.0" webpack-sources "^1.4.3" terser-webpack-plugin@^1.4.3: @@ -9166,16 +9249,16 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.7.0.tgz#15852cf1a08e3256a80428e865a2fa893ffba006" - integrity sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw== +terser@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.0.tgz#c481f4afecdcc182d5e2bdd2ff2dc61555161e81" + integrity sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg== dependencies: commander "^2.20.0" source-map "~0.6.1" source-map-support "~0.5.12" -terser@^4.1.2, terser@^4.6.13: +terser@^4.1.2: version "4.8.0" resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== @@ -9184,6 +9267,15 @@ terser@^4.1.2, terser@^4.6.13: source-map "~0.6.1" source-map-support "~0.5.12" +terser@^5.0.0: + version "5.3.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.4.tgz#e510e05f86e0bd87f01835c3238839193f77a60c" + integrity sha512-dxuB8KQo8Gt6OVOeLg/rxfcxdNZI/V1G6ze1czFUzPeCFWZRtvZMgSzlZZ5OYBZ4HoG607F6pFPNLekJyV+yVw== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -9224,10 +9316,10 @@ tiny-warning@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tinycolor2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" - integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= +tinycolor2@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" + integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== tmp@0.0.30: version "0.0.30" @@ -9327,10 +9419,10 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-node@^8.10.2: - version "8.10.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== +ts-node@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" + integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== dependencies: arg "^4.1.0" diff "^4.0.1" @@ -9343,21 +9435,21 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== -tslib@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" - integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== - -tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -tslib@^2.0.0, tslib@^2.0.1: +tslib@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== +tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" + integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== + +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242" + integrity sha512-wAH28hcEKwna96/UacuWaVspVLkg4x1aDM9JlzqaQTOFczCktkVAb5fmXChgandR1EraDPs2w8P+ozM+oafwxg== + tslint@~6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904" @@ -9425,24 +9517,29 @@ type@^1.0.1: integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3" - integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow== + version "2.1.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" + integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typeface-roboto@^0.0.75: - version "0.0.75" - resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b" - integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg== +typeface-roboto@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5" + integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw== -typescript@~3.9.7: - version "3.9.7" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +typescript@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== + +typescript@~4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== ua-parser-js@0.7.21: version "0.7.21" @@ -9506,13 +9603,13 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" -universal-analytics@0.4.20: - version "0.4.20" - resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.20.tgz#d6b64e5312bf74f7c368e3024a922135dbf24b03" - integrity sha512-gE91dtMvNkjO+kWsPstHRtSwHXz0l2axqptGYp5ceg4MsuurloM0PU3pdOfpb5zBXUvyjT4PwhWK2m39uczZuw== +universal-analytics@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.23.tgz#d915e676850c25c4156762471bdd7cf2eaaca8ac" + integrity sha512-lgMIH7XBI6OgYn1woDEmxhGdj8yDefMKg7GkWdeATAlQZFrMrNyxSkpDzY57iY0/6fdlzTbBV03OawvvzG+q7A== dependencies: - debug "^3.0.0" - request "^2.88.0" + debug "^4.1.1" + request "^2.88.2" uuid "^3.0.0" universalify@^0.1.0: @@ -9544,9 +9641,9 @@ upath@^1.1.1: integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" @@ -9576,7 +9673,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -9617,10 +9714,10 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" - integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== +uuid@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" @@ -9685,7 +9782,7 @@ watchpack-chokidar2@^2.0.0: dependencies: chokidar "^2.1.8" -watchpack@^1.6.1, watchpack@^1.7.4: +watchpack@^1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== @@ -9735,10 +9832,10 @@ webdriver-manager@^12.1.7: semver "^5.3.0" xml2js "^0.4.17" -webidl-conversions@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" - integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== webpack-dev-middleware@3.7.2, webpack-dev-middleware@^3.7.2: version "3.7.2" @@ -9820,10 +9917,10 @@ webpack-subresource-integrity@1.4.1: dependencies: webpack-sources "^1.3.0" -webpack@4.43.0: - version "4.43.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" - integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== +webpack@4.44.1: + version "4.44.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" @@ -9833,7 +9930,7 @@ webpack@4.43.0: ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" + enhanced-resolve "^4.3.0" eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" loader-runner "^2.4.0" @@ -9846,13 +9943,13 @@ webpack@4.43.0: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.1" + watchpack "^1.7.4" webpack-sources "^1.4.1" -webpack@^4.44.1: - version "4.44.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" - integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== +webpack@^4.44.2: + version "4.44.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" @@ -9905,13 +10002,13 @@ whatwg-mimetype@^2.3.0: integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.1.0.tgz#c628acdcf45b82274ce7281ee31dd3c839791771" - integrity sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw== + version "8.3.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.3.0.tgz#d1e11e565334486cdb280d3101b9c3fd1c867582" + integrity sha512-BQRf/ej5Rp3+n7k0grQXZj9a1cHtsp4lqj01p59xBWFKdezR8sO37XnpafwNqiFac/v2Il12EIMjX/Y4VZtT8Q== dependencies: lodash.sortby "^4.7.0" tr46 "^2.0.2" - webidl-conversions "^5.0.0" + webidl-conversions "^6.1.0" when@~3.6.x: version "3.6.4" @@ -9937,10 +10034,10 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -worker-plugin@4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-4.0.3.tgz#7c42e600d5931ad154d3d5f187a32446df64db0f" - integrity sha512-7hFDYWiKcE3yHZvemsoM9lZis/PzurHAEX1ej8PLCu818Rt6QqUAiDdxHPCKZctzmhqzPpcFSgvMCiPbtooqAg== +worker-plugin@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-5.0.0.tgz#113b5fe1f4a5d6a957cecd29915bedafd70bb537" + integrity sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ== dependencies: loader-utils "^1.1.0" @@ -10109,3 +10206,10 @@ zone.js@~0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== + +zone.js@~0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.1.tgz#0301d00d26febb2722f074c46aac4a948698ce39" + integrity sha512-KcZawpmVgS+3U2rzKTM6fLKaCX1QDv3//NxiSOOsqpQY/r5hl+xpYikPwY93Sp7CAB+J5mZJpb/YubxEYLGK5g== + dependencies: + tslib "^2.0.0"