Browse Source

Merge with master

pull/1166/head
Andrew Shvayka 8 years ago
parent
commit
8c9a6c3e47
  1. 110
      application/src/main/data/upgrade/2.1.2/schema_update.cql
  2. 32
      application/src/main/data/upgrade/2.1.2/schema_update.sql
  3. 14
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  4. 33
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  5. 5
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  6. 49
      application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
  7. 9
      application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
  8. 34
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
  10. 97
      application/src/main/resources/thingsboard.yml
  11. 104
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  12. 32
      application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
  13. 66
      application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
  14. 1
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  15. 1
      common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java
  16. 69
      dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java
  17. 32
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java
  18. 7
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  19. 54
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  20. 7
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  21. 7
      dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java
  22. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java
  23. 41
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  24. 42
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  25. 23
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  26. 49
      dao/src/main/resources/cassandra/schema-entities.cql
  27. 240
      dao/src/main/resources/cassandra/system-data.cql
  28. 3
      dao/src/main/resources/sql/schema-entities.sql
  29. 2
      dao/src/test/resources/sql/drop-all-tables.sql
  30. 5
      msa/docker/.env
  31. 16
      msa/docker/docker-compose.yml
  32. 2
      msa/docker/docker-start-services.sh
  33. 1
      msa/docker/docker-update-service.sh
  34. 3
      msa/docker/haproxy/config/haproxy.cfg
  35. 2
      msa/docker/tb-node/conf/thingsboard.conf
  36. 2
      msa/js-executor/docker/Dockerfile
  37. 3
      msa/js-executor/pom.xml
  38. 2
      msa/js-executor/src/main/scripts/control/deb/postinst
  39. 3
      msa/tb-node/pom.xml
  40. 2
      msa/web-ui/docker/Dockerfile
  41. 3
      msa/web-ui/pom.xml
  42. 2
      msa/web-ui/src/main/scripts/control/deb/postinst
  43. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java
  44. 2
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
  45. 2
      transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  46. 4
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
  47. 7
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  48. 17
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  49. 11
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  50. 2
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
  51. 2
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  52. 21
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java
  53. 55
      transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java
  54. 53
      transport/mqtt-transport/src/main/resources/tb-mqtt-transport.yml
  55. 33
      ui/src/app/api/entity.service.js
  56. 8
      ui/src/app/api/subscription.js
  57. 53
      ui/src/app/api/time.service.js
  58. 3
      ui/src/app/api/user.service.js
  59. 8
      ui/src/app/common/types.constant.js
  60. 10
      ui/src/app/components/timewindow-panel.controller.js
  61. 15
      ui/src/app/components/timewindow-panel.tpl.html
  62. 2
      ui/src/app/components/timewindow.directive.js
  63. 22
      ui/src/app/entity-view/entity-view-fieldset.tpl.html
  64. 4
      ui/src/app/entity-view/entity-view.routes.js
  65. 22
      ui/src/app/entity/entity-filter-view.directive.js
  66. 8
      ui/src/app/entity/entity-filter.directive.js
  67. 83
      ui/src/app/entity/entity-filter.tpl.html
  68. 11
      ui/src/app/entity/entity-subtype-autocomplete.directive.js
  69. 10
      ui/src/app/entity/entity-subtype-list.directive.js
  70. 11
      ui/src/app/entity/entity-subtype-select.directive.js
  71. 1
      ui/src/app/help/help-links.constant.js
  72. 1545
      ui/src/app/locale/locale.constant-TR.json
  73. 11
      ui/src/app/locale/locale.constant-en_US.json
  74. 2107
      ui/src/app/locale/locale.constant-es_ES.json
  75. 2919
      ui/src/app/locale/locale.constant-fr_FR.json
  76. 89
      ui/src/app/locale/locale.constant-it_IT.json
  77. 3
      ui/src/app/locale/locale.constant-ja_JA.json
  78. 3
      ui/src/app/locale/locale.constant-ko_KR.json
  79. 3
      ui/src/app/locale/locale.constant-ru_RU.json
  80. 3
      ui/src/app/locale/locale.constant-zh_CN.json
  81. 8
      ui/src/app/services/menu.service.js
  82. 2
      ui/src/app/widget/lib/google-map.js

110
application/src/main/data/upgrade/2.1.2/schema_update.cql

@ -0,0 +1,110 @@
--
-- Copyright © 2016-2018 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT 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 MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_name;
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_search_text;
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_customer;
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_entity_id;
DROP TABLE IF EXISTS thingsboard.entity_views;
CREATE TABLE IF NOT EXISTS thingsboard.entity_view (
id timeuuid,
entity_id timeuuid,
entity_type text,
tenant_id timeuuid,
customer_id timeuuid,
name text,
type text,
keys text,
start_ts bigint,
end_ts bigint,
search_text text,
additional_info text,
PRIMARY KEY (id, entity_id, tenant_id, customer_id, type)
);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND name IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, name, id, customer_id, entity_id, type)
WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id, type)
WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_by_type_and_search_text AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, type, search_text, id, customer_id, entity_id)
WITH CLUSTERING ORDER BY (type ASC, search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND entity_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id, type)
WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer_and_type AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND entity_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, type, customer_id, search_text, id, entity_id)
WITH CLUSTERING ORDER BY (type ASC, customer_id DESC, search_text ASC, id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_entity_id AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND entity_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id, type)
WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC);

32
application/src/main/data/upgrade/2.1.2/schema_update.sql

@ -0,0 +1,32 @@
--
-- Copyright © 2016-2018 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT 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 TABLE IF EXISTS entity_views;
CREATE TABLE IF NOT EXISTS entity_view (
id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
entity_id varchar(31),
entity_type varchar(255),
tenant_id varchar(31),
customer_id varchar(31),
type varchar(255),
name varchar(255),
keys varchar(255),
start_ts bigint,
end_ts bigint,
search_text varchar(255),
additional_info varchar
);

14
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -49,6 +51,11 @@ public class DashboardController extends BaseController {
public static final String DASHBOARD_ID = "dashboardId"; public static final String DASHBOARD_ID = "dashboardId";
@Value("${dashboard.max_datapoints_limit}")
@Getter
private long maxDatapointsLimit;
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET) @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET)
@ResponseBody @ResponseBody
@ -56,6 +63,13 @@ public class DashboardController extends BaseController {
return System.currentTimeMillis(); return System.currentTimeMillis();
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/maxDatapointsLimit", method = RequestMethod.GET)
@ResponseBody
public long getMaxDatapointsLimit() throws ThingsboardException {
return maxDatapointsLimit;
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET) @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET)
@ResponseBody @ResponseBody

33
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -26,6 +27,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -161,6 +164,7 @@ public class EntityViewController extends BaseController {
public TextPageData<EntityView> getCustomerEntityViews( public TextPageData<EntityView> getCustomerEntityViews(
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@RequestParam int limit, @RequestParam int limit,
@RequestParam(required = false) String type,
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset, @RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException { @RequestParam(required = false) String textOffset) throws ThingsboardException {
@ -170,7 +174,11 @@ public class EntityViewController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId); checkCustomerId(customerId);
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); if (type != null && type.trim().length() > 0) {
return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type));
} else {
return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -181,13 +189,19 @@ public class EntityViewController extends BaseController {
@ResponseBody @ResponseBody
public TextPageData<EntityView> getTenantEntityViews( public TextPageData<EntityView> getTenantEntityViews(
@RequestParam int limit, @RequestParam int limit,
@RequestParam(required = false) String type,
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset, @RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException { @RequestParam(required = false) String textOffset) throws ThingsboardException {
try { try {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink));
if (type != null && type.trim().length() > 0) {
return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type));
} else {
return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink));
}
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -199,6 +213,7 @@ public class EntityViewController extends BaseController {
public List<EntityView> findByQuery(@RequestBody EntityViewSearchQuery query) throws ThingsboardException { public List<EntityView> findByQuery(@RequestBody EntityViewSearchQuery query) throws ThingsboardException {
checkNotNull(query); checkNotNull(query);
checkNotNull(query.getParameters()); checkNotNull(query.getParameters());
checkNotNull(query.getEntityViewTypes());
checkEntityId(query.getParameters().getEntityId()); checkEntityId(query.getParameters().getEntityId());
try { try {
List<EntityView> entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(query).get()); List<EntityView> entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(query).get());
@ -215,4 +230,18 @@ public class EntityViewController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/types", method = RequestMethod.GET)
@ResponseBody
public List<EntitySubtype> getEntityViewTypes() throws ThingsboardException {
try {
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
ListenableFuture<List<EntitySubtype>> entityViewTypes = entityViewService.findEntityViewTypesByTenantId(tenantId);
return checkNotNull(entityViewTypes.get());
} catch (Exception e) {
throw handleException(e);
}
}
} }

5
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -97,6 +97,11 @@ public class ThingsboardInstallService {
databaseUpgradeService.upgradeDatabase("2.0.0"); databaseUpgradeService.upgradeDatabase("2.0.0");
case "2.1.1":
log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ...");
databaseUpgradeService.upgradeDatabase("2.1.1");
log.info("Updating system data..."); log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts"); systemDataLoaderService.deleteSystemWidgetBundle("charts");

49
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java

@ -39,10 +39,19 @@ import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATIO
import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID; import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;
import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD; import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;
import static org.thingsboard.server.service.install.DatabaseHelper.DEVICE; import static org.thingsboard.server.service.install.DatabaseHelper.DEVICE;
import static org.thingsboard.server.service.install.DatabaseHelper.END_TS;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_ID;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_TYPE;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEW;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEWS;
import static org.thingsboard.server.service.install.DatabaseHelper.ID; import static org.thingsboard.server.service.install.DatabaseHelper.ID;
import static org.thingsboard.server.service.install.DatabaseHelper.KEYS;
import static org.thingsboard.server.service.install.DatabaseHelper.NAME;
import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT; import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;
import static org.thingsboard.server.service.install.DatabaseHelper.START_TS;
import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID; import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;
import static org.thingsboard.server.service.install.DatabaseHelper.TITLE; import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;
import static org.thingsboard.server.service.install.DatabaseHelper.TYPE;
@Service @Service
@NoSqlDao @NoSqlDao
@ -203,6 +212,46 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Schema updated."); log.info("Schema updated.");
break; break;
case "2.0.0":
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.1", SCHEMA_UPDATE_CQL);
loadCql(schemaUpdateFile);
log.info("Schema updated.");
break;
case "2.1.1":
log.info("Upgrading Cassandra DataBase from version {} to 2.1.2 ...", fromVersion);
cluster.getSession();
ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName());
log.info("Dumping entity views ...");
Path entityViewsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ENTITY_VIEWS,
new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, NAME, TYPE, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO},
new String[]{"", "", "", "", "", "", "default", "", "0", "0", "", ""},
"tb-entity-views");
log.info("Entity views dumped.");
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.2", SCHEMA_UPDATE_CQL);
loadCql(schemaUpdateFile);
log.info("Schema updated.");
log.info("Restoring entity views ...");
if (entityViewsDump != null) {
CassandraDbHelper.loadCf(ks, cluster.getSession(), ENTITY_VIEW,
new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, NAME, TYPE, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO}, entityViewsDump);
Files.deleteIfExists(entityViewsDump);
}
log.info("Entity views restored.");
break;
default: default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
} }

9
application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java

@ -45,14 +45,23 @@ public class DatabaseHelper {
public static final CSVFormat CSV_DUMP_FORMAT = CSVFormat.DEFAULT.withNullString("\\N"); public static final CSVFormat CSV_DUMP_FORMAT = CSVFormat.DEFAULT.withNullString("\\N");
public static final String DEVICE = "device"; public static final String DEVICE = "device";
public static final String ENTITY_ID = "entity_id";
public static final String TENANT_ID = "tenant_id"; public static final String TENANT_ID = "tenant_id";
public static final String ENTITY_TYPE = "entity_type";
public static final String CUSTOMER_ID = "customer_id"; public static final String CUSTOMER_ID = "customer_id";
public static final String SEARCH_TEXT = "search_text"; public static final String SEARCH_TEXT = "search_text";
public static final String ADDITIONAL_INFO = "additional_info"; public static final String ADDITIONAL_INFO = "additional_info";
public static final String ASSET = "asset"; public static final String ASSET = "asset";
public static final String DASHBOARD = "dashboard"; public static final String DASHBOARD = "dashboard";
public static final String ENTITY_VIEWS = "entity_views";
public static final String ENTITY_VIEW = "entity_view";
public static final String ID = "id"; public static final String ID = "id";
public static final String TITLE = "title"; public static final String TITLE = "title";
public static final String TYPE = "type";
public static final String NAME = "name";
public static final String KEYS = "keys";
public static final String START_TS = "start_ts";
public static final String END_TS = "end_ts";
public static final String ASSIGNED_CUSTOMERS = "assigned_customers"; public static final String ASSIGNED_CUSTOMERS = "assigned_customers";
public static final String CONFIGURATION = "configuration"; public static final String CONFIGURATION = "configuration";

34
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -31,14 +31,24 @@ import java.nio.file.Paths;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION; import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION;
import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID; import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;
import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD; import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;
import static org.thingsboard.server.service.install.DatabaseHelper.END_TS;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_ID;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_TYPE;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEW;
import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEWS;
import static org.thingsboard.server.service.install.DatabaseHelper.ID; import static org.thingsboard.server.service.install.DatabaseHelper.ID;
import static org.thingsboard.server.service.install.DatabaseHelper.KEYS;
import static org.thingsboard.server.service.install.DatabaseHelper.NAME;
import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT; import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;
import static org.thingsboard.server.service.install.DatabaseHelper.START_TS;
import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID; import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;
import static org.thingsboard.server.service.install.DatabaseHelper.TITLE; import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;
import static org.thingsboard.server.service.install.DatabaseHelper.TYPE;
@Service @Service
@Profile("install") @Profile("install")
@ -115,6 +125,30 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Schema updated."); log.info("Schema updated.");
} }
break; break;
case "2.1.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Dumping entity views ...");
Path entityViewsDump = SqlDbHelper.dumpTableIfExists(conn, ENTITY_VIEWS,
new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, TYPE, NAME, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO},
new String[]{"", "", "", "", "", "default", "", "", "0", "0", "", ""},
"tb-entity-views", true);
log.info("Entity views dumped.");
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.2", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
log.info("Schema updated.");
log.info("Restoring entity views ...");
if (entityViewsDump != null) {
SqlDbHelper.loadTable(conn, ENTITY_VIEW,
new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, TYPE, NAME, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO}, entityViewsDump, true);
Files.deleteIfExists(entityViewsDump);
}
log.info("Entity views restored.");
}
break;
default: default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);

4
application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java

@ -147,6 +147,8 @@ public class CassandraDbHelper {
str = new Double(row.getDouble(index)).toString(); str = new Double(row.getDouble(index)).toString();
} else if (type == DataType.cint()) { } else if (type == DataType.cint()) {
str = new Integer(row.getInt(index)).toString(); str = new Integer(row.getInt(index)).toString();
} else if (type == DataType.bigint()) {
str = new Long(row.getLong(index)).toString();
} else if (type == DataType.uuid()) { } else if (type == DataType.uuid()) {
str = row.getUUID(index).toString(); str = row.getUUID(index).toString();
} else if (type == DataType.timeuuid()) { } else if (type == DataType.timeuuid()) {
@ -193,6 +195,8 @@ public class CassandraDbHelper {
boundStatement.setDouble(column, Double.valueOf(value)); boundStatement.setDouble(column, Double.valueOf(value));
} else if (type == DataType.cint()) { } else if (type == DataType.cint()) {
boundStatement.setInt(column, Integer.valueOf(value)); boundStatement.setInt(column, Integer.valueOf(value));
} else if (type == DataType.bigint()) {
boundStatement.setLong(column, Long.valueOf(value));
} else if (type == DataType.uuid()) { } else if (type == DataType.uuid()) {
boundStatement.setUUID(column, UUID.fromString(value)); boundStatement.setUUID(column, UUID.fromString(value));
} else if (type == DataType.timeuuid()) { } else if (type == DataType.timeuuid()) {

97
application/src/main/resources/thingsboard.yml

@ -77,46 +77,10 @@ security:
# Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}"
# Device communication protocol parameters # Dashboard parameters
http: dashboard:
request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # Maximum allowed datapoints fetched by widgets
max_datapoints_limit: "${DASHBOARD_MAX_DATAPOINTS_LIMIT:50000}"
# MQTT server parameters
mqtt:
# Enable/disable mqtt transport protocol.
enabled: "${MQTT_ENABLED:false}"
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_LEASK_DETECTOR_LVL:DISABLED}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
# MQTT SSL configuration
ssl:
# Enable/disable SSL support
enabled: "${MQTT_SSL_ENABLED:false}"
# SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext
protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}"
# Path to the key store that holds the SSL certificate
key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
# Password used to access the key store
key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Type of the key store
key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
# CoAP server parameters
coap:
# Enable/disable coap transport protocol.
enabled: "${COAP_ENABLED:false}"
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
timeout: "${COAP_TIMEOUT:10000}"
#Quota parameters #Quota parameters
quota: quota:
@ -136,8 +100,8 @@ quota:
# Array of blacklist hosts # Array of blacklist hosts
blacklist: "${QUOTA_HOST_BLACKLIST:}" blacklist: "${QUOTA_HOST_BLACKLIST:}"
log: log:
topSize: 10 topSize: "${QUOTA_HOST_LOG_TOP_SIZE:10}"
intervalMin: 2 intervalMin: "${QUOTA_HOST_LOG_INTERVAL_MIN:2}"
rule: rule:
tenant: tenant:
# Max allowed number of API requests in interval for single tenant # Max allowed number of API requests in interval for single tenant
@ -153,10 +117,10 @@ quota:
# Array of whitelist tenants # Array of whitelist tenants
whitelist: "${QUOTA_TENANT_WHITELIST:}" whitelist: "${QUOTA_TENANT_WHITELIST:}"
# Array of blacklist tenants # Array of blacklist tenants
blacklist: "${QUOTA_HOST_BLACKLIST:}" blacklist: "${QUOTA_HOST_TENANT_BLACKLIST:}"
log: log:
topSize: 10 topSize: "${QUOTA_TENANT_LOG_TOP_SIZE:10}"
intervalMin: 2 intervalMin: "${QUOTA_TENANT_LOG_INTERVAL_MIN:2}"
database: database:
entities: entities:
@ -185,7 +149,8 @@ cassandra:
init_timeout_ms: "${CASSANDRA_CLUSTER_INIT_TIMEOUT_MS:300000}" init_timeout_ms: "${CASSANDRA_CLUSTER_INIT_TIMEOUT_MS:300000}"
# Specify cassandra claster initialization retry interval (if no hosts available during startup) # Specify cassandra claster initialization retry interval (if no hosts available during startup)
init_retry_interval_ms: "${CASSANDRA_CLUSTER_INIT_RETRY_INTERVAL_MS:3000}" init_retry_interval_ms: "${CASSANDRA_CLUSTER_INIT_RETRY_INTERVAL_MS:3000}"
max_requests_per_connection_local: "${CASSANDRA_MAX_REQUESTS_PER_CONNECTION_LOCAL:32768}"
max_requests_per_connection_remote: "${CASSANDRA_MAX_REQUESTS_PER_CONNECTION_REMOTE:32768}"
# Credential parameters # # Credential parameters #
credentials: "${CASSANDRA_USE_CREDENTIALS:false}" credentials: "${CASSANDRA_USE_CREDENTIALS:false}"
# Specify your username # Specify your username
@ -466,4 +431,42 @@ transport:
poll_interval: "${TB_RULE_ENGINE_POLL_INTERVAL_MS:25}" poll_interval: "${TB_RULE_ENGINE_POLL_INTERVAL_MS:25}"
auto_commit_interval: "${TB_RULE_ENGINE_AUTO_COMMIT_INTERVAL_MS:100}" auto_commit_interval: "${TB_RULE_ENGINE_AUTO_COMMIT_INTERVAL_MS:100}"
notifications: notifications:
topic: "${TB_TRANSPORT_NOTIFICATIONS_TOPIC:tb.transport.notifications}" topic: "${TB_TRANSPORT_NOTIFICATIONS_TOPIC:tb.transport.notifications}"
# Local HTTP transport parameters
http:
request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
# Local MQTT transport parameters
mqtt:
# Enable/disable mqtt transport protocol.
enabled: "${MQTT_ENABLED:false}"
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_LEASK_DETECTOR_LVL:DISABLED}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
# MQTT SSL configuration
ssl:
# Enable/disable SSL support
enabled: "${MQTT_SSL_ENABLED:false}"
# SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext
protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}"
# Path to the key store that holds the SSL certificate
key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
# Password used to access the key store
key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Type of the key store
key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
# Local CoAP transport parameters
coap:
# Enable/disable coap transport protocol.
enabled: "${COAP_ENABLED:false}"
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
timeout: "${COAP_TIMEOUT:10000}"

104
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -85,11 +85,11 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
testDevice = doPost("/api/device", device, Device.class); testDevice = doPost("/api/device", device, Device.class);
telemetry = new TelemetryEntityView( telemetry = new TelemetryEntityView(
Arrays.asList("109", "209", "309"), Arrays.asList("tsKey1", "tsKey2", "tsKey3"),
new AttributesEntityView( new AttributesEntityView(
Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4"), Arrays.asList("caKey1", "caKey2", "caKey3", "caKey4"),
Arrays.asList("saValue1", "saValue2", "saValue3", "saValue4"), Arrays.asList("saKey1", "saKey2", "saKey3", "saKey4"),
Arrays.asList("shValue1", "shValue2", "shValue3", "shValue4"))); Arrays.asList("shKey1", "shKey2", "shKey3", "shKey4")));
} }
@After @After
@ -144,7 +144,9 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
@Test @Test
public void testSaveEntityViewWithEmptyName() throws Exception { public void testSaveEntityViewWithEmptyName() throws Exception {
doPost("/api/entityView", new EntityView()) EntityView entityView = new EntityView();
entityView.setType("default");
doPost("/api/entityView", entityView)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Entity view name should be specified!"))); .andExpect(statusReason(containsString("Entity view name should be specified!")));
} }
@ -322,10 +324,10 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
@Test @Test
public void testTheCopyOfAttrsIntoTSForTheView() throws Exception { public void testTheCopyOfAttrsIntoTSForTheView() throws Exception {
Set<String> actualAttributesSet = Set<String> actualAttributesSet =
getAttributesByKeys("{\"caValue1\":\"value1\", \"caValue2\":true, \"caValue3\":42.0, \"caValue4\":73}"); getAttributesByKeys("{\"caKey1\":\"value1\", \"caKey2\":true, \"caKey3\":42.0, \"caKey4\":73}");
Set<String> expectedActualAttributesSet = Set<String> expectedActualAttributesSet =
new HashSet<>(Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4")); new HashSet<>(Arrays.asList("caKey1", "caKey2", "caKey3", "caKey4"));
assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet)); assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet));
Thread.sleep(1000); Thread.sleep(1000);
@ -333,18 +335,18 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
List<Map<String, Object>> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + List<Map<String, Object>> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() +
"/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class);
assertEquals("value1", getValue(values, "caValue1")); assertEquals("value1", getValue(values, "caKey1"));
assertEquals(true, getValue(values, "caValue2")); assertEquals(true, getValue(values, "caKey2"));
assertEquals(42.0, getValue(values, "caValue3")); assertEquals(42.0, getValue(values, "caKey3"));
assertEquals(73, getValue(values, "caValue4")); assertEquals(73, getValue(values, "caKey4"));
} }
@Test @Test
public void testTheCopyOfAttrsOutOfTSForTheView() throws Exception { public void testTheCopyOfAttrsOutOfTSForTheView() throws Exception {
Set<String> actualAttributesSet = Set<String> actualAttributesSet =
getAttributesByKeys("{\"caValue1\":\"value1\", \"caValue2\":true, \"caValue3\":42.0, \"caValue4\":73}"); getAttributesByKeys("{\"caKey1\":\"value1\", \"caKey2\":true, \"caKey3\":42.0, \"caKey4\":73}");
Set<String> expectedActualAttributesSet = new HashSet<>(Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4")); Set<String> expectedActualAttributesSet = new HashSet<>(Arrays.asList("caKey1", "caKey2", "caKey3", "caKey4"));
assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet)); assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet));
Thread.sleep(1000); Thread.sleep(1000);
@ -355,6 +357,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
view.setEntityId(testDevice.getId()); view.setEntityId(testDevice.getId());
view.setTenantId(savedTenant.getId()); view.setTenantId(savedTenant.getId());
view.setName("Test entity view"); view.setName("Test entity view");
view.setType("default");
view.setKeys(telemetry); view.setKeys(telemetry);
view.setStartTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") * 10); view.setStartTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") * 10);
view.setEndTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") / 10); view.setEndTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") / 10);
@ -365,6 +368,69 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
assertEquals(0, values.size()); assertEquals(0, values.size());
} }
@Test
public void testGetTelemetryWhenEntityViewTimeRangeInsideTimestampRange() throws Exception {
uploadTelemetry("{\"tsKey1\":\"value1\", \"tsKey2\":true, \"tsKey3\":40.0}");
Thread.sleep(1000);
long startTimeMs = System.currentTimeMillis();
uploadTelemetry("{\"tsKey1\":\"value2\", \"tsKey2\":false, \"tsKey3\":80.0}");
Thread.sleep(1000);
uploadTelemetry("{\"tsKey1\":\"value3\", \"tsKey2\":false, \"tsKey3\":120.0}");
long endTimeMs = System.currentTimeMillis();
uploadTelemetry("{\"tsKey1\":\"value4\", \"tsKey2\":true, \"tsKey3\":160.0}");
String deviceId = testDevice.getId().getId().toString();
Set<String> keys = getTelemetryKeys("DEVICE", deviceId);
Thread.sleep(1000);
EntityView view = createEntityView("Test entity view", startTimeMs, endTimeMs);
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
String entityViewId = savedView.getId().getId().toString();
Map<String, List<Map<String, String>>> expectedValues = getTelemetryValues("DEVICE", deviceId, keys, 0L, (startTimeMs + endTimeMs) / 2);
Assert.assertEquals(2, expectedValues.get("tsKey1").size());
Assert.assertEquals(2, expectedValues.get("tsKey2").size());
Assert.assertEquals(2, expectedValues.get("tsKey3").size());
Map<String, List<Map<String, String>>> actualValues = getTelemetryValues("ENTITY_VIEW", entityViewId, keys, 0L, (startTimeMs + endTimeMs) / 2);
Assert.assertEquals(1, actualValues.get("tsKey1").size());
Assert.assertEquals(1, actualValues.get("tsKey2").size());
Assert.assertEquals(1, actualValues.get("tsKey3").size());
}
private void uploadTelemetry(String strKvs) throws Exception {
String viewDeviceId = testDevice.getId().getId().toString();
DeviceCredentials deviceCredentials =
doGet("/api/device/" + viewDeviceId + "/credentials", DeviceCredentials.class);
assertEquals(testDevice.getId(), deviceCredentials.getDeviceId());
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
client.connect(options);
Thread.sleep(3000);
MqttMessage message = new MqttMessage();
message.setPayload(strKvs.getBytes());
client.publish("v1/devices/me/telemetry", message);
Thread.sleep(1000);
}
private Set<String> getTelemetryKeys(String type, String id) throws Exception {
return new HashSet<>(doGetAsync("/api/plugins/telemetry/" + type + "/" + id + "/keys/timeseries", List.class));
}
private Map<String, List<Map<String, String>>> getTelemetryValues(String type, String id, Set<String> keys, Long startTs, Long endTs) throws Exception {
return doGetAsync("/api/plugins/telemetry/" + type + "/" + id +
"/values/timeseries?keys=" + String.join(",", keys) + "&startTs=" + startTs + "&endTs=" + endTs, Map.class);
}
private Set<String> getAttributesByKeys(String stringKV) throws Exception { private Set<String> getAttributesByKeys(String stringKV) throws Exception {
String viewDeviceId = testDevice.getId().getId().toString(); String viewDeviceId = testDevice.getId().getId().toString();
DeviceCredentials deviceCredentials = DeviceCredentials deviceCredentials =
@ -387,7 +453,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
client.publish("v1/devices/me/attributes", message); client.publish("v1/devices/me/attributes", message);
Thread.sleep(1000); Thread.sleep(1000);
return new HashSet<>(doGetAsync("/api/plugins/telemetry/DEVICE/" + viewDeviceId + "/keys/attributes", List.class)); return new HashSet<>(doGetAsync("/api/plugins/telemetry/DEVICE/" + viewDeviceId + "/keys/attributes", List.class));
} }
private Object getValue(List<Map<String, Object>> values, String stringValue) { private Object getValue(List<Map<String, Object>> values, String stringValue) {
@ -398,12 +464,20 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
} }
private EntityView getNewSavedEntityView(String name) throws Exception { private EntityView getNewSavedEntityView(String name) throws Exception {
EntityView view = createEntityView(name, 0, 0);
return doPost("/api/entityView", view, EntityView.class);
}
private EntityView createEntityView(String name, long startTimeMs, long endTimeMs) {
EntityView view = new EntityView(); EntityView view = new EntityView();
view.setEntityId(testDevice.getId()); view.setEntityId(testDevice.getId());
view.setTenantId(savedTenant.getId()); view.setTenantId(savedTenant.getId());
view.setName(name); view.setName(name);
view.setType("default");
view.setKeys(telemetry); view.setKeys(telemetry);
return doPost("/api/entityView", view, EntityView.class); view.setStartTimeMs(startTimeMs);
view.setEndTimeMs(endTimeMs);
return view;
} }
private Customer getNewCustomer(String title) { private Customer getNewCustomer(String title) {

32
application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.mqtt.rpc; package org.thingsboard.server.mqtt.rpc;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
@ -23,19 +24,19 @@ import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.junit.After; import org.junit.*;
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.Device;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest;
import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.AccessValidator;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -101,13 +102,19 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
MqttConnectOptions options = new MqttConnectOptions(); MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken); options.setUserName(accessToken);
client.connect(options).waitForCompletion(); client.connect(options).waitForCompletion();
client.subscribe("v1/devices/me/rpc/request/+", 1);
client.setCallback(new TestMqttCallback(client)); 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());
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
Assert.assertTrue(StringUtils.isEmpty(result)); Assert.assertTrue(StringUtils.isEmpty(result));
latch.await(3, TimeUnit.SECONDS);
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
} }
@Test @Test
@ -156,7 +163,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
options.setUserName(accessToken); options.setUserName(accessToken);
client.connect(options).waitForCompletion(); client.connect(options).waitForCompletion();
client.subscribe("v1/devices/me/rpc/request/+", 1); client.subscribe("v1/devices/me/rpc/request/+", 1);
client.setCallback(new TestMqttCallback(client)); client.setCallback(new TestMqttCallback(client, new CountDownLatch(1)));
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();
@ -204,9 +211,16 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
private static class TestMqttCallback implements MqttCallback { private static class TestMqttCallback implements MqttCallback {
private final MqttAsyncClient client; private final MqttAsyncClient client;
private final CountDownLatch latch;
private Integer qoS;
TestMqttCallback(MqttAsyncClient client) { TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) {
this.client = client; this.client = client;
this.latch = latch;
}
int getQoS() {
return qoS;
} }
@Override @Override
@ -219,7 +233,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
MqttMessage message = new MqttMessage(); MqttMessage message = new MqttMessage();
String responseTopic = requestTopic.replace("request", "response"); String responseTopic = requestTopic.replace("request", "response");
message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes("UTF-8")); message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes("UTF-8"));
qoS = mqttMessage.getQos();
client.publish(responseTopic, message); client.publish(responseTopic, message);
latch.countDown();
} }
@Override @Override

66
application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java

@ -15,10 +15,9 @@
*/ */
package org.thingsboard.server.mqtt.telemetry; package org.thingsboard.server.mqtt.telemetry;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
@ -30,9 +29,12 @@ import org.thingsboard.server.dao.service.DaoNoSqlTest;
import java.net.URI; import java.net.URI;
import java.util.*; import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** /**
* @author Valerii Sosliuk * @author Valerii Sosliuk
@ -94,4 +96,62 @@ public abstract class AbstractMqttTelemetryIntegrationTest extends AbstractContr
assertEquals("3.0", values.get("key3").get(0).get("value")); assertEquals("3.0", values.get("key3").get(0).get("value"));
assertEquals("4", values.get("key4").get(0).get("value")); assertEquals("4", values.get("key4").get(0).get("value"));
} }
@Test
public void testMqttQoSLevel() throws Exception {
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
client.connect(options).waitForCompletion(3000);
CountDownLatch latch = new CountDownLatch(1);
TestMqttCallback callback = new TestMqttCallback(client, latch);
client.setCallback(callback);
client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value());
String payload = "{\"key\":\"value\"}";
String result = doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk());
latch.await(3, 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 Integer qoS;
private 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) {
}
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
payload = new String(mqttMessage.getPayload());
qoS = mqttMessage.getQos();
latch.countDown();
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
}
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -40,6 +40,7 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo<EntityViewId>
private TenantId tenantId; private TenantId tenantId;
private CustomerId customerId; private CustomerId customerId;
private String name; private String name;
private String type;
private TelemetryEntityView keys; private TelemetryEntityView keys;
private long startTimeMs; private long startTimeMs;
private long endTimeMs; private long endTimeMs;

1
common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java

@ -30,6 +30,7 @@ public class EntityViewSearchQuery {
private RelationsSearchParameters parameters; private RelationsSearchParameters parameters;
private String relationType; private String relationType;
private List<String> entityViewTypes;
public EntityRelationsQuery toEntitySearchQuery() { public EntityRelationsQuery toEntitySearchQuery() {
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();

69
dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java

@ -15,8 +15,13 @@
*/ */
package org.thingsboard.server.dao.entityview; package org.thingsboard.server.dao.entityview;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Statement; import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.mapping.Result;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -30,6 +35,8 @@ import org.thingsboard.server.dao.model.nosql.EntityViewEntity;
import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
import org.thingsboard.server.dao.util.NoSqlDao; import org.thingsboard.server.dao.util.NoSqlDao;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -39,14 +46,21 @@ import java.util.UUID;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select; import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_TYPE_CF;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_NAME; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_CF;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_CF;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_NAME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_NAME_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_PROPERTY;
/** /**
@ -82,13 +96,25 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit
public List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) { public List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) {
log.debug("Try to find entity views by tenantId [{}] and pageLink [{}]", tenantId, pageLink); log.debug("Try to find entity views by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
List<EntityViewEntity> entityViewEntities = List<EntityViewEntity> entityViewEntities =
findPageWithTextSearch(ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, findPageWithTextSearch(ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_CF,
Collections.singletonList(eq(TENANT_ID_PROPERTY, tenantId)), pageLink); Collections.singletonList(eq(TENANT_ID_PROPERTY, tenantId)), pageLink);
log.trace("Found entity views [{}] by tenantId [{}] and pageLink [{}]", log.trace("Found entity views [{}] by tenantId [{}] and pageLink [{}]",
entityViewEntities, tenantId, pageLink); entityViewEntities, tenantId, pageLink);
return DaoUtil.convertDataList(entityViewEntities); return DaoUtil.convertDataList(entityViewEntities);
} }
@Override
public List<EntityView> findEntityViewsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
log.debug("Try to find entity views by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink);
List<EntityViewEntity> entityViewEntities =
findPageWithTextSearch(ENTITY_VIEW_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_CF,
Arrays.asList(eq(ENTITY_VIEW_TYPE_PROPERTY, type),
eq(TENANT_ID_PROPERTY, tenantId)), pageLink);
log.trace("Found entity views [{}] by tenantId [{}], type [{}] and pageLink [{}]",
entityViewEntities, tenantId, type, pageLink);
return DaoUtil.convertDataList(entityViewEntities);
}
@Override @Override
public Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name) { public Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name) {
Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_NAME).where(); Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_NAME).where();
@ -110,6 +136,19 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit
return DaoUtil.convertDataList(entityViewEntities); return DaoUtil.convertDataList(entityViewEntities);
} }
@Override
public List<EntityView> findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
log.debug("Try to find entity views by tenantId [{}], customerId[{}], type [{}] and pageLink [{}]",
tenantId, customerId, type, pageLink);
List<EntityViewEntity> entityViewEntities = findPageWithTextSearch(
ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_TYPE_CF,
Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type), eq(CUSTOMER_ID_PROPERTY, customerId), eq(TENANT_ID_PROPERTY, tenantId)),
pageLink);
log.trace("Found find entity views [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]",
entityViewEntities, tenantId, customerId, type, pageLink);
return DaoUtil.convertDataList(entityViewEntities);
}
@Override @Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) {
log.debug("Try to find entity views by tenantId [{}] and entityId [{}]", tenantId, entityId); log.debug("Try to find entity views by tenantId [{}] and entityId [{}]", tenantId, entityId);
@ -118,4 +157,30 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit
query.and(eq(ENTITY_ID_COLUMN, entityId)); query.and(eq(ENTITY_ID_COLUMN, entityId));
return findListByStatementAsync(query); return findListByStatementAsync(query);
} }
@Override
public ListenableFuture<List<EntitySubtype>> findTenantEntityViewTypesAsync(UUID tenantId) {
Select select = select().from(ENTITY_SUBTYPE_COLUMN_FAMILY_NAME);
Select.Where query = select.where();
query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ENTITY_VIEW));
query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = executeAsyncRead(query);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
@Nullable
@Override
public List<EntitySubtype> apply(@Nullable ResultSet resultSet) {
Result<EntitySubtypeEntity> result = cluster.getMapper(EntitySubtypeEntity.class).map(resultSet);
if (result != null) {
List<EntitySubtype> entitySubtypes = new ArrayList<>();
result.all().forEach((entitySubtypeEntity) ->
entitySubtypes.add(entitySubtypeEntity.toEntitySubtype())
);
return entitySubtypes;
} else {
return Collections.emptyList();
}
}
});
}
} }

32
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.entityview;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
@ -47,6 +48,16 @@ public interface EntityViewDao extends Dao<EntityView> {
*/ */
List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink); List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink);
/**
* Find entity views by tenantId, type and page link.
*
* @param tenantId the tenantId
* @param type the type
* @param pageLink the page link
* @return the list of entity view objects
*/
List<EntityView> findEntityViewsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink);
/** /**
* Find entity views by tenantId and entity view name. * Find entity views by tenantId and entity view name.
* *
@ -68,6 +79,27 @@ public interface EntityViewDao extends Dao<EntityView> {
UUID customerId, UUID customerId,
TextPageLink pageLink); TextPageLink pageLink);
/**
* Find entity views by tenantId, customerId, type and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param type the type
* @param pageLink the page link
* @return the list of entity view objects
*/
List<EntityView> findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId,
UUID customerId,
String type,
TextPageLink pageLink);
ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId); ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId);
/**
* Find tenants entity view types.
*
* @return the list of tenant entity view type objects
*/
ListenableFuture<List<EntitySubtype>> findTenantEntityViewTypesAsync(UUID tenantId);
} }

7
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.entityview; package org.thingsboard.server.dao.entityview;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -44,8 +45,12 @@ public interface EntityViewService {
TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink); TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink);
TextPageData<EntityView> findEntityViewByTenantIdAndType(TenantId tenantId, TextPageLink pageLink, String type);
TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, TextPageLink pageLink, String type);
ListenableFuture<List<EntityView>> findEntityViewsByQuery(EntityViewSearchQuery query); ListenableFuture<List<EntityView>> findEntityViewsByQuery(EntityViewSearchQuery query);
ListenableFuture<EntityView> findEntityViewByIdAsync(EntityViewId entityViewId); ListenableFuture<EntityView> findEntityViewByIdAsync(EntityViewId entityViewId);
@ -55,4 +60,6 @@ public interface EntityViewService {
void deleteEntityView(EntityViewId entityViewId); void deleteEntityView(EntityViewId entityViewId);
void deleteEntityViewsByTenantId(TenantId tenantId); void deleteEntityViewsByTenantId(TenantId tenantId);
ListenableFuture<List<EntitySubtype>> findEntityViewTypesByTenantId(TenantId tenantId);
} }

54
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.entityview; package org.thingsboard.server.dao.entityview;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -29,6 +30,8 @@ import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
@ -54,6 +57,8 @@ import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -63,6 +68,7 @@ import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validateString;
/** /**
* Created by Victor Basanets on 8/28/2017. * Created by Victor Basanets on 8/28/2017.
@ -157,6 +163,16 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
return new TextPageData<>(entityViews, pageLink); return new TextPageData<>(entityViews, pageLink);
} }
@Override
public TextPageData<EntityView> findEntityViewByTenantIdAndType(TenantId tenantId, TextPageLink pageLink, String type) {
log.trace("Executing findEntityViewByTenantIdAndType, tenantId [{}], pageLink [{}], type [{}]", tenantId, pageLink, type);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink);
validateString(type, "Incorrect type " + type);
List<EntityView> entityViews = entityViewDao.findEntityViewsByTenantIdAndType(tenantId.getId(), type, pageLink);
return new TextPageData<>(entityViews, pageLink);
}
@Override @Override
public TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, public TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId,
TextPageLink pageLink) { TextPageLink pageLink) {
@ -170,6 +186,19 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
return new TextPageData<>(entityViews, pageLink); return new TextPageData<>(entityViews, pageLink);
} }
@Override
public TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, TextPageLink pageLink, String type) {
log.trace("Executing findEntityViewsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}]," +
" pageLink [{}], type [{}]", tenantId, customerId, pageLink, type);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink);
validateString(type, "Incorrect type " + type);
List<EntityView> entityViews = entityViewDao.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId.getId(),
customerId.getId(), type, pageLink);
return new TextPageData<>(entityViews, pageLink);
}
@Override @Override
public ListenableFuture<List<EntityView>> findEntityViewsByQuery(EntityViewSearchQuery query) { public ListenableFuture<List<EntityView>> findEntityViewsByQuery(EntityViewSearchQuery query) {
ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery()); ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
@ -184,6 +213,15 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
} }
return Futures.successfulAsList(futures); return Futures.successfulAsList(futures);
}); });
entityViews = Futures.transform(entityViews, new Function<List<EntityView>, List<EntityView>>() {
@Nullable
@Override
public List<EntityView> apply(@Nullable List<EntityView> entityViewList) {
return entityViewList == null ? Collections.emptyList() : entityViewList.stream().filter(entityView -> query.getEntityViewTypes().contains(entityView.getType())).collect(Collectors.toList());
}
});
return entityViews; return entityViews;
} }
@ -216,6 +254,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
public void onSuccess(@Nullable List<EntityView> result) { public void onSuccess(@Nullable List<EntityView> result) {
cache.putIfAbsent(tenantIdAndEntityId, result); cache.putIfAbsent(tenantIdAndEntityId, result);
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("Error while finding entity views by tenantId and entityId", t); log.error("Error while finding entity views by tenantId and entityId", t);
@ -243,6 +282,18 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
tenantEntityViewRemover.removeEntities(tenantId); tenantEntityViewRemover.removeEntities(tenantId);
} }
@Override
public ListenableFuture<List<EntitySubtype>> findEntityViewTypesByTenantId(TenantId tenantId) {
log.trace("Executing findEntityViewTypesByTenantId, tenantId [{}]", tenantId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
ListenableFuture<List<EntitySubtype>> tenantEntityViewTypes = entityViewDao.findTenantEntityViewTypesAsync(tenantId.getId());
return Futures.transform(tenantEntityViewTypes,
entityViewTypes -> {
entityViewTypes.sort(Comparator.comparing(EntitySubtype::getType));
return entityViewTypes;
});
}
private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys) { private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys) {
if (keys != null && !keys.isEmpty()) { if (keys != null && !keys.isEmpty()) {
ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys); ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys);
@ -296,6 +347,9 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
@Override @Override
protected void validateDataImpl(EntityView entityView) { protected void validateDataImpl(EntityView entityView) {
if (StringUtils.isEmpty(entityView.getType())) {
throw new DataValidationException("Entity View type should be specified!");
}
if (StringUtils.isEmpty(entityView.getName())) { if (StringUtils.isEmpty(entityView.getName())) {
throw new DataValidationException("Entity view name should be specified!"); throw new DataValidationException("Entity view name should be specified!");
} }

7
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -145,18 +145,21 @@ public class ModelConstants {
/** /**
* Cassandra entityView constants. * Cassandra entityView constants.
*/ */
public static final String ENTITY_VIEW_TABLE_FAMILY_NAME = "entity_views"; public static final String ENTITY_VIEW_TABLE_FAMILY_NAME = "entity_view";
public static final String ENTITY_VIEW_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN; public static final String ENTITY_VIEW_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN;
public static final String ENTITY_VIEW_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String ENTITY_VIEW_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String ENTITY_VIEW_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String ENTITY_VIEW_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String ENTITY_VIEW_NAME_PROPERTY = DEVICE_NAME_PROPERTY; public static final String ENTITY_VIEW_NAME_PROPERTY = DEVICE_NAME_PROPERTY;
public static final String ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF = "entity_view_by_tenant_and_customer"; public static final String ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF = "entity_view_by_tenant_and_customer";
public static final String ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_TYPE_CF = "entity_view_by_tenant_and_customer_and_type";
public static final String ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF = "entity_view_by_tenant_and_entity_id"; public static final String ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF = "entity_view_by_tenant_and_entity_id";
public static final String ENTITY_VIEW_KEYS_PROPERTY = "keys"; public static final String ENTITY_VIEW_KEYS_PROPERTY = "keys";
public static final String ENTITY_VIEW_TYPE_PROPERTY = "type";
public static final String ENTITY_VIEW_START_TS_PROPERTY = "start_ts"; public static final String ENTITY_VIEW_START_TS_PROPERTY = "start_ts";
public static final String ENTITY_VIEW_END_TS_PROPERTY = "end_ts"; public static final String ENTITY_VIEW_END_TS_PROPERTY = "end_ts";
public static final String ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "entity_view_by_tenant_and_search_text"; public static final String ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_CF = "entity_view_by_tenant_and_search_text";
public static final String ENTITY_VIEW_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_CF = "entity_view_by_tenant_by_type_and_search_text";
public static final String ENTITY_VIEW_BY_TENANT_AND_NAME = "entity_view_by_tenant_and_name"; public static final String ENTITY_VIEW_BY_TENANT_AND_NAME = "entity_view_by_tenant_and_name";
/** /**

7
dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java

@ -41,6 +41,7 @@ import javax.persistence.Enumerated;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@ -71,6 +72,10 @@ public class EntityViewEntity implements SearchTextEntity<EntityView> {
@Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY)
private UUID customerId; private UUID customerId;
@PartitionKey(value = 3)
@Column(name = DEVICE_TYPE_PROPERTY)
private String type;
@Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY)
private UUID entityId; private UUID entityId;
@ -113,6 +118,7 @@ public class EntityViewEntity implements SearchTextEntity<EntityView> {
if (entityView.getCustomerId() != null) { if (entityView.getCustomerId() != null) {
this.customerId = entityView.getCustomerId().getId(); this.customerId = entityView.getCustomerId().getId();
} }
this.type = entityView.getType();
this.name = entityView.getName(); this.name = entityView.getName();
try { try {
this.keys = mapper.writeValueAsString(entityView.getKeys()); this.keys = mapper.writeValueAsString(entityView.getKeys());
@ -143,6 +149,7 @@ public class EntityViewEntity implements SearchTextEntity<EntityView> {
if (customerId != null) { if (customerId != null) {
entityView.setCustomerId(new CustomerId(customerId)); entityView.setCustomerId(new CustomerId(customerId));
} }
entityView.setType(type);
entityView.setName(name); entityView.setName(name);
try { try {
entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class));

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java

@ -69,6 +69,9 @@ public class EntityViewEntity extends BaseSqlEntity<EntityView> implements Searc
@Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY)
private String customerId; private String customerId;
@Column(name = ModelConstants.DEVICE_TYPE_PROPERTY)
private String type;
@Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY) @Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY)
private String name; private String name;
@ -108,6 +111,7 @@ public class EntityViewEntity extends BaseSqlEntity<EntityView> implements Searc
if (entityView.getCustomerId() != null) { if (entityView.getCustomerId() != null) {
this.customerId = toString(entityView.getCustomerId().getId()); this.customerId = toString(entityView.getCustomerId().getId());
} }
this.type = entityView.getType();
this.name = entityView.getName(); this.name = entityView.getName();
try { try {
this.keys = mapper.writeValueAsString(entityView.getKeys()); this.keys = mapper.writeValueAsString(entityView.getKeys());
@ -144,6 +148,7 @@ public class EntityViewEntity extends BaseSqlEntity<EntityView> implements Searc
if (customerId != null) { if (customerId != null) {
entityView.setCustomerId(new CustomerId(toUUID(customerId))); entityView.setCustomerId(new CustomerId(toUUID(customerId)));
} }
entityView.setType(type);
entityView.setName(name); entityView.setName(name);
try { try {
entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class));

41
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -19,8 +19,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewEntity;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
@ -36,21 +34,46 @@ public interface EntityViewRepository extends CrudRepository<EntityViewEntity, S
"AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " +
"AND e.id > :idOffset ORDER BY e.id") "AND e.id > :idOffset ORDER BY e.id")
List<EntityViewEntity> findByTenantId(@Param("tenantId") String tenantId, List<EntityViewEntity> findByTenantId(@Param("tenantId") String tenantId,
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
@Param("idOffset") String idOffset, @Param("idOffset") String idOffset,
Pageable pageable); Pageable pageable);
@Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " +
"AND e.type = :type " +
"AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " +
"AND e.id > :idOffset ORDER BY e.id")
List<EntityViewEntity> findByTenantIdAndType(@Param("tenantId") String tenantId,
@Param("type") String type,
@Param("textSearch") String textSearch,
@Param("idOffset") String idOffset,
Pageable pageable);
@Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " +
"AND e.customerId = :customerId " + "AND e.customerId = :customerId " +
"AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " +
"AND e.id > :idOffset ORDER BY e.id") "AND e.id > :idOffset ORDER BY e.id")
List<EntityViewEntity> findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, List<EntityViewEntity> findByTenantIdAndCustomerId(@Param("tenantId") String tenantId,
@Param("customerId") String customerId, @Param("customerId") String customerId,
@Param("searchText") String searchText, @Param("searchText") String searchText,
@Param("idOffset") String idOffset, @Param("idOffset") String idOffset,
Pageable pageable); Pageable pageable);
@Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " +
"AND e.customerId = :customerId " +
"AND e.type = :type " +
"AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " +
"AND e.id > :idOffset ORDER BY e.id")
List<EntityViewEntity> findByTenantIdAndCustomerIdAndType(@Param("tenantId") String tenantId,
@Param("customerId") String customerId,
@Param("type") String type,
@Param("searchText") String searchText,
@Param("idOffset") String idOffset,
Pageable pageable);
EntityViewEntity findByTenantIdAndName(String tenantId, String name); EntityViewEntity findByTenantIdAndName(String tenantId, String name);
List<EntityViewEntity> findAllByTenantIdAndEntityId(String tenantId, String entityId); List<EntityViewEntity> findAllByTenantIdAndEntityId(String tenantId, String entityId);
@Query("SELECT DISTINCT ev.type FROM EntityViewEntity ev WHERE ev.tenantId = :tenantId")
List<String> findTenantEntityViewTypes(@Param("tenantId") String tenantId);
} }

42
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
@ -41,7 +40,6 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
/** /**
@ -75,6 +73,17 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao<EntityViewEntity,
new PageRequest(0, pageLink.getLimit()))); new PageRequest(0, pageLink.getLimit())));
} }
@Override
public List<EntityView> findEntityViewsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
return DaoUtil.convertDataList(
entityViewRepository.findByTenantIdAndType(
fromTimeUUID(tenantId),
type,
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())));
}
@Override @Override
public Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name) { public Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name) {
return Optional.ofNullable( return Optional.ofNullable(
@ -95,9 +104,38 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao<EntityViewEntity,
)); ));
} }
@Override
public List<EntityView> findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
return DaoUtil.convertDataList(
entityViewRepository.findByTenantIdAndCustomerIdAndType(
fromTimeUUID(tenantId),
fromTimeUUID(customerId),
type,
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())
));
}
@Override @Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) {
return service.submit(() -> DaoUtil.convertDataList( return service.submit(() -> DaoUtil.convertDataList(
entityViewRepository.findAllByTenantIdAndEntityId(UUIDConverter.fromTimeUUID(tenantId), UUIDConverter.fromTimeUUID(entityId)))); entityViewRepository.findAllByTenantIdAndEntityId(UUIDConverter.fromTimeUUID(tenantId), UUIDConverter.fromTimeUUID(entityId))));
} }
@Override
public ListenableFuture<List<EntitySubtype>> findTenantEntityViewTypesAsync(UUID tenantId) {
return service.submit(() -> convertTenantEntityViewTypesToDto(tenantId, entityViewRepository.findTenantEntityViewTypes(fromTimeUUID(tenantId))));
}
private List<EntitySubtype> convertTenantEntityViewTypesToDto(UUID tenantId, List<String> types) {
List<EntitySubtype> list = Collections.emptyList();
if (types != null && !types.isEmpty()) {
list = new ArrayList<>();
for (String type : types) {
list.add(new EntitySubtype(new TenantId(tenantId), EntityType.ENTITY_VIEW, type));
}
}
return list;
}
} }

23
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -87,7 +87,11 @@ public class BaseTimeseriesService implements TimeseriesService {
.map(key -> new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), entityView.getEndTimeMs(), 1, "ASC")) .map(key -> new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), entityView.getEndTimeMs(), 1, "ASC"))
.collect(Collectors.toList()); .collect(Collectors.toList());
return timeseriesDao.findAllAsync(entityView.getEntityId(), updateQueriesForEntityView(entityView, queries)); if (queries.size() > 0) {
return timeseriesDao.findAllAsync(entityView.getEntityId(), queries);
} else {
return Futures.immediateFuture(new ArrayList<>());
}
} }
keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityId, key))); keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityId, key)));
return Futures.allAsList(futures); return Futures.allAsList(futures);
@ -133,11 +137,20 @@ public class BaseTimeseriesService implements TimeseriesService {
private List<ReadTsKvQuery> updateQueriesForEntityView(EntityView entityView, List<ReadTsKvQuery> queries) { private List<ReadTsKvQuery> updateQueriesForEntityView(EntityView entityView, List<ReadTsKvQuery> queries) {
return queries.stream().map(query -> { return queries.stream().map(query -> {
long startTs = entityView.getStartTimeMs() == 0 ? query.getStartTs() : entityView.getStartTimeMs(); long startTs;
long endTs = entityView.getEndTimeMs() == 0 ? query.getEndTs() : entityView.getEndTimeMs(); if (entityView.getStartTimeMs() != 0 && entityView.getStartTimeMs() > query.getStartTs()) {
startTs = entityView.getStartTimeMs();
} else {
startTs = query.getStartTs();
}
return startTs <= query.getStartTs() && endTs >= query.getEndTs() ? query : long endTs;
new BaseReadTsKvQuery(query.getKey(), startTs, endTs, query.getInterval(), query.getLimit(), query.getAggregation()); if (entityView.getEndTimeMs() != 0 && entityView.getEndTimeMs() < query.getEndTs()) {
endTs = entityView.getEndTimeMs();
} else {
endTs = query.getEndTs();
}
return new BaseReadTsKvQuery(query.getKey(), startTs, endTs, query.getInterval(), query.getLimit(), query.getAggregation());
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }

49
dao/src/main/resources/cassandra/schema-entities.cql

@ -624,61 +624,90 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
PRIMARY KEY (id) PRIMARY KEY (id)
); );
CREATE TABLE IF NOT EXISTS thingsboard.entity_views ( CREATE TABLE IF NOT EXISTS thingsboard.entity_view (
id timeuuid, id timeuuid,
entity_id timeuuid, entity_id timeuuid,
entity_type text, entity_type text,
tenant_id timeuuid, tenant_id timeuuid,
customer_id timeuuid, customer_id timeuuid,
name text, name text,
type text,
keys text, keys text,
start_ts bigint, start_ts bigint,
end_ts bigint, end_ts bigint,
search_text text, search_text text,
additional_info text, additional_info text,
PRIMARY KEY (id, entity_id, tenant_id, customer_id) PRIMARY KEY (id, entity_id, tenant_id, customer_id, type)
); );
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS
SELECT * SELECT *
from thingsboard.entity_views from thingsboard.entity_view
WHERE tenant_id IS NOT NULL WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL AND entity_id IS NOT NULL
AND customer_id IS NOT NULL AND customer_id IS NOT NULL
AND type IS NOT NULL
AND name IS NOT NULL AND name IS NOT NULL
AND id IS NOT NULL AND id IS NOT NULL
PRIMARY KEY (tenant_id, name, id, customer_id, entity_id) PRIMARY KEY (tenant_id, name, id, customer_id, entity_id, type)
WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC); WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS
SELECT * SELECT *
from thingsboard.entity_views from thingsboard.entity_view
WHERE tenant_id IS NOT NULL WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL AND entity_id IS NOT NULL
AND customer_id IS NOT NULL AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL AND search_text IS NOT NULL
AND id IS NOT NULL AND id IS NOT NULL
PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id) PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id, type)
WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC); WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_by_type_and_search_text AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, type, search_text, id, customer_id, entity_id)
WITH CLUSTERING ORDER BY (type ASC, search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS
SELECT * SELECT *
from thingsboard.entity_views from thingsboard.entity_view
WHERE tenant_id IS NOT NULL WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL AND customer_id IS NOT NULL
AND entity_id IS NOT NULL AND entity_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL AND search_text IS NOT NULL
AND id IS NOT NULL AND id IS NOT NULL
PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id) PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id, type)
WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC); WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer_and_type AS
SELECT *
from thingsboard.entity_view
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND entity_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, type, customer_id, search_text, id, entity_id)
WITH CLUSTERING ORDER BY (type ASC, customer_id DESC, search_text ASC, id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_entity_id AS CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_entity_id AS
SELECT * SELECT *
from thingsboard.entity_views from thingsboard.entity_view
WHERE tenant_id IS NOT NULL WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL AND customer_id IS NOT NULL
AND entity_id IS NOT NULL AND entity_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL AND search_text IS NOT NULL
AND id IS NOT NULL AND id IS NOT NULL
PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id) PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id, type)
WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC);

240
dao/src/main/resources/cassandra/system-data.cql

File diff suppressed because one or more lines are too long

3
dao/src/main/resources/sql/schema-entities.sql

@ -228,12 +228,13 @@ CREATE TABLE IF NOT EXISTS rule_node (
search_text varchar(255) search_text varchar(255)
); );
CREATE TABLE IF NOT EXISTS entity_views ( CREATE TABLE IF NOT EXISTS entity_view (
id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
entity_id varchar(31), entity_id varchar(31),
entity_type varchar(255), entity_type varchar(255),
tenant_id varchar(31), tenant_id varchar(31),
customer_id varchar(31), customer_id varchar(31),
type varchar(255),
name varchar(255), name varchar(255),
keys varchar(255), keys varchar(255),
start_ts bigint, start_ts bigint,

2
dao/src/test/resources/sql/drop-all-tables.sql

@ -19,4 +19,4 @@ DROP TABLE IF EXISTS widget_type;
DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS widgets_bundle;
DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_node;
DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS rule_chain;
DROP TABLE IF EXISTS entity_views; DROP TABLE IF EXISTS entity_view;

5
msa/docker/.env

@ -1,5 +1,10 @@
DOCKER_REPO=local-maven-build DOCKER_REPO=local-maven-build
JS_EXECUTOR_DOCKER_NAME=tb-js-executor
TB_NODE_DOCKER_NAME=tb-node
WEB_UI_DOCKER_NAME=tb-web-ui
TB_VERSION=2.2.0-SNAPSHOT TB_VERSION=2.2.0-SNAPSHOT
KAFKA_TOPICS=js.eval.requests:100:1 KAFKA_TOPICS=js.eval.requests:100:1

16
msa/docker/docker-compose.yml

@ -15,7 +15,7 @@
# #
version: '2' version: '2.2'
services: services:
zookeeper: zookeeper:
@ -40,7 +40,8 @@ services:
- zookeeper - zookeeper
tb-js-executor: tb-js-executor:
restart: always restart: always
image: "${DOCKER_REPO}/tb-js-executor:${TB_VERSION}" image: "${DOCKER_REPO}/${JS_EXECUTOR_DOCKER_NAME}:${TB_VERSION}"
scale: 20
environment: environment:
TB_KAFKA_SERVERS: kafka:9092 TB_KAFKA_SERVERS: kafka:9092
env_file: env_file:
@ -49,11 +50,16 @@ services:
- kafka - kafka
tb: tb:
restart: always restart: always
image: "${DOCKER_REPO}/tb-node:${TB_VERSION}" image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}"
ports: ports:
- "8080" - "8080"
- "1883:1883" - "1883:1883"
- "5683:5683/udp" - "5683:5683/udp"
logging:
driver: "json-file"
options:
max-size: "200m"
max-file: "30"
env_file: env_file:
- tb-node.env - tb-node.env
environment: environment:
@ -68,7 +74,7 @@ services:
- kafka - kafka
tb-web-ui1: tb-web-ui1:
restart: always restart: always
image: "${DOCKER_REPO}/tb-web-ui:${TB_VERSION}" image: "${DOCKER_REPO}/${WEB_UI_DOCKER_NAME}:${TB_VERSION}"
ports: ports:
- "8080" - "8080"
environment: environment:
@ -78,7 +84,7 @@ services:
- tb-web-ui.env - tb-web-ui.env
tb-web-ui2: tb-web-ui2:
restart: always restart: always
image: "${DOCKER_REPO}/tb-web-ui:${TB_VERSION}" image: "${DOCKER_REPO}/${WEB_UI_DOCKER_NAME}:${TB_VERSION}"
ports: ports:
- "8080" - "8080"
environment: environment:

2
msa/docker/docker-start-services.sh

@ -17,4 +17,4 @@
./check-dirs.sh ./check-dirs.sh
docker-compose up -d --scale tb-js-executor=20 docker-compose up -d

1
msa/docker/docker-update-service.sh

@ -15,4 +15,5 @@
# limitations under the License. # limitations under the License.
# #
docker-compose pull $@
docker-compose up -d --no-deps --build $@ docker-compose up -d --no-deps --build $@

3
msa/docker/haproxy/config/haproxy.cfg

@ -35,8 +35,9 @@ frontend http-in
reqadd X-Forwarded-Proto:\ http reqadd X-Forwarded-Proto:\ http
acl transport_http_acl path_beg /api/v1/
acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/
redirect scheme https if !letsencrypt_http_acl redirect scheme https if !letsencrypt_http_acl !transport_http_acl
use_backend letsencrypt_http if letsencrypt_http_acl use_backend letsencrypt_http if letsencrypt_http_acl
default_backend tb-web-backend default_backend tb-web-backend

2
msa/docker/tb-node/conf/thingsboard.conf

@ -19,6 +19,6 @@ export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/gc.log -XX:+IgnoreUnre
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
export LOG_FILENAME=thingsboard.out export LOG_FILENAME=thingsboard.out
export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions

2
msa/js-executor/docker/Dockerfile

@ -23,4 +23,6 @@ RUN chmod a+x /tmp/*.sh \
RUN dpkg -i /tmp/${pkg.name}.deb RUN dpkg -i /tmp/${pkg.name}.deb
RUN update-rc.d ${pkg.name} disable
CMD ["start-js-executor.sh"] CMD ["start-js-executor.sh"]

3
msa/js-executor/pom.xml

@ -35,6 +35,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir> <main.dir>${basedir}/../..</main.dir>
<pkg.name>tb-js-executor</pkg.name> <pkg.name>tb-js-executor</pkg.name>
<docker.name>tb-js-executor</docker.name>
<pkg.user>thingsboard</pkg.user> <pkg.user>thingsboard</pkg.user>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder> <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
@ -290,7 +291,7 @@
</executions> </executions>
<configuration> <configuration>
<skip>${dockerfile.skip}</skip> <skip>${dockerfile.skip}</skip>
<repository>${docker.repo}/${pkg.name}</repository> <repository>${docker.repo}/${docker.name}</repository>
<tag>${project.version}</tag> <tag>${project.version}</tag>
<verbose>true</verbose> <verbose>true</verbose>
<googleContainerRegistryEnabled>false</googleContainerRegistryEnabled> <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>

2
msa/js-executor/src/main/scripts/control/deb/postinst

@ -2,5 +2,5 @@
chown -R ${pkg.user}: ${pkg.logFolder} chown -R ${pkg.user}: ${pkg.logFolder}
chown -R ${pkg.user}: ${pkg.installFolder} chown -R ${pkg.user}: ${pkg.installFolder}
# update-rc.d ${pkg.name} defaults update-rc.d ${pkg.name} defaults

3
msa/tb-node/pom.xml

@ -35,6 +35,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir> <main.dir>${basedir}/../..</main.dir>
<pkg.name>thingsboard</pkg.name> <pkg.name>thingsboard</pkg.name>
<docker.name>tb-node</docker.name>
<pkg.user>thingsboard</pkg.user> <pkg.user>thingsboard</pkg.user>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder> <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
@ -114,7 +115,7 @@
</executions> </executions>
<configuration> <configuration>
<skip>${dockerfile.skip}</skip> <skip>${dockerfile.skip}</skip>
<repository>${docker.repo}/tb-node</repository> <repository>${docker.repo}/${docker.name}</repository>
<tag>${project.version}</tag> <tag>${project.version}</tag>
<verbose>true</verbose> <verbose>true</verbose>
<googleContainerRegistryEnabled>false</googleContainerRegistryEnabled> <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>

2
msa/web-ui/docker/Dockerfile

@ -23,4 +23,6 @@ RUN chmod a+x /tmp/*.sh \
RUN dpkg -i /tmp/${pkg.name}.deb RUN dpkg -i /tmp/${pkg.name}.deb
RUN update-rc.d ${pkg.name} disable
CMD ["start-web-ui.sh"] CMD ["start-web-ui.sh"]

3
msa/web-ui/pom.xml

@ -35,6 +35,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir> <main.dir>${basedir}/../..</main.dir>
<pkg.name>tb-web-ui</pkg.name> <pkg.name>tb-web-ui</pkg.name>
<docker.name>tb-web-ui</docker.name>
<pkg.user>thingsboard</pkg.user> <pkg.user>thingsboard</pkg.user>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder> <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
@ -314,7 +315,7 @@
</executions> </executions>
<configuration> <configuration>
<skip>${dockerfile.skip}</skip> <skip>${dockerfile.skip}</skip>
<repository>${docker.repo}/${pkg.name}</repository> <repository>${docker.repo}/${docker.name}</repository>
<tag>${project.version}</tag> <tag>${project.version}</tag>
<verbose>true</verbose> <verbose>true</verbose>
<googleContainerRegistryEnabled>false</googleContainerRegistryEnabled> <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>

2
msa/web-ui/src/main/scripts/control/deb/postinst

@ -2,5 +2,5 @@
chown -R ${pkg.user}: ${pkg.logFolder} chown -R ${pkg.user}: ${pkg.logFolder}
chown -R ${pkg.user}: ${pkg.installFolder} chown -R ${pkg.user}: ${pkg.installFolder}
# update-rc.d ${pkg.name} defaults update-rc.d ${pkg.name} defaults

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java

@ -48,7 +48,7 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
name = "copy attributes", name = "copy to view",
configClazz = EmptyNodeConfiguration.class, configClazz = EmptyNodeConfiguration.class,
nodeDescription = "Copy attributes from asset/device to entity view and changes message originator to related entity view", nodeDescription = "Copy attributes from asset/device to entity view and changes message originator to related entity view",
nodeDetails = "Copy attributes from asset/device to related entity view according to entity view configuration. \n " + nodeDetails = "Copy attributes from asset/device to related entity view according to entity view configuration. \n " +

2
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java

@ -37,7 +37,7 @@ import java.net.InetSocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@Service("CoapTransportService") @Service("CoapTransportService")
@ConditionalOnProperty(prefix = "coap", value = "enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnProperty(prefix = "transport.coap", value = "enabled", havingValue = "true")
@Slf4j @Slf4j
public class CoapTransportService { public class CoapTransportService {

2
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.http;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -34,6 +35,7 @@ import javax.servlet.http.HttpServletRequest;
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@RestController @RestController
@ConditionalOnProperty(prefix = "transport.http", value = "enabled", havingValue = "true")
@RequestMapping("/api/v1") @RequestMapping("/api/v1")
@Slf4j @Slf4j
public class DeviceApiController { public class DeviceApiController {

4
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java

@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -41,6 +42,7 @@ import java.util.concurrent.Executors;
* Created by ashvayka on 04.10.18. * Created by ashvayka on 04.10.18.
*/ */
@Slf4j @Slf4j
@ConditionalOnProperty(prefix = "transport.mqtt", value = "enabled", havingValue = "true", matchIfMissing = true)
@Component @Component
@Data @Data
public class MqttTransportContext { public class MqttTransportContext {
@ -59,7 +61,7 @@ public class MqttTransportContext {
@Autowired @Autowired
private MqttTransportAdaptor adaptor; private MqttTransportAdaptor adaptor;
@Value("${mqtt.netty.max_payload_size}") @Value("${transport.mqtt.netty.max_payload_size}")
private Integer maxPayloadSize; private Integer maxPayloadSize;
@Value("${cluster.node_id:#{null}}") @Value("${cluster.node_id:#{null}}")

7
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -55,6 +55,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx;
import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler;
import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher;
import org.thingsboard.server.transport.mqtt.util.SslUtil; import org.thingsboard.server.transport.mqtt.util.SslUtil;
import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLPeerUnverifiedException;
@ -93,7 +94,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private final TransportService transportService; private final TransportService transportService;
private final QuotaService quotaService; private final QuotaService quotaService;
private final SslHandler sslHandler; private final SslHandler sslHandler;
private final ConcurrentMap<String, Integer> mqttQoSMap; private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
private volatile SessionInfoProto sessionInfo; private volatile SessionInfoProto sessionInfo;
private volatile InetSocketAddress address; private volatile InetSocketAddress address;
@ -295,7 +296,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private void registerSubQoS(String topic, List<Integer> grantedQoSList, MqttQoS reqQoS) { private void registerSubQoS(String topic, List<Integer> grantedQoSList, MqttQoS reqQoS) {
grantedQoSList.add(getMinSupportedQos(reqQoS)); grantedQoSList.add(getMinSupportedQos(reqQoS));
mqttQoSMap.put(topic, getMinSupportedQos(reqQoS)); mqttQoSMap.put(new MqttTopicMatcher(topic), getMinSupportedQos(reqQoS));
} }
private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) { private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
@ -304,7 +305,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
log.trace("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId()); log.trace("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId());
for (String topicName : mqttMsg.payload().topics()) { for (String topicName : mqttMsg.payload().topics()) {
mqttQoSMap.remove(topicName); mqttQoSMap.remove(new MqttTopicMatcher(topicName));
try { try {
switch (topicName) { switch (topicName) {
case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: {

17
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -44,25 +44,22 @@ import javax.annotation.PreDestroy;
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@Service("MqttTransportService") @Service("MqttTransportService")
@ConditionalOnProperty(prefix = "mqtt", value = "enabled", havingValue = "true", matchIfMissing = false) @ConditionalOnProperty(prefix = "transport.mqtt", value = "enabled", havingValue = "true", matchIfMissing = true)
@Slf4j @Slf4j
public class MqttTransportService { public class MqttTransportService {
private static final String V1 = "v1"; @Value("${transport.mqtt.bind_address}")
private static final String DEVICE = "device";
@Value("${mqtt.bind_address}")
private String host; private String host;
@Value("${mqtt.bind_port}") @Value("${transport.mqtt.bind_port}")
private Integer port; private Integer port;
@Value("${mqtt.adaptor}") @Value("${transport.mqtt.adaptor}")
private String adaptorName; private String adaptorName;
@Value("${mqtt.netty.leak_detector_level}") @Value("${transport.mqtt.netty.leak_detector_level}")
private String leakDetectorLevel; private String leakDetectorLevel;
@Value("${mqtt.netty.boss_group_thread_count}") @Value("${transport.mqtt.netty.boss_group_thread_count}")
private Integer bossGroupThreadCount; private Integer bossGroupThreadCount;
@Value("${mqtt.netty.worker_group_thread_count}") @Value("${transport.mqtt.netty.worker_group_thread_count}")
private Integer workerGroupThreadCount; private Integer workerGroupThreadCount;
@Autowired @Autowired

11
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java

@ -36,19 +36,10 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private ChannelHandlerContext channel; private ChannelHandlerContext channel;
private AtomicInteger msgIdSeq = new AtomicInteger(0); private AtomicInteger msgIdSeq = new AtomicInteger(0);
public DeviceSessionCtx(UUID sessionId, ConcurrentMap<String, Integer> mqttQoSMap) { public DeviceSessionCtx(UUID sessionId, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap) {
super(sessionId, mqttQoSMap); super(sessionId, mqttQoSMap);
} }
private void logAndWrap(AdaptorException e) throws SessionException {
log.warn("Failed to convert msg: {}", e.getMessage(), e);
throw new SessionException(e);
}
private void pushToNetwork(MqttMessage msg) {
channel.writeAndFlush(msg);
}
public void setChannel(ChannelHandlerContext channel) { public void setChannel(ChannelHandlerContext channel) {
this.channel = channel; this.channel = channel;
} }

2
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java

@ -33,7 +33,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple
private final GatewaySessionHandler parent; private final GatewaySessionHandler parent;
private final SessionInfoProto sessionInfo; private final SessionInfoProto sessionInfo;
public GatewayDeviceSessionCtx(GatewaySessionHandler parent, DeviceInfoProto deviceInfo, ConcurrentMap<String, Integer> mqttQoSMap) { public GatewayDeviceSessionCtx(GatewaySessionHandler parent, DeviceInfoProto deviceInfo, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap) {
super(UUID.randomUUID(), mqttQoSMap); super(UUID.randomUUID(), mqttQoSMap);
this.parent = parent; this.parent = parent;
this.sessionInfo = SessionInfoProto.newBuilder() this.sessionInfo = SessionInfoProto.newBuilder()

2
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -68,7 +68,7 @@ public class GatewaySessionHandler {
private final DeviceInfoProto gateway; private final DeviceInfoProto gateway;
private final UUID sessionId; private final UUID sessionId;
private final Map<String, GatewayDeviceSessionCtx> devices; private final Map<String, GatewayDeviceSessionCtx> devices;
private final ConcurrentMap<String, Integer> mqttQoSMap; private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
private final ChannelHandlerContext channel; private final ChannelHandlerContext channel;
private final DeviceSessionCtx deviceSessionCtx; private final DeviceSessionCtx deviceSessionCtx;

21
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java

@ -16,35 +16,38 @@
package org.thingsboard.server.transport.mqtt.session; package org.thingsboard.server.transport.mqtt.session;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
/** /**
* Created by ashvayka on 30.08.18. * Created by ashvayka on 30.08.18.
*/ */
public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext { public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext {
private final ConcurrentMap<String, Integer> mqttQoSMap; private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
public MqttDeviceAwareSessionContext(UUID sessionId, ConcurrentMap<String, Integer> mqttQoSMap) { public MqttDeviceAwareSessionContext(UUID sessionId, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap) {
super(sessionId); super(sessionId);
this.mqttQoSMap = mqttQoSMap; this.mqttQoSMap = mqttQoSMap;
} }
public ConcurrentMap<String, Integer> getMqttQoSMap() { public ConcurrentMap<MqttTopicMatcher, Integer> getMqttQoSMap() {
return mqttQoSMap; return mqttQoSMap;
} }
public MqttQoS getQoSForTopic(String topic) { public MqttQoS getQoSForTopic(String topic) {
Integer qos = mqttQoSMap.get(topic); List<Integer> qosList = mqttQoSMap.entrySet()
if (qos != null) { .stream()
return MqttQoS.valueOf(qos); .filter(entry -> entry.getKey().matches(topic))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
if (!qosList.isEmpty()) {
return MqttQoS.valueOf(qosList.get(0));
} else { } else {
return MqttQoS.AT_LEAST_ONCE; return MqttQoS.AT_LEAST_ONCE;
} }

55
transport/mqtt-common/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.session;
import java.util.regex.Pattern;
public class MqttTopicMatcher {
private final String topic;
private final Pattern topicRegex;
public MqttTopicMatcher(String topic) {
if(topic == null){
throw new NullPointerException("topic");
}
this.topic = topic;
this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$");
}
public String getTopic() {
return topic;
}
public boolean matches(String topic){
return this.topicRegex.matcher(topic).matches();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MqttTopicMatcher that = (MqttTopicMatcher) o;
return topic.equals(that.topic);
}
@Override
public int hashCode() {
return topic.hashCode();
}
}

53
transport/mqtt-transport/src/main/resources/tb-mqtt-transport.yml

@ -17,32 +17,31 @@
spring.main.web-application-type: none spring.main.web-application-type: none
# MQTT server parameters # MQTT server parameters
mqtt: transport:
# Enable/disable mqtt transport protocol. mqtt:
enabled: "${MQTT_ENABLED:true}" bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" bind_port: "${MQTT_BIND_PORT:1883}"
bind_port: "${MQTT_BIND_PORT:1883}" adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}"
adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}" timeout: "${MQTT_TIMEOUT:10000}"
timeout: "${MQTT_TIMEOUT:10000}" netty:
netty: leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}"
leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}" boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}" max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}" # MQTT SSL configuration
# MQTT SSL configuration ssl:
ssl: # Enable/disable SSL support
# Enable/disable SSL support enabled: "${MQTT_SSL_ENABLED:false}"
enabled: "${MQTT_SSL_ENABLED:false}" # SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext
# SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}"
protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}" # Path to the key store that holds the SSL certificate
# Path to the key store that holds the SSL certificate key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}" # Password used to access the key store
# Password used to access the key store key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}" # Password used to access the key
# Password used to access the key key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" # Type of the key store
# Type of the key store key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
#Quota parameters #Quota parameters
quota: quota:
@ -56,7 +55,7 @@ quota:
# Interval for scheduled task that cleans expired records. TTL is used for expiring # Interval for scheduled task that cleans expired records. TTL is used for expiring
cleanPeriodMs: "${QUOTA_HOST_CLEAN_PERIOD_MS:300000}" cleanPeriodMs: "${QUOTA_HOST_CLEAN_PERIOD_MS:300000}"
# Enable Host API Limits # Enable Host API Limits
enabled: "${QUOTA_HOST_ENABLED:false}" enabled: "${QUOTA_HOST_ENABLED:true}"
# Array of whitelist hosts # Array of whitelist hosts
whitelist: "${QUOTA_HOST_WHITELIST:localhost,127.0.0.1}" whitelist: "${QUOTA_HOST_WHITELIST:localhost,127.0.0.1}"
# Array of blacklist hosts # Array of blacklist hosts

33
ui/src/app/api/entity.service.js

@ -533,6 +533,21 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
} }
); );
break; break;
case types.aliasFilterType.entityViewType.value:
getEntitiesByNameFilter(types.entityType.entityView, filter.entityViewNameFilter, maxItems, {ignoreLoading: true}, filter.entityViewType).then(
function success(entities) {
if (entities && entities.length || !failOnEmpty) {
result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
}
},
function fail() {
deferred.reject();
}
);
break;
case types.aliasFilterType.relationsQuery.value: case types.aliasFilterType.relationsQuery.value:
result.stateEntity = filter.rootStateEntity; result.stateEntity = filter.rootStateEntity;
var rootEntityType; var rootEntityType;
@ -578,6 +593,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
break; break;
case types.aliasFilterType.assetSearchQuery.value: case types.aliasFilterType.assetSearchQuery.value:
case types.aliasFilterType.deviceSearchQuery.value: case types.aliasFilterType.deviceSearchQuery.value:
case types.aliasFilterType.entityViewSearchQuery.value:
result.stateEntity = filter.rootStateEntity; result.stateEntity = filter.rootStateEntity;
if (result.stateEntity && stateEntityId) { if (result.stateEntity && stateEntityId) {
rootEntityType = stateEntityId.entityType; rootEntityType = stateEntityId.entityType;
@ -604,6 +620,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
} else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) { } else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) {
searchQuery.deviceTypes = filter.deviceTypes; searchQuery.deviceTypes = filter.deviceTypes;
findByQueryPromise = deviceService.findByQuery(searchQuery, false, {ignoreLoading: true}); findByQueryPromise = deviceService.findByQuery(searchQuery, false, {ignoreLoading: true});
} else if (filter.type == types.aliasFilterType.entityViewSearchQuery.value) {
searchQuery.entityViewTypes = filter.entityViewTypes;
findByQueryPromise = entityViewService.findByQuery(searchQuery, false, {ignoreLoading: true});
} }
findByQueryPromise.then( findByQueryPromise.then(
function success(entities) { function success(entities) {
@ -646,6 +665,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
case types.aliasFilterType.deviceType.value: case types.aliasFilterType.deviceType.value:
return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
case types.aliasFilterType.entityViewType.value:
return entityTypes.indexOf(types.entityType.entityView) > -1 ? true : false;
case types.aliasFilterType.relationsQuery.value: case types.aliasFilterType.relationsQuery.value:
if (filter.filters && filter.filters.length) { if (filter.filters && filter.filters.length) {
var match = false; var match = false;
@ -671,6 +692,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
case types.aliasFilterType.deviceSearchQuery.value: case types.aliasFilterType.deviceSearchQuery.value:
return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
case types.aliasFilterType.entityViewSearchQuery.value:
return entityTypes.indexOf(types.entityType.entityView) > -1 ? true : false;
} }
} }
return false; return false;
@ -690,12 +713,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return entityType === types.entityType.asset; return entityType === types.entityType.asset;
case types.aliasFilterType.deviceType.value: case types.aliasFilterType.deviceType.value:
return entityType === types.entityType.device; return entityType === types.entityType.device;
case types.aliasFilterType.entityViewType.value:
return entityType === types.entityType.entityView;
case types.aliasFilterType.relationsQuery.value: case types.aliasFilterType.relationsQuery.value:
return true; return true;
case types.aliasFilterType.assetSearchQuery.value: case types.aliasFilterType.assetSearchQuery.value:
return entityType === types.entityType.asset; return entityType === types.entityType.asset;
case types.aliasFilterType.deviceSearchQuery.value: case types.aliasFilterType.deviceSearchQuery.value:
return entityType === types.entityType.device; return entityType === types.entityType.device;
case types.aliasFilterType.entityViewSearchQuery.value:
return entityType === types.entityType.entityView;
} }
return false; return false;
} }
@ -1046,6 +1073,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return assetService.deleteAsset(entityId.id); return assetService.deleteAsset(entityId.id);
} else if (entityId.entityType == types.entityType.device) { } else if (entityId.entityType == types.entityType.device) {
return deviceService.deleteDevice(entityId.id); return deviceService.deleteDevice(entityId.id);
} else if (entityId.entityType == types.entityType.entityView) {
return entityViewService.deleteEntityView(entityId.id);
} }
} }
@ -1151,6 +1180,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return assetService.saveAsset(entity); return assetService.saveAsset(entity);
} else if (entityType == types.entityType.device) { } else if (entityType == types.entityType.device) {
return deviceService.saveDevice(entity); return deviceService.saveDevice(entity);
} else if (entityType == types.entityType.entityView) {
return entityViewService.saveEntityView(entity);
} }
} }
@ -1279,6 +1310,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
searchQuery.assetTypes = entitySubTypes; searchQuery.assetTypes = entitySubTypes;
} else if (entityType == types.entityType.device) { } else if (entityType == types.entityType.device) {
searchQuery.deviceTypes = entitySubTypes; searchQuery.deviceTypes = entitySubTypes;
} else if (entityType == types.entityType.entityView) {
searchQuery.entityViewTypes = entitySubTypes;
} else { } else {
return null; //Not supported return null; //Not supported
} }

8
ui/src/app/api/subscription.js

@ -172,9 +172,9 @@ export default class Subscription {
if (this.type === this.ctx.types.widgetType.rpc.value) { if (this.type === this.ctx.types.widgetType.rpc.value) {
if (this.targetDeviceId) { if (this.targetDeviceId) {
entityId = { entityId = {
entityType: this.ctx.entityType.device, entityType: this.ctx.types.entityType.device,
id: this.targetDeviceId id: this.targetDeviceId
} };
entityName = this.targetDeviceName; entityName = this.targetDeviceName;
} }
} else if (this.type == this.ctx.types.widgetType.alarm.value) { } else if (this.type == this.ctx.types.widgetType.alarm.value) {
@ -182,7 +182,7 @@ export default class Subscription {
entityId = { entityId = {
entityType: this.alarmSource.entityType, entityType: this.alarmSource.entityType,
id: this.alarmSource.entityId id: this.alarmSource.entityId
} };
entityName = this.alarmSource.entityName; entityName = this.alarmSource.entityName;
} }
} else { } else {
@ -192,7 +192,7 @@ export default class Subscription {
entityId = { entityId = {
entityType: datasource.entityType, entityType: datasource.entityType,
id: datasource.entityId id: datasource.entityId
} };
entityName = datasource.entityName; entityName = datasource.entityName;
break; break;
} }

53
ui/src/app/api/time.service.js

@ -26,15 +26,17 @@ const MIN_INTERVAL = SECOND;
const MAX_INTERVAL = 365 * 20 * DAY; const MAX_INTERVAL = 365 * 20 * DAY;
const MIN_LIMIT = 10; const MIN_LIMIT = 10;
const AVG_LIMIT = 200; //const AVG_LIMIT = 200;
const MAX_LIMIT = 500; //const MAX_LIMIT = 500;
/*@ngInject*/ /*@ngInject*/
function TimeService($translate, types) { function TimeService($translate, $http, $q, types) {
var predefIntervals; var predefIntervals;
var maxDatapointsLimit;
var service = { var service = {
loadMaxDatapointsLimit: loadMaxDatapointsLimit,
minIntervalLimit: minIntervalLimit, minIntervalLimit: minIntervalLimit,
maxIntervalLimit: maxIntervalLimit, maxIntervalLimit: maxIntervalLimit,
boundMinInterval: boundMinInterval, boundMinInterval: boundMinInterval,
@ -45,20 +47,38 @@ function TimeService($translate, types) {
defaultTimewindow: defaultTimewindow, defaultTimewindow: defaultTimewindow,
toHistoryTimewindow: toHistoryTimewindow, toHistoryTimewindow: toHistoryTimewindow,
createSubscriptionTimewindow: createSubscriptionTimewindow, createSubscriptionTimewindow: createSubscriptionTimewindow,
avgAggregationLimit: function () { getMaxDatapointsLimit: function () {
return AVG_LIMIT; return maxDatapointsLimit;
},
getMinDatapointsLimit: function () {
return MIN_LIMIT;
} }
} }
return service; return service;
function loadMaxDatapointsLimit() {
var deferred = $q.defer();
var url = '/api/dashboard/maxDatapointsLimit';
$http.get(url, {ignoreLoading: true}).then(function success(response) {
maxDatapointsLimit = response.data;
if (!maxDatapointsLimit || maxDatapointsLimit <= MIN_LIMIT) {
maxDatapointsLimit = MIN_LIMIT + 1;
}
deferred.resolve();
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function minIntervalLimit(timewindow) { function minIntervalLimit(timewindow) {
var min = timewindow / MAX_LIMIT; var min = timewindow / 500;
return boundMinInterval(min); return boundMinInterval(min);
} }
function avgInterval(timewindow) { function avgInterval(timewindow) {
var avg = timewindow / AVG_LIMIT; var avg = timewindow / 200;
return boundMinInterval(avg); return boundMinInterval(avg);
} }
@ -230,7 +250,7 @@ function TimeService($translate, types) {
}, },
aggregation: { aggregation: {
type: types.aggregation.avg.value, type: types.aggregation.avg.value,
limit: AVG_LIMIT limit: Math.floor(maxDatapointsLimit / 2)
} }
} }
return timewindow; return timewindow;
@ -246,22 +266,27 @@ function TimeService($translate, types) {
} }
var aggType; var aggType;
var limit;
if (timewindow.aggregation) { if (timewindow.aggregation) {
aggType = timewindow.aggregation.type || types.aggregation.avg.value; aggType = timewindow.aggregation.type || types.aggregation.avg.value;
limit = timewindow.aggregation.limit || maxDatapointsLimit;
} else { } else {
aggType = types.aggregation.avg.value; aggType = types.aggregation.avg.value;
limit = maxDatapointsLimit;
} }
var historyTimewindow = { var historyTimewindow = {
history: { history: {
fixedTimewindow: { fixedTimewindow: {
startTimeMs: startTimeMs, startTimeMs: startTimeMs,
endTimeMs: endTimeMs endTimeMs: endTimeMs
}, },
interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, aggType) interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, types.aggregation.avg.value)
}, },
aggregation: { aggregation: {
type: aggType type: aggType,
limit: limit
} }
} }
@ -275,7 +300,7 @@ function TimeService($translate, types) {
realtimeWindowMs: null, realtimeWindowMs: null,
aggregation: { aggregation: {
interval: SECOND, interval: SECOND,
limit: AVG_LIMIT, limit: maxDatapointsLimit,
type: types.aggregation.avg.value type: types.aggregation.avg.value
} }
}; };
@ -283,14 +308,14 @@ function TimeService($translate, types) {
if (stateData) { if (stateData) {
subscriptionTimewindow.aggregation = { subscriptionTimewindow.aggregation = {
interval: SECOND, interval: SECOND,
limit: MAX_LIMIT, limit: maxDatapointsLimit,
type: types.aggregation.none.value, type: types.aggregation.none.value,
stateData: true stateData: true
}; };
} else { } else {
subscriptionTimewindow.aggregation = { subscriptionTimewindow.aggregation = {
interval: SECOND, interval: SECOND,
limit: AVG_LIMIT, limit: maxDatapointsLimit,
type: types.aggregation.avg.value type: types.aggregation.avg.value
}; };
} }
@ -298,7 +323,7 @@ function TimeService($translate, types) {
if (angular.isDefined(timewindow.aggregation) && !stateData) { if (angular.isDefined(timewindow.aggregation) && !stateData) {
subscriptionTimewindow.aggregation = { subscriptionTimewindow.aggregation = {
type: timewindow.aggregation.type || types.aggregation.avg.value, type: timewindow.aggregation.type || types.aggregation.avg.value,
limit: timewindow.aggregation.limit || AVG_LIMIT limit: timewindow.aggregation.limit || maxDatapointsLimit
}; };
} }
if (angular.isDefined(timewindow.realtime)) { if (angular.isDefined(timewindow.realtime)) {

3
ui/src/app/api/user.service.js

@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
.name; .name;
/*@ngInject*/ /*@ngInject*/
function UserService($http, $q, $rootScope, adminService, dashboardService, loginService, toast, store, jwtHelper, $translate, $state, $location) { function UserService($http, $q, $rootScope, adminService, dashboardService, timeService, loginService, toast, store, jwtHelper, $translate, $state, $location) {
var currentUser = null, var currentUser = null,
currentUserDetails = null, currentUserDetails = null,
lastPublicDashboardId = null, lastPublicDashboardId = null,
@ -390,6 +390,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
function loadSystemParams() { function loadSystemParams() {
var promises = []; var promises = [];
promises.push(loadIsUserTokenAccessEnabled()); promises.push(loadIsUserTokenAccessEnabled());
promises.push(timeService.loadMaxDatapointsLimit());
return $q.all(promises); return $q.all(promises);
} }

8
ui/src/app/common/types.constant.js

@ -253,6 +253,10 @@ export default angular.module('thingsboard.types', [])
value: 'deviceType', value: 'deviceType',
name: 'alias.filter-type-device-type' name: 'alias.filter-type-device-type'
}, },
entityViewType: {
value: 'entityViewType',
name: 'alias.filter-type-entity-view-type'
},
relationsQuery: { relationsQuery: {
value: 'relationsQuery', value: 'relationsQuery',
name: 'alias.filter-type-relations-query' name: 'alias.filter-type-relations-query'
@ -264,6 +268,10 @@ export default angular.module('thingsboard.types', [])
deviceSearchQuery: { deviceSearchQuery: {
value: 'deviceSearchQuery', value: 'deviceSearchQuery',
name: 'alias.filter-type-device-search-query' name: 'alias.filter-type-device-search-query'
},
entityViewSearchQuery: {
value: 'entityViewSearchQuery',
name: 'alias.filter-type-entity-view-search-query'
} }
}, },
position: { position: {

10
ui/src/app/components/timewindow-panel.controller.js

@ -31,6 +31,8 @@ export default function TimewindowPanelController(mdPanelRef, $scope, timeServic
vm.maxRealtimeAggInterval = maxRealtimeAggInterval; vm.maxRealtimeAggInterval = maxRealtimeAggInterval;
vm.minHistoryAggInterval = minHistoryAggInterval; vm.minHistoryAggInterval = minHistoryAggInterval;
vm.maxHistoryAggInterval = maxHistoryAggInterval; vm.maxHistoryAggInterval = maxHistoryAggInterval;
vm.minDatapointsLimit = minDatapointsLimit;
vm.maxDatapointsLimit = maxDatapointsLimit;
if (vm.historyOnly) { if (vm.historyOnly) {
vm.timewindow.selectedTab = 1; vm.timewindow.selectedTab = 1;
@ -86,6 +88,14 @@ export default function TimewindowPanelController(mdPanelRef, $scope, timeServic
return timeService.maxIntervalLimit(currentHistoryTimewindow()); return timeService.maxIntervalLimit(currentHistoryTimewindow());
} }
function minDatapointsLimit () {
return timeService.getMinDatapointsLimit();
}
function maxDatapointsLimit () {
return timeService.getMaxDatapointsLimit();
}
function currentHistoryTimewindow() { function currentHistoryTimewindow() {
if (vm.timewindow.history.historyType === 0) { if (vm.timewindow.history.historyType === 0) {
return vm.timewindow.history.timewindowMs; return vm.timewindow.history.timewindowMs;

15
ui/src/app/components/timewindow-panel.tpl.html

@ -60,19 +60,22 @@
</md-option> </md-option>
</md-select> </md-select>
</md-input-container> </md-input-container>
<md-slider-container ng-show="vm.showLimit()"> <md-slider-container ng-if="vm.showLimit()">
<span translate>aggregation.limit</span> <span translate>aggregation.limit</span>
<md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider"> <md-slider flex min="{{vm.minDatapointsLimit()}}" max="{{vm.maxDatapointsLimit()}}" step="1"
ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider">
</md-slider> </md-slider>
<md-input-container> <md-input-container style="max-width: 80px;">
<input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider"> <input flex type="number" step="1"
min="{{vm.minDatapointsLimit()}}" max="{{vm.maxDatapointsLimit()}}"
ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider">
</md-input-container> </md-input-container>
</md-slider-container> </md-slider-container>
<tb-timeinterval ng-show="vm.showRealtimeAggInterval()" min="vm.minRealtimeAggInterval()" max="vm.maxRealtimeAggInterval()" <tb-timeinterval ng-if="vm.showRealtimeAggInterval()" min="vm.minRealtimeAggInterval()" max="vm.maxRealtimeAggInterval()"
predefined-name="'aggregation.group-interval'" predefined-name="'aggregation.group-interval'"
ng-model="vm.timewindow.realtime.interval"> ng-model="vm.timewindow.realtime.interval">
</tb-timeinterval> </tb-timeinterval>
<tb-timeinterval ng-show="vm.showHistoryAggInterval()" min="vm.minHistoryAggInterval()" max="vm.maxHistoryAggInterval()" <tb-timeinterval ng-if="vm.showHistoryAggInterval()" min="vm.minHistoryAggInterval()" max="vm.maxHistoryAggInterval()"
predefined-name="'aggregation.group-interval'" predefined-name="'aggregation.group-interval'"
ng-model="vm.timewindow.history.interval"> ng-model="vm.timewindow.history.interval">
</tb-timeinterval> </tb-timeinterval>

2
ui/src/app/components/timewindow.directive.js

@ -228,7 +228,7 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) { if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) {
model.aggregation.type = value.aggregation.type; model.aggregation.type = value.aggregation.type;
} }
model.aggregation.limit = value.aggregation.limit || timeService.avgAggregationLimit(); model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
} }
} }
scope.updateDisplayValue(); scope.updateDisplayValue();

22
ui/src/app/entity-view/entity-view-fieldset.tpl.html

@ -52,12 +52,22 @@
<div translate ng-message="required">entity-view.name-required</div> <div translate ng-message="required">entity-view.name-required</div>
</div> </div>
</md-input-container> </md-input-container>
<tb-entity-select flex ng-disabled="!isEdit" <tb-entity-subtype-autocomplete
the-form="theForm" ng-disabled="$root.loading || !isEdit"
tb-required="true" tb-required="true"
allowed-entity-types="allowedEntityTypes" the-form="theForm"
ng-model="entityView.entityId"> ng-model="entityView.type"
</tb-entity-select> entity-type="types.entityType.entityView">
</tb-entity-subtype-autocomplete>
<section layout="column">
<label translate class="tb-title no-padding">entity-view.related-entity</label>
<tb-entity-select flex ng-disabled="!isEdit"
the-form="theForm"
tb-required="true"
allowed-entity-types="allowedEntityTypes"
ng-model="entityView.entityId">
</tb-entity-select>
</section>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>entity-view.description</label> <label translate>entity-view.description</label>
<textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea> <textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea>

4
ui/src/app/entity-view/entity-view.routes.js

@ -42,7 +42,7 @@ export default function EntityViewRoutes($stateProvider, types) {
pageTitle: 'entity-view.entity-views' pageTitle: 'entity-view.entity-views'
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
label: '{"icon": "view_stream", "label": "entity-view.entity-views"}' label: '{"icon": "view_quilt", "label": "entity-view.entity-views"}'
} }
}) })
.state('home.customers.entityViews', { .state('home.customers.entityViews', {
@ -65,7 +65,7 @@ export default function EntityViewRoutes($stateProvider, types) {
pageTitle: 'customer.entity-views' pageTitle: 'customer.entity-views'
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
label: '{"icon": "view_stream", "label": "{{ vm.customerEntityViewsTitle }}", "translate": "false"}' label: '{"icon": "view_quilt", "label": "{{ vm.customerEntityViewsTitle }}", "translate": "false"}'
} }
}); });

22
ui/src/app/entity/entity-filter-view.directive.js

@ -77,6 +77,15 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q,
scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType}); scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType});
} }
break; break;
case types.aliasFilterType.entityViewType.value:
var entityViewType = scope.filter.entityViewType;
prefix = scope.filter.entityViewNameFilter;
if (prefix && prefix.length) {
scope.filterDisplayValue = $translate.instant('alias.filter-type-entity-view-type-and-name-description', {entityViewType: entityViewType, prefix: prefix});
} else {
scope.filterDisplayValue = $translate.instant('alias.filter-type-entity-view-type-description', {entityViewType: entityViewType});
}
break;
case types.aliasFilterType.relationsQuery.value: case types.aliasFilterType.relationsQuery.value:
var rootEntityText; var rootEntityText;
var directionText; var directionText;
@ -134,6 +143,7 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q,
break; break;
case types.aliasFilterType.assetSearchQuery.value: case types.aliasFilterType.assetSearchQuery.value:
case types.aliasFilterType.deviceSearchQuery.value: case types.aliasFilterType.deviceSearchQuery.value:
case types.aliasFilterType.entityViewSearchQuery.value:
allEntitiesText = $translate.instant('alias.all-entities'); allEntitiesText = $translate.instant('alias.all-entities');
anyRelationText = $translate.instant('alias.any-relation'); anyRelationText = $translate.instant('alias.any-relation');
if (scope.filter.rootStateEntity) { if (scope.filter.rootStateEntity) {
@ -165,7 +175,7 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q,
scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description', scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description',
translationValues translationValues
); );
} else { } else if (scope.filter.type == types.aliasFilterType.deviceSearchQuery.value) {
var deviceTypesQuoted = []; var deviceTypesQuoted = [];
scope.filter.deviceTypes.forEach(function(deviceType) { scope.filter.deviceTypes.forEach(function(deviceType) {
deviceTypesQuoted.push("'"+deviceType+"'"); deviceTypesQuoted.push("'"+deviceType+"'");
@ -175,6 +185,16 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q,
scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description', scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description',
translationValues translationValues
); );
} else if (scope.filter.type == types.aliasFilterType.entityViewSearchQuery.value) {
var entityViewTypesQuoted = [];
scope.filter.entityViewTypes.forEach(function(entityViewType) {
entityViewTypesQuoted.push("'"+entityViewType+"'");
});
var entityViewTypesText = entityViewTypesQuoted.join(', ');
translationValues.entityViewTypes = entityViewTypesText;
scope.filterDisplayValue = $translate.instant('alias.filter-type-entity-view-search-query-description',
translationValues
);
} }
break; break;
default: default:

8
ui/src/app/entity/entity-filter.directive.js

@ -69,9 +69,14 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc
filter.deviceType = null; filter.deviceType = null;
filter.deviceNameFilter = ''; filter.deviceNameFilter = '';
break; break;
case types.aliasFilterType.entityViewType.value:
filter.entityViewType = null;
filter.entityViewNameFilter = '';
break;
case types.aliasFilterType.relationsQuery.value: case types.aliasFilterType.relationsQuery.value:
case types.aliasFilterType.assetSearchQuery.value: case types.aliasFilterType.assetSearchQuery.value:
case types.aliasFilterType.deviceSearchQuery.value: case types.aliasFilterType.deviceSearchQuery.value:
case types.aliasFilterType.entityViewSearchQuery.value:
filter.rootStateEntity = false; filter.rootStateEntity = false;
filter.stateEntityParamName = null; filter.stateEntityParamName = null;
filter.defaultStateEntity = null; filter.defaultStateEntity = null;
@ -86,6 +91,9 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc
} else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) { } else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) {
filter.relationType = null; filter.relationType = null;
filter.deviceTypes = []; filter.deviceTypes = [];
} else if (filter.type === types.aliasFilterType.entityViewSearchQuery.value) {
filter.relationType = null;
filter.entityViewTypes = [];
} }
break; break;
} }

83
ui/src/app/entity/entity-filter.tpl.html

@ -112,6 +112,20 @@
aria-label="{{ 'device.name-starts-with' | translate }}"> aria-label="{{ 'device.name-starts-with' | translate }}">
</md-input-container> </md-input-container>
</section> </section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.entityViewType.value" id="entityViewTypeFilter">
<tb-entity-subtype-autocomplete
tb-required="true"
the-form="theForm"
ng-model="filter.entityViewType"
entity-type="types.entityType.entityView">
</tb-entity-subtype-autocomplete>
<md-input-container class="md-block">
<label translate>entity-view.name-starts-with</label>
<input name="entityViewNameFilter"
ng-model="filter.entityViewNameFilter"
aria-label="{{ 'entity-view.name-starts-with' | translate }}">
</md-input-container>
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.relationsQuery.value" id="relationsQueryFilter"> <section layout="column" ng-if="filter.type == types.aliasFilterType.relationsQuery.value" id="relationsQueryFilter">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label> <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<section class="tb-root-state-entity-switch" layout="row" layout-align="start center" style="padding-left: 0px;"> <section class="tb-root-state-entity-switch" layout="row" layout-align="start center" style="padding-left: 0px;">
@ -311,4 +325,73 @@
ng-model="filter.deviceTypes"> ng-model="filter.deviceTypes">
</tb-entity-subtype-list> </tb-entity-subtype-list>
</section> </section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.entityViewSearchQuery.value" id="entityViewSearchQueryFilter">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<section class="tb-root-state-entity-switch" layout="row" layout-align="start center" style="padding-left: 0px;">
<md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
aria-label="{{ 'alias.root-state-entity' | translate }}">
</md-switch>
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
</section>
<div flex layout="row" ng-if="!filter.rootStateEntity">
<tb-entity-select flex
the-form="theForm"
tb-required="!filter.rootStateEntity"
ng-disabled="filter.rootStateEntity"
use-alias-entity-types="true"
ng-model="filter.rootEntity">
</tb-entity-select>
</div>
<div flex layout="row" ng-if="filter.rootStateEntity">
<md-input-container class="md-block" style="margin-top: 32px;">
<label translate>alias.state-entity-parameter-name</label>
<input name="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
ng-model="filter.stateEntityParamName"
aria-label="{{ 'alias.state-entity-parameter-name' | translate }}">
</md-input-container>
<div flex layout="column">
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
<tb-entity-select flex
the-form="theForm"
tb-required="false"
use-alias-entity-types="true"
ng-model="filter.defaultStateEntity">
</tb-entity-select>
</div>
</div>
<div flex layout="row">
<md-input-container class="md-block" style="min-width: 100px;">
<label translate>relation.direction</label>
<md-select required ng-model="filter.direction">
<md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
{{ ('relation.search-direction.' + direction) | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alias.max-relation-level</label>
<input name="maxRelationLevel"
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
ng-model="filter.maxLevel"
aria-label="{{ 'alias.max-relation-level' | translate }}">
</md-input-container>
</div>
<div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
<tb-relation-type-autocomplete flex
hide-label
the-form="theForm"
ng-model="filter.relationType"
tb-required="false">
</tb-relation-type-autocomplete>
<div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>entity-view.entity-view-types</div>
<tb-entity-subtype-list
tb-required="true"
entity-type="types.entityType.entityView"
ng-model="filter.entityViewTypes">
</tb-entity-subtype-list>
</section>
</div> </div>

11
ui/src/app/entity/entity-subtype-autocomplete.directive.js

@ -22,7 +22,7 @@ import entitySubtypeAutocompleteTemplate from './entity-subtype-autocomplete.tpl
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) { export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, entityViewService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) { var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySubtypeAutocompleteTemplate); var template = $templateCache.get(entitySubtypeAutocompleteTemplate);
@ -96,6 +96,8 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q,
entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true}); entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true});
} else if (scope.entityType == types.entityType.device) { } else if (scope.entityType == types.entityType.device) {
entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true}); entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true});
} else if (scope.entityType == types.entityType.entityView) {
entitySubtypesPromise = entityViewService.getEntityViewTypes({ignoreLoading: true});
} }
if (entitySubtypesPromise) { if (entitySubtypesPromise) {
entitySubtypesPromise.then( entitySubtypesPromise.then(
@ -134,6 +136,13 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q,
scope.$on('deviceSaved', function() { scope.$on('deviceSaved', function() {
scope.entitySubtypes = null; scope.entitySubtypes = null;
}); });
} else if (scope.entityType == types.entityType.entityView) {
scope.selectEntitySubtypeText = 'entity-view.select-entity-view-type';
scope.entitySubtypeText = 'entity-view.entity-view-type';
scope.entitySubtypeRequiredText = 'entity-view.entity-view-type-required';
scope.$on('entityViewSaved', function() {
scope.entitySubtypes = null;
});
} }
} }

10
ui/src/app/entity/entity-subtype-list.directive.js

@ -22,7 +22,7 @@ import entitySubtypeListTemplate from './entity-subtype-list.tpl.html';
import './entity-subtype-list.scss'; import './entity-subtype-list.scss';
/*@ngInject*/ /*@ngInject*/
export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService) { export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService, entityViewService) {
var linker = function (scope, element, attrs, ngModelCtrl) { var linker = function (scope, element, attrs, ngModelCtrl) {
@ -47,6 +47,12 @@ export default function EntitySubtypeListDirective($compile, $templateCache, $q,
scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type'); scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type');
scope.noSubtypesMathingText = 'device.no-device-types-matching'; scope.noSubtypesMathingText = 'device.no-device-types-matching';
scope.subtypeListEmptyText = 'device.device-type-list-empty'; scope.subtypeListEmptyText = 'device.device-type-list-empty';
} else if (scope.entityType == types.entityType.entityView) {
scope.placeholder = scope.tbRequired ? $translate.instant('entity-view.enter-entity-view-type')
: $translate.instant('entity-view.any-entity-view');
scope.secondaryPlaceholder = '+' + $translate.instant('entity-view.entity-view-type');
scope.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching';
scope.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty';
} }
scope.$watch('tbRequired', function () { scope.$watch('tbRequired', function () {
@ -97,6 +103,8 @@ export default function EntitySubtypeListDirective($compile, $templateCache, $q,
entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true}); entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true});
} else if (scope.entityType == types.entityType.device) { } else if (scope.entityType == types.entityType.device) {
entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true}); entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true});
} else if (scope.entityType == types.entityType.entityView) {
entitySubtypesPromise = entityViewService.getEntityViewTypes({ignoreLoading: true});
} }
if (entitySubtypesPromise) { if (entitySubtypesPromise) {
entitySubtypesPromise.then( entitySubtypesPromise.then(

11
ui/src/app/entity/entity-subtype-select.directive.js

@ -22,7 +22,7 @@ import entitySubtypeSelectTemplate from './entity-subtype-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, types) { export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, entityViewService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) { var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySubtypeSelectTemplate); var template = $templateCache.get(entitySubtypeSelectTemplate);
@ -75,6 +75,8 @@ export default function EntitySubtypeSelect($compile, $templateCache, $translate
entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true}); entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true});
} else if (scope.entityType == types.entityType.device) { } else if (scope.entityType == types.entityType.device) {
entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true}); entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true});
} else if (scope.entityType == types.entityType.entityView) {
entitySubtypesPromise = entityViewService.getEntityViewTypes({ignoreLoading: true});
} }
if (entitySubtypesPromise) { if (entitySubtypesPromise) {
entitySubtypesPromise.then( entitySubtypesPromise.then(
@ -100,6 +102,9 @@ export default function EntitySubtypeSelect($compile, $templateCache, $translate
} else if (scope.entityType == types.entityType.device) { } else if (scope.entityType == types.entityType.device) {
scope.entitySubtypeTitle = 'device.device-type'; scope.entitySubtypeTitle = 'device.device-type';
scope.entitySubtypeRequiredText = 'device.device-type-required'; scope.entitySubtypeRequiredText = 'device.device-type-required';
} else if (scope.entityType == types.entityType.entityView) {
scope.entitySubtypeTitle = 'entity-view.entity-view-type';
scope.entitySubtypeRequiredText = 'entity-view.entity-view-type-required';
} }
scope.entitySubtypes.length = 0; scope.entitySubtypes.length = 0;
if (scope.entitySubtypesList && scope.entitySubtypesList.length) { if (scope.entitySubtypesList && scope.entitySubtypesList.length) {
@ -116,6 +121,10 @@ export default function EntitySubtypeSelect($compile, $templateCache, $translate
scope.$on('deviceSaved', function() { scope.$on('deviceSaved', function() {
loadSubTypes(); loadSubTypes();
}); });
} else if (scope.entityType == types.entityType.entityView) {
scope.$on('entityViewSaved', function() {
loadSubTypes();
});
} }
} }
} }

1
ui/src/app/help/help-links.constant.js

@ -96,6 +96,7 @@ export default angular.module('thingsboard.help', [])
customers: helpBaseUrl + "/docs/user-guide/ui/customers", customers: helpBaseUrl + "/docs/user-guide/ui/customers",
assets: helpBaseUrl + "/docs/user-guide/ui/assets", assets: helpBaseUrl + "/docs/user-guide/ui/assets",
devices: helpBaseUrl + "/docs/user-guide/ui/devices", devices: helpBaseUrl + "/docs/user-guide/ui/devices",
entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views",
dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards", dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards",
users: helpBaseUrl + "/docs/user-guide/ui/users", users: helpBaseUrl + "/docs/user-guide/ui/users",
widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles", widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles",

1545
ui/src/app/locale/locale.constant-TR.json

File diff suppressed because it is too large

11
ui/src/app/locale/locale.constant-en_US.json

@ -158,12 +158,17 @@
"filter-type-device-type": "Device type", "filter-type-device-type": "Device type",
"filter-type-device-type-description": "Devices of type '{{deviceType}}'", "filter-type-device-type-description": "Devices of type '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
"filter-type-entity-view-type": "Entity View type",
"filter-type-entity-view-type-description": "Entity Views of type '{{entityView}}'",
"filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityView}}' and with name starting with '{{prefix}}'",
"filter-type-relations-query": "Relations query", "filter-type-relations-query": "Relations query",
"filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Asset search query", "filter-type-asset-search-query": "Asset search query",
"filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-device-search-query": "Device search query", "filter-type-device-search-query": "Device search query",
"filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-entity-view-search-query": "Entity view search query",
"filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"entity-filter": "Entity filter", "entity-filter": "Entity filter",
"resolve-multiple": "Resolve as multiple entities", "resolve-multiple": "Resolve as multiple entities",
"filter-type": "Filter type", "filter-type": "Filter type",
@ -839,7 +844,8 @@
"client-attributes": "Client attributes", "client-attributes": "Client attributes",
"shared-attributes": "Shared attributes", "shared-attributes": "Shared attributes",
"server-attributes": "Server attributes", "server-attributes": "Server attributes",
"latest-timeseries": "Latest timeseries" "latest-timeseries": "Latest timeseries",
"related-entity": "Related entity"
}, },
"event": { "event": {
"event-type": "Event type", "event-type": "Event type",
@ -1551,7 +1557,8 @@
"ko_KR": "Korean", "ko_KR": "Korean",
"ru_RU": "Russian", "ru_RU": "Russian",
"es_ES": "Spanish", "es_ES": "Spanish",
"ja_JA": "Japanese" "ja_JA": "Japanese",
"TR": "Turkish"
} }
} }
} }

2107
ui/src/app/locale/locale.constant-es_ES.json

File diff suppressed because it is too large

2919
ui/src/app/locale/locale.constant-fr_FR.json

File diff suppressed because it is too large

89
ui/src/app/locale/locale.constant-it_IT.json

@ -528,13 +528,13 @@
"decimals": "Numero cifre decimali", "decimals": "Numero cifre decimali",
"data-generation-func": "Funzione generazione dati", "data-generation-func": "Funzione generazione dati",
"use-data-post-processing-func": "Use data post-processing function", "use-data-post-processing-func": "Use data post-processing function",
"configuration": "Data key configuration", "configuration": "Configurazione data key",
"timeseries": "Serie temporali", "timeseries": "Serie temporali",
"attributes": "Attributi", "attributes": "Attributi",
"alarm": "Campi allarme", "alarm": "Campi allarme",
"timeseries-required": "Entity timeseries are required.", "timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes are required.", "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
"maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", "maximum-timeseries-or-attributes": "Massimo { count, plural, 1 {1 serie temporale/attributo consentito.} other {# serie temporali/attributi consentiti.} }",
"alarm-fields-required": "Campi allarme obbligatori.", "alarm-fields-required": "Campi allarme obbligatori.",
"function-types": "Tipi funzione", "function-types": "Tipi funzione",
"function-types-required": "Tipi funzione obbligatorio.", "function-types-required": "Tipi funzione obbligatorio.",
@ -759,7 +759,7 @@
"entity": "Entità", "entity": "Entità",
"message-id": "Id Messaggio", "message-id": "Id Messaggio",
"message-type": "Tipo Messaggio", "message-type": "Tipo Messaggio",
"data-type": "Data Type", "data-type": "Tipo di dato",
"relation-type": "Tipo di relazione", "relation-type": "Tipo di relazione",
"metadata": "Metadati", "metadata": "Metadati",
"data": "Dati", "data": "Dati",
@ -807,7 +807,7 @@
"add-map": "Add mapping element", "add-map": "Add mapping element",
"timeseries": "Serie temporali", "timeseries": "Serie temporali",
"add-timeseries": "Add timeseries", "add-timeseries": "Add timeseries",
"field-required": "Field is required", "field-required": "Campo obbligatorio",
"brokers": "Broker", "brokers": "Broker",
"add-broker": "Aggiungi broker", "add-broker": "Aggiungi broker",
"host": "Host", "host": "Host",
@ -855,15 +855,15 @@
"add-attribute-request": "Add attribute request", "add-attribute-request": "Add attribute request",
"attribute-updates": "Attribute updates", "attribute-updates": "Attribute updates",
"add-attribute-update": "Add attribute update", "add-attribute-update": "Add attribute update",
"server-side-rpc": "Server side RPC", "server-side-rpc": "RPC lato server",
"add-server-side-rpc-request": "Add server-side RPC request", "add-server-side-rpc-request": "Add server-side RPC request",
"device-name-filter": "Device name filter", "device-name-filter": "Filtro nome dispositivo",
"attribute-filter": "Filtro attributo", "attribute-filter": "Filtro attributo",
"method-filter": "Filtro metodo", "method-filter": "Filtro metodo",
"request-topic-expression": "Request topic expression", "request-topic-expression": "Request topic expression",
"response-timeout": "Response timeout in milliseconds", "response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression", "topic-expression": "Topic expression",
"client-scope": "Client scope", "client-scope": "Visibilità client",
"add-device": "Aggiungi dispositivo", "add-device": "Aggiungi dispositivo",
"opc-server": "Server", "opc-server": "Server",
"opc-add-server": "Aggiungi server", "opc-add-server": "Aggiungi server",
@ -892,19 +892,19 @@
"modbus-baudrate": "Baud rate", "modbus-baudrate": "Baud rate",
"modbus-databits": "Data bits", "modbus-databits": "Data bits",
"modbus-stopbits": "Stop bits", "modbus-stopbits": "Stop bits",
"modbus-databits-range": "Data bits should be in a range from 7 to 8.", "modbus-databits-range": "Data bits deve essere compreso nell'intervallo 7-8.",
"modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", "modbus-stopbits-range": "Stop bits deve essere compreso nell'intervallo 1-2.",
"modbus-unit-id": "Unit ID", "modbus-unit-id": "ID unità",
"modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", "modbus-unit-id-range": "ID unità deve essere compreso nell'intervallo 1-247.",
"modbus-device-name": "Nome dispositivo", "modbus-device-name": "Nome dispositivo",
"modbus-poll-period": "Intervallo di polling (ms)", "modbus-poll-period": "Intervallo di polling (ms)",
"modbus-attributes-poll-period": "Attributes poll period (ms)", "modbus-attributes-poll-period": "Intervallo di polling degli attributi (ms)",
"modbus-timeseries-poll-period": "Timeseries poll period (ms)", "modbus-timeseries-poll-period": "Intervallo di polling delle serie temporali (ms)",
"modbus-poll-period-range": "L'intervallo di polling deve essere un valore positivo.", "modbus-poll-period-range": "L'intervallo di polling deve essere un valore positivo.",
"modbus-tag": "Tag", "modbus-tag": "Tag",
"modbus-function": "Funzione", "modbus-function": "Funzione",
"modbus-register-address": "Indirizzo registro", "modbus-register-address": "Indirizzo registro",
"modbus-register-address-range": "L'indirizzo del registro deve essere compreso tra 0 e 65535.", "modbus-register-address-range": "Indirizzo registro deve essere compreso tra 0 e 65535.",
"modbus-register-bit-index": "Bit index", "modbus-register-bit-index": "Bit index",
"modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.",
"modbus-register-count": "Register count", "modbus-register-count": "Register count",
@ -950,7 +950,7 @@
"scroll-to-top": "Scorri verso l'alto" "scroll-to-top": "Scorri verso l'alto"
}, },
"help": { "help": {
"goto-help-page": "Go to help page" "goto-help-page": "Vai all'help"
}, },
"home": { "home": {
"home": "Home", "home": "Home",
@ -975,9 +975,9 @@
"key-val": { "key-val": {
"key": "Chiave", "key": "Chiave",
"value": "Valore", "value": "Valore",
"remove-entry": "Remove entry", "remove-entry": "Rimuovi voce",
"add-entry": "Add entry", "add-entry": "Aggiungi voce",
"no-data": "No entries" "no-data": "Nessuna voce"
}, },
"layout": { "layout": {
"layout": "Layout", "layout": "Layout",
@ -1002,8 +1002,8 @@
}, },
"login": { "login": {
"login": "Login", "login": "Login",
"request-password-reset": "Request Password Reset", "request-password-reset": "Richiesta reset password",
"reset-password": "Azzera Password", "reset-password": "Reset Password",
"create-password": "Crea Password", "create-password": "Crea Password",
"passwords-mismatch-error": "Le password inserite devono corrispondere!", "passwords-mismatch-error": "Le password inserite devono corrispondere!",
"password-again": "Ripeti Password", "password-again": "Ripeti Password",
@ -1014,7 +1014,7 @@
"password-reset": "Password reset", "password-reset": "Password reset",
"new-password": "Nuova password", "new-password": "Nuova password",
"new-password-again": "Ripeti nuova password", "new-password-again": "Ripeti nuova password",
"password-link-sent-message": "Link azzeramento password inviato con successo!", "password-link-sent-message": "Link reset password inviato con successo!",
"email": "Email" "email": "Email"
}, },
"position": { "position": {
@ -1029,7 +1029,7 @@
"current-password": "Password attuale" "current-password": "Password attuale"
}, },
"relation": { "relation": {
"relations": "Relations", "relations": "Relazioni",
"direction": "Direzione", "direction": "Direzione",
"search-direction": { "search-direction": {
"FROM": "Da", "FROM": "Da",
@ -1039,9 +1039,9 @@
"FROM": "da", "FROM": "da",
"TO": "a" "TO": "a"
}, },
"from-relations": "Outbound relations", "from-relations": "Relazioni in uscita",
"to-relations": "Inbound relations", "to-relations": "Relazioni in ingresso",
"selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected", "selected-relations": "{ count, plural, 1 {1 relazione selezionata} other {# relazioni selezionate} }",
"type": "Tipo", "type": "Tipo",
"to-entity-type": "A tipo entità", "to-entity-type": "A tipo entità",
"to-entity-name": "A nome entità", "to-entity-name": "A nome entità",
@ -1049,26 +1049,26 @@
"from-entity-name": "Da nome entità", "from-entity-name": "Da nome entità",
"to-entity": "A entità", "to-entity": "A entità",
"from-entity": "Da entità", "from-entity": "Da entità",
"delete": "Delete relation", "delete": "Elimina relazione",
"relation-type": "Relation type", "relation-type": "Tipo di relazione",
"relation-type-required": "Relation type is required.", "relation-type-required": "Tipo di relazione obbligatorio.",
"any-relation-type": "Ogni tipo", "any-relation-type": "Ogni tipo",
"add": "Add relation", "add": "Aggiungi relazione",
"edit": "Edit relation", "edit": "Modifica relazione",
"delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", "delete-to-relation-title": "Sei sicuro di voler eliminare la relazione con l'entità '{{entityName}}'?",
"delete-to-relation-text": "Attenzione, dopo la conferma l'entità '{{entityName}}' sarà scollegata dall'entità corrente.", "delete-to-relation-text": "Attenzione, dopo la conferma l'entità '{{entityName}}' sarà scollegata dall'entità corrente.",
"delete-to-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", "delete-to-relations-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 relazione} other {# relazioni} }?",
"delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.", "delete-to-relations-text": "Attenzione, dopo la conferma tutte le relazioni selezionate saranno rimosse e le corrispondenti entità scollegate da quella corrente.",
"delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", "delete-from-relation-title": "Sei sicuro di voler eliminare la relazione dall'entità '{{entityName}}'?",
"delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", "delete-from-relation-text": "Attenzione, dopo la conferma l'entità corrente sarà scollegata dall'entità '{{entityName}}'.",
"delete-from-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", "delete-from-relations-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 relazione} other {# relazioni} }?",
"delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", "delete-from-relations-text": "Attenzione, dopo la conferma tutte le relazioni selezionate saranno rimosse e l'entità corrente scollegata dalle corrispondenti entità.",
"remove-relation-filter": "Remove relation filter", "remove-relation-filter": "Rimuovi filtro relazioni",
"add-relation-filter": "Add relation filter", "add-relation-filter": "Aggiungi filtro relazioni",
"any-relation": "Any relation", "any-relation": "Qualsiasi relazione",
"relation-filters": "Relation filters", "relation-filters": "Filtri relazioni",
"additional-info": "Additional info (JSON)", "additional-info": "Informazioni aggiuntive (JSON)",
"invalid-additional-info": "Unable to parse additional info json." "invalid-additional-info": "Impossibile analizzare le informazioni aggiuntive in JSON."
}, },
"rulechain": { "rulechain": {
"rulechain": "Rule chain", "rulechain": "Rule chain",
@ -1441,7 +1441,8 @@
"it_IT": "Italiano", "it_IT": "Italiano",
"ru_RU": "Russo", "ru_RU": "Russo",
"es_ES": "Spagnolo", "es_ES": "Spagnolo",
"ja_JA": "Giapponese" "ja_JA": "Giapponese",
"TR": "Turco"
} }
} }
} }

3
ui/src/app/locale/locale.constant-ja_JA.json

@ -1457,7 +1457,8 @@
"zh_CN": "中国語", "zh_CN": "中国語",
"ru_RU": "ロシア", "ru_RU": "ロシア",
"es_ES": "スペイン語", "es_ES": "スペイン語",
"ja_JA": "日本語" "ja_JA": "日本語",
"TR": "トルコ語"
} }
} }
} }

3
ui/src/app/locale/locale.constant-ko_KR.json

@ -1335,7 +1335,8 @@
"ru_RU": "러시아어", "ru_RU": "러시아어",
"es_ES": "스페인어", "es_ES": "스페인어",
"it_IT": "이탈리아 사람", "it_IT": "이탈리아 사람",
"ja_JA": "일본어" "ja_JA": "일본어",
"TR": "터키어"
} }
} }
} }

3
ui/src/app/locale/locale.constant-ru_RU.json

@ -1360,7 +1360,8 @@
"es_ES": "Испанский", "es_ES": "Испанский",
"it_IT": "Итальянский", "it_IT": "Итальянский",
"ru_RU": "Русский", "ru_RU": "Русский",
"ja_JA": "Японский" "ja_JA": "Японский",
"TR": "Турецкий"
} }
} }

3
ui/src/app/locale/locale.constant-zh_CN.json

@ -1444,7 +1444,8 @@
"ru_RU": "俄语", "ru_RU": "俄语",
"es_ES": "西班牙语", "es_ES": "西班牙语",
"it_IT": "意大利", "it_IT": "意大利",
"ja_JA": "日本" "ja_JA": "日本",
"TR": "土耳其"
} }
} }
} }

8
ui/src/app/services/menu.service.js

@ -171,7 +171,7 @@ function Menu(userService, $state, $rootScope) {
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
state: 'home.entityViews', state: 'home.entityViews',
icon: 'view_stream' icon: 'view_quilt'
}, },
{ {
name: 'widget.widget-library', name: 'widget.widget-library',
@ -238,7 +238,7 @@ function Menu(userService, $state, $rootScope) {
places: [ places: [
{ {
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
icon: 'view_stream', icon: 'view_quilt',
state: 'home.entityViews' state: 'home.entityViews'
} }
] ]
@ -293,7 +293,7 @@ function Menu(userService, $state, $rootScope) {
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
state: 'home.entityViews', state: 'home.entityViews',
icon: 'view_stream' icon: 'view_quilt'
}, },
{ {
name: 'dashboard.dashboards', name: 'dashboard.dashboards',
@ -328,7 +328,7 @@ function Menu(userService, $state, $rootScope) {
places: [ places: [
{ {
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
icon: 'view_stream', icon: 'view_quilt',
state: 'home.entityViews' state: 'home.entityViews'
} }
] ]

2
ui/src/app/widget/lib/google-map.js

@ -44,7 +44,7 @@ export default class TbGoogleMap {
function initGoogleMap() { function initGoogleMap() {
tbMap.map = new google.maps.Map($containerElement[0], { // eslint-disable-line no-undef tbMap.map = new google.maps.Map($containerElement[0], { // eslint-disable-line no-undef
scrollwheel: false, scrollwheel: true,
mapTypeId: getGoogleMapTypeId(tbMap.defaultMapType), mapTypeId: getGoogleMapTypeId(tbMap.defaultMapType),
zoom: tbMap.defaultZoomLevel || 8 zoom: tbMap.defaultZoomLevel || 8
}); });

Loading…
Cancel
Save