From 3880af82d325baec9c2b9d4a6868f1ff89ce156b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 17 Sep 2019 13:05:27 +0300 Subject: [PATCH] Added Edge entity --- .../main/data/upgrade/2.4.x/schema_update.cql | 32 +++ .../main/data/upgrade/2.4.x/schema_update.sql | 24 ++ .../server/controller/BaseController.java | 19 +- .../server/controller/EdgeController.java | 163 ++++++++++++++ .../install/ThingsboardInstallService.java | 5 + .../CassandraDatabaseUpgradeService.java | 8 + .../install/SqlDatabaseUpgradeService.java | 8 + .../service/security/permission/Resource.java | 3 +- .../permission/TenantAdminPermissions.java | 1 + .../controller/BaseEdgeControllerTest.java | 206 ++++++++++++++++++ .../nosql/EdgeControllerNoSqlTest.java | 23 ++ .../controller/sql/EdgeControllerSqlTest.java | 23 ++ .../server/dao/edge/EdgeService.java | 45 ++++ .../server/common/data/EntityType.java | 2 +- .../server/common/data/edge/Edge.java | 65 ++++++ .../server/common/data/id/EdgeId.java | 43 ++++ .../common/data/id/EntityIdFactory.java | 2 + .../server/dao/edge/BaseEdgeService.java | 156 +++++++++++++ .../server/dao/edge/CassandraEdgeDao.java | 77 +++++++ .../thingsboard/server/dao/edge/EdgeDao.java | 51 +++++ .../server/dao/model/ModelConstants.java | 12 + .../server/dao/model/nosql/EdgeEntity.java | 98 +++++++++ .../server/dao/model/sql/EdgeEntity.java | 108 +++++++++ .../server/dao/sql/edge/EdgeRepository.java | 40 ++++ .../server/dao/sql/edge/JpaEdgeDao.java | 72 ++++++ .../resources/cassandra/schema-entities.cql | 19 +- .../main/resources/sql/schema-entities.sql | 9 + .../test/resources/sql/drop-all-tables.sql | 1 + ui/src/app/api/edge.service.js | 111 ++++++++++ ui/src/app/app.js | 2 + ui/src/app/common/types.constant.js | 11 +- ui/src/app/edge/add-edge.tpl.html | 46 ++++ ui/src/app/edge/edge-card.tpl.html | 21 ++ ui/src/app/edge/edge-fieldset.tpl.html | 49 +++++ ui/src/app/edge/edge.controller.js | 166 ++++++++++++++ ui/src/app/edge/edge.directive.js | 49 +++++ ui/src/app/edge/edge.routes.js | 46 ++++ ui/src/app/edge/edges.tpl.html | 75 +++++++ ui/src/app/edge/index.js | 25 +++ .../entity/entity-autocomplete.directive.js | 6 + ui/src/app/help/help-links.constant.js | 1 + ui/src/app/layout/index.js | 4 +- ui/src/app/locale/locale.constant-en_US.json | 31 +++ ui/src/app/services/menu.service.js | 32 +++ 44 files changed, 1983 insertions(+), 7 deletions(-) create mode 100644 application/src/main/data/upgrade/2.4.x/schema_update.cql create mode 100644 application/src/main/data/upgrade/2.4.x/schema_update.sql create mode 100644 application/src/main/java/org/thingsboard/server/controller/EdgeController.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/nosql/EdgeControllerNoSqlTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/CassandraEdgeDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java create mode 100644 ui/src/app/api/edge.service.js create mode 100644 ui/src/app/edge/add-edge.tpl.html create mode 100644 ui/src/app/edge/edge-card.tpl.html create mode 100644 ui/src/app/edge/edge-fieldset.tpl.html create mode 100644 ui/src/app/edge/edge.controller.js create mode 100644 ui/src/app/edge/edge.directive.js create mode 100644 ui/src/app/edge/edge.routes.js create mode 100644 ui/src/app/edge/edges.tpl.html create mode 100644 ui/src/app/edge/index.js diff --git a/application/src/main/data/upgrade/2.4.x/schema_update.cql b/application/src/main/data/upgrade/2.4.x/schema_update.cql new file mode 100644 index 0000000000..3b25daa2fd --- /dev/null +++ b/application/src/main/data/upgrade/2.4.x/schema_update.cql @@ -0,0 +1,32 @@ +-- +-- Copyright © 2016-2019 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS thingsboard.edge ( + id timeuuid, + tenant_id timeuuid, + name text, + search_text text, + configuration text, + additional_info text, + PRIMARY KEY (id, tenant_id) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.edge_by_tenant_and_search_text AS + SELECT * + from thingsboard.edge + WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, search_text, id ) + WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); \ No newline at end of file diff --git a/application/src/main/data/upgrade/2.4.x/schema_update.sql b/application/src/main/data/upgrade/2.4.x/schema_update.sql new file mode 100644 index 0000000000..7fe32deb9a --- /dev/null +++ b/application/src/main/data/upgrade/2.4.x/schema_update.sql @@ -0,0 +1,24 @@ +-- +-- Copyright © 2016-2019 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS edge ( + id varchar(31) NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + additional_info varchar, + configuration varchar(10000000), + name varchar(255), + search_text varchar(255), + tenant_id varchar(31) +); \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 3fc809c424..23044b9733 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -43,12 +43,14 @@ import org.thingsboard.server.common.data.alarm.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -82,6 +84,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -185,6 +188,9 @@ public abstract class BaseController { @Autowired protected ClaimDevicesService claimDevicesService; + @Autowired + protected EdgeService edgeService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -458,6 +464,18 @@ public abstract class BaseController { } } + Edge checkEdgeId(EdgeId edgeId, Operation operation) throws ThingsboardException { + try { + validateId(edgeId, "Incorrect edgeId " + edgeId); + Edge edge = edgeService.findEdgeById(getTenantId(), edgeId); + checkNotNull(edge); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); + return edge; + } catch (Exception e) { + throw handleException(e, false); + } + } + DashboardInfo checkDashboardInfoId(DashboardId dashboardId, Operation operation) throws ThingsboardException { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); @@ -513,7 +531,6 @@ public abstract class BaseController { return ruleNode; } - protected String constructBaseUrl(HttpServletRequest request) { String scheme = request.getScheme(); if (request.getHeader("x-forwarded-proto") != null) { diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java new file mode 100644 index 0000000000..5580abcc88 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api") +public class EdgeController extends BaseController { + + private static final String EDGE_ID = "edgeId"; + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public Edge getEdgeById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + return checkEdgeId(edgeId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge", method = RequestMethod.POST) + @ResponseBody + public Edge saveEdge(@RequestBody Edge edge) throws ThingsboardException { + try { + edge.setTenantId(getCurrentUser().getTenantId()); + boolean created = edge.getId() == null; + + Operation operation = created ? Operation.CREATE : Operation.WRITE; + + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, + edge.getId(), edge); + + Edge result = checkNotNull(edgeService.saveEdge(edge)); + + logEntityAction(result.getId(), result, null, created ? ActionType.ADDED : ActionType.UPDATED, null); + return result; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), edge, + null, edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edges", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TextPageData getEdges( + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.READ); + TenantId tenantId = getCurrentUser().getTenantId(); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(edgeService.findTenantEdges(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteEdge(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.DELETE); + edgeService.deleteEdge(getTenantId(), edgeId); + + logEntityAction(edgeId, edge, + null, + ActionType.DELETED, null, strEdgeId); + + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.EDGE), + null, + null, + ActionType.DELETED, e, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET) + @ResponseBody + public List getEdgesByIds( + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException { + checkArrayParameter("edgeIds", strEdgeIds); + try { + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.READ); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List edgeIds = new ArrayList<>(); + for (String strEdgeId : strEdgeIds) { + edgeIds.add(new EdgeId(toUUID(strEdgeId))); + } + List edges = checkNotNull(edgeService.findEdgesByIdsAsync(tenantId, edgeIds).get()); + return filterEdgesByReadPermission(edges); + } catch (Exception e) { + throw handleException(e); + } + } + + private List filterEdgesByReadPermission(List edges) { + return edges.stream().filter(edge -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.READ, edge.getId(), edge); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index f384817465..f899f7be13 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -119,6 +119,11 @@ public class ThingsboardInstallService { case "2.4.0": log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ..."); + case "2.4.2": + log.info("Upgrading ThingsBoard from version 2.4.2 to 2.4.x ..."); + + databaseUpgradeService.upgradeDatabase("2.4.2"); + log.info("Updating system data..."); systemDataLoaderService.deleteSystemWidgetBundle("charts"); diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java index c73c12e65f..4886e346ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java @@ -266,6 +266,14 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { Thread.sleep(2500); } catch (InvalidQueryException e) {} log.info("Schema updated."); + break; + case "2.4.2": + + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.x", SCHEMA_UPDATE_CQL); + loadCql(schemaUpdateFile); + log.info("Schema updated."); + break; default: throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index d086e4be8b..eed3ddc263 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -176,6 +176,14 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { log.info("Schema updated."); } break; + case "2.4.2": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.x", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + log.info("Schema updated."); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index e086637028..a696ca31ce 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -31,7 +31,8 @@ public enum Resource { RULE_CHAIN(EntityType.RULE_CHAIN), USER(EntityType.USER), WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE), - WIDGET_TYPE(EntityType.WIDGET_TYPE); + WIDGET_TYPE(EntityType.WIDGET_TYPE), + EDGE(EntityType.EDGE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 99446db889..bfc4e24607 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -42,6 +42,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker); put(Resource.WIDGET_TYPE, widgetsPermissionChecker); + put(Resource.EDGE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java new file mode 100644 index 0000000000..d4b25763f8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -0,0 +1,206 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseEdgeControllerTest extends AbstractControllerTest { + + private IdComparator idComparator; + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + idComparator = new IdComparator<>(); + + savedTenant = doPost("/api/tenant", getNewTenant("My tenant"), Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testFindEdgeById() throws Exception { + Edge savedEdge = getNewSavedEdge("Test edge"); + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertNotNull(foundEdge); + assertEquals(savedEdge, foundEdge); + } + + @Test + public void testSaveEdge() throws Exception { + Edge savedEdge = getNewSavedEdge("Test edge"); + + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + assertEquals(savedTenant.getId(), savedEdge.getTenantId()); + + savedEdge.setName("New test edge"); + doPost("/api/edge", savedEdge, Edge.class); + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + + assertEquals(foundEdge.getName(), savedEdge.getName()); + } + + @Test + public void testDeleteEdge() throws Exception { + Edge edge = getNewSavedEdge("Test edge"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testSaveEdgeWithEmptyName() throws Exception { + Edge edge = new Edge(); + doPost("/api/edge", edge) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Edge name should be specified!"))); + } + + @Test + public void testGetEdges() throws Exception { + + List edges = new ArrayList<>(); + for (int i = 0; i < 178; i++) { + edges.add(getNewSavedEdge("Test edge " + i)); + } + List loadedEdges = loadListOf(new TextPageLink(23), "/api/edges?"); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + assertEquals(edges, loadedEdges); + } + + @Test + public void testGetEdgesByName() throws Exception { + String name1 = "Entity edge1"; + List namesOfEdge1 = fillListOf(143, name1); + List loadedNamesOfEdge1 = loadListOf(new TextPageLink(15, name1), "/api/edges?"); + Collections.sort(namesOfEdge1, idComparator); + Collections.sort(loadedNamesOfEdge1, idComparator); + assertEquals(namesOfEdge1, loadedNamesOfEdge1); + + String name2 = "Entity edge2"; + List namesOfEdge2 = fillListOf(75, name2); + List loadedNamesOfEdge2 = loadListOf(new TextPageLink(4, name2), "/api/edges?"); + Collections.sort(namesOfEdge2, idComparator); + Collections.sort(loadedNamesOfEdge2, idComparator); + assertEquals(namesOfEdge2, loadedNamesOfEdge2); + + for (Edge edge : loadedNamesOfEdge1) { + doDelete("/api/edge/" + edge.getId().getId().toString()).andExpect(status().isOk()); + } + TextPageData pageData = doGetTypedWithPageLink("/api/edges?", + new TypeReference>() { + }, new TextPageLink(4, name1)); + Assert.assertFalse(pageData.hasNext()); + assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedNamesOfEdge2) { + doDelete("/api/edge/" + edge.getId().getId().toString()).andExpect(status().isOk()); + } + pageData = doGetTypedWithPageLink("/api/edges?", new TypeReference>() { + }, new TextPageLink(4, name2)); + Assert.assertFalse(pageData.hasNext()); + assertEquals(0, pageData.getData().size()); + } + + private Edge getNewSavedEdge(String name) throws Exception { + Edge edge = createEdge(name); + return doPost("/api/edge", edge, Edge.class); + } + + private Edge createEdge(String name) { + Edge edge = new Edge(); + edge.setTenantId(savedTenant.getId()); + edge.setName(name); + return edge; + } + + private Tenant getNewTenant(String title) { + Tenant tenant = new Tenant(); + tenant.setTitle(title); + return tenant; + } + + private List fillListOf(int limit, String partOfName) throws Exception { + List edgeNames = new ArrayList<>(); + for (int i = 0; i < limit; i++) { + String fullName = partOfName + ' ' + RandomStringUtils.randomAlphanumeric(15); + fullName = i % 2 == 0 ? fullName.toLowerCase() : fullName.toUpperCase(); + Edge edge = getNewSavedEdge(fullName); + edgeNames.add(doPost("/api/edge", edge, Edge.class)); + } + return edgeNames; + } + + private List loadListOf(TextPageLink pageLink, String urlTemplate) throws Exception { + List loadedItems = new ArrayList<>(); + TextPageData pageData; + do { + pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { + }, pageLink); + loadedItems.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + return loadedItems; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/EdgeControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/EdgeControllerNoSqlTest.java new file mode 100644 index 0000000000..cf9c1978fa --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/nosql/EdgeControllerNoSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.nosql; + +import org.thingsboard.server.controller.BaseEdgeControllerTest; +import org.thingsboard.server.dao.service.DaoNoSqlTest; + +@DaoNoSqlTest +public class EdgeControllerNoSqlTest extends BaseEdgeControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java new file mode 100644 index 0000000000..00589b3169 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseEdgeControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeControllerSqlTest extends BaseEdgeControllerTest { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java new file mode 100644 index 0000000000..00c0ee05f5 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; + +import java.util.List; + +public interface EdgeService { + + Edge saveEdge(Edge edge); + + Edge findEdgeById(TenantId tenantId, EdgeId edgeId); + + ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId); + + ListenableFuture> findEdgesByIdsAsync(TenantId tenantId, List edgeIds); + + List findAllEdges(TenantId tenantId); + + TextPageData findTenantEdges(TenantId tenantId, TextPageLink pageLink); + + void deleteEdge(TenantId tenantId, EdgeId edgeId); + + void deleteEdgesByTenantId(TenantId tenantId); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index fedca0caff..e10ebb66a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, EDGE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java new file mode 100644 index 0000000000..2bd11cdac6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Setter +public class Edge extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { + + private static final long serialVersionUID = 4934987555236873728L; + + private TenantId tenantId; + private CustomerId customerId; + private String name; + private transient JsonNode configuration; + private transient JsonNode additionalInfo; + + public Edge() { + super(); + } + + public Edge(EdgeId id) { + super(id); + } + + public Edge(Edge edge) { + super(edge); + this.tenantId = edge.getTenantId(); + this.name = edge.getName(); + this.configuration = edge.getConfiguration(); + this.additionalInfo = edge.getAdditionalInfo(); + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java new file mode 100644 index 0000000000..4f0c487040 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class EdgeId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public EdgeId(@JsonProperty("id") UUID id) { + super(id); + } + + public static EdgeId fromString(String integrationId) { + return new EdgeId(UUID.fromString(integrationId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.EDGE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index d2dae513e4..a2409cf388 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -63,6 +63,8 @@ public class EntityIdFactory { return new WidgetsBundleId(uuid); case WIDGET_TYPE: return new WidgetTypeId(uuid); + case EDGE: + return new EdgeId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeService.java new file mode 100644 index 0000000000..03036cbf9c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeService.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.tenant.TenantDao; + +import java.util.List; + +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + +@Service +@Slf4j +public class BaseEdgeService extends AbstractEntityService implements EdgeService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + + @Autowired + private EdgeDao edgeDao; + + @Autowired + private TenantDao tenantDao; + + @Override + public Edge saveEdge(Edge edge) { + log.trace("Executing saveEdge [{}]", edge); + edgeValidator.validate(edge, Edge::getTenantId); + return edgeDao.save(edge.getTenantId(), edge); + } + + @Override + public Edge findEdgeById(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findById(tenantId, edgeId.getId()); + } + + @Override + public ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeByIdAsync [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findByIdAsync(tenantId, edgeId.getId()); + } + + @Override + public ListenableFuture> findEdgesByIdsAsync(TenantId tenantId, List edgeIds) { + log.trace("Executing findEdgesByIdsAsync, tenantId [{}], edgeIds [{}]", tenantId, edgeIds); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + return edgeDao.findEdgesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(edgeIds)); + } + + @Override + public List findAllEdges(TenantId tenantId) { + log.trace("Executing findAllEdges"); + return edgeDao.find(tenantId); + } + + @Override + public TextPageData findTenantEdges(TenantId tenantId, TextPageLink pageLink) { + log.trace("Executing findTenantEdges, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); + List edges = edgeDao.findByTenantIdAndPageLink(tenantId.getId(), pageLink); + return new TextPageData<>(edges, pageLink); + } + + @Override + public void deleteEdge(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing deleteEdge [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + deleteEntityRelations(tenantId, edgeId); + edgeDao.removeById(tenantId, edgeId.getId()); + } + + @Override + public void deleteEdgesByTenantId(TenantId tenantId) { + log.trace("Executing deleteEdgesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantEdgesRemover.removeEntities(tenantId, tenantId); + } + + private DataValidator edgeValidator = + new DataValidator() { + + @Override + protected void validateCreate(TenantId tenantId, Edge edge) { + } + + @Override + protected void validateUpdate(TenantId tenantId, Edge edge) { + } + + @Override + protected void validateDataImpl(TenantId tenantId, Edge edge) { + if (StringUtils.isEmpty(edge.getName())) { + throw new DataValidationException("Edge name should be specified!"); + } + if (edge.getTenantId() == null || edge.getTenantId().isNullUid()) { + throw new DataValidationException("Edge should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(tenantId, edge.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Edge is referencing to non-existent tenant!"); + } + } + } + }; + + private PaginatedRemover tenantEdgesRemover = + new PaginatedRemover() { + + @Override + protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + return edgeDao.findByTenantIdAndPageLink(id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Edge entity) { + deleteEdge(tenantId, new EdgeId(entity.getId().getId())); + } + }; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/CassandraEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/CassandraEdgeDao.java new file mode 100644 index 0000000000..49b3881a9f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/CassandraEdgeDao.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge; + +import com.datastax.driver.core.querybuilder.Select; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.nosql.EdgeEntity; +import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; +import org.thingsboard.server.dao.util.NoSqlDao; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.in; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; + +@Component +@Slf4j +@NoSqlDao +public class CassandraEdgeDao extends CassandraAbstractSearchTextDao implements EdgeDao { + + @Override + protected Class getColumnFamilyClass() { + return EdgeEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return EDGE_COLUMN_FAMILY_NAME; + } + + @Override + public List findByTenantIdAndPageLink(UUID tenantId, TextPageLink pageLink) { + log.debug("Try to find edges by tenantId [{}] and pageLink [{}]", tenantId, pageLink); + List edgeEntities = findPageWithTextSearch(new TenantId(tenantId), EDGE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, + Collections.singletonList(eq(EDGE_TENANT_ID_PROPERTY, tenantId)), pageLink); + + log.trace("Found edges [{}] by tenantId [{}] and pageLink [{}]", edgeEntities, tenantId, pageLink); + return DaoUtil.convertDataList(edgeEntities); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds) { + log.debug("Try to find edges by tenantId [{}] and edge Ids [{}]", tenantId, edgeIds); + Select select = select().from(getColumnFamilyName()); + Select.Where query = select.where(); + query.and(eq(EDGE_TENANT_ID_PROPERTY, tenantId)); + query.and(in(ID_PROPERTY, edgeIds)); + return findListByStatementAsync(new TenantId(tenantId), query); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java new file mode 100644 index 0000000000..9bb3e9c9b4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.UUID; + +/** + * The Interface EdgeDao. + * + */ +public interface EdgeDao extends Dao { + + /** + * Find edges by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of edge objects + */ + List findByTenantIdAndPageLink(UUID tenantId, TextPageLink pageLink); + + /** + * Find edges by tenantId and edge Ids. + * + * @param tenantId the tenantId + * @param edgeIds the edge Ids + * @return the list of edge objects + */ + ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds); + + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index f065443489..987d0e69dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -347,6 +347,18 @@ public class ModelConstants { public static final String RULE_NODE_NAME_PROPERTY = "name"; public static final String RULE_NODE_CONFIGURATION_PROPERTY = "configuration"; + /** + * Cassandra edge constants. + */ + public static final String EDGE_COLUMN_FAMILY_NAME = "edge"; + public static final String EDGE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String EDGE_NAME_PROPERTY = "name"; + public static final String EDGE_CONFIGURATION_PROPERTY = "configuration"; + public static final String EDGE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + + public static final String EDGE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_tenant_and_search_text"; + + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java new file mode 100644 index 0000000000..1196d14314 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.nosql; + +import com.datastax.driver.core.utils.UUIDs; +import com.datastax.driver.mapping.annotations.ClusteringColumn; +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.PartitionKey; +import com.datastax.driver.mapping.annotations.Table; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.model.type.JsonCodec; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ADDITIONAL_INFO_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CONFIGURATION_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@Table(name = EDGE_COLUMN_FAMILY_NAME) +public class EdgeEntity implements SearchTextEntity { + + @PartitionKey + @Column(name = ID_PROPERTY) + private UUID id; + + @ClusteringColumn + @Column(name = EDGE_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = EDGE_NAME_PROPERTY) + private String name; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = EDGE_CONFIGURATION_PROPERTY, codec = JsonCodec.class) + private JsonNode configuration; + + @Column(name = EDGE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) + private JsonNode additionalInfo; + + public EdgeEntity() { + super(); + } + + public EdgeEntity(Edge edge) { + if (edge.getId() != null) { + this.id = edge.getId().getId(); + } + if (edge.getTenantId() != null) { + this.tenantId = edge.getTenantId().getId(); + } + this.name = edge.getName(); + this.configuration = edge.getConfiguration(); + this.additionalInfo = edge.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return getName(); + } + + @Override + public Edge toData() { + Edge edge = new Edge(new EdgeId(id)); + edge.setCreatedTime(UUIDs.unixTimestamp(id)); + if (tenantId != null) { + edge.setTenantId(new TenantId(tenantId)); + } + edge.setName(name); + edge.setConfiguration(configuration); + edge.setAdditionalInfo(additionalInfo); + return edge; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java new file mode 100644 index 0000000000..f70028e5da --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = EDGE_COLUMN_FAMILY_NAME) +public class EdgeEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = EDGE_TENANT_ID_PROPERTY) + private String tenantId; + + @Column(name = EDGE_NAME_PROPERTY) + private String name; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Type(type = "json") + @Column(name = ModelConstants.EDGE_CONFIGURATION_PROPERTY) + private JsonNode configuration; + + @Type(type = "json") + @Column(name = ModelConstants.EDGE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public EdgeEntity() { + super(); + } + + public EdgeEntity(Edge edge) { + if (edge.getId() != null) { + this.setId(edge.getId().getId()); + } + if (edge.getTenantId() != null) { + this.tenantId = UUIDConverter.fromTimeUUID(edge.getTenantId().getId()); + } + this.name = edge.getName(); + this.configuration = edge.getConfiguration(); + this.additionalInfo = edge.getAdditionalInfo(); + } + + public String getSearchText() { + return searchText; + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + @Override + public Edge toData() { + Edge edge = new Edge(new EdgeId(UUIDConverter.fromString(id))); + edge.setCreatedTime(UUIDs.unixTimestamp(UUIDConverter.fromString(id))); + if (tenantId != null) { + edge.setTenantId(new TenantId(UUIDConverter.fromString(tenantId))); + } + edge.setName(name); + edge.setConfiguration(configuration); + edge.setAdditionalInfo(additionalInfo); + return edge; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java new file mode 100644 index 0000000000..498e828501 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.edge; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.EdgeEntity; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; + +@SqlDao +public interface EdgeRepository extends CrudRepository { + + @Query("SELECT a FROM EdgeEntity a WHERE a.tenantId = :tenantId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + + "AND a.id > :idOffset ORDER BY a.id") + List findByTenantIdAndPageLink(@Param("tenantId") String tenantId, + @Param("textSearch") String textSearch, + @Param("idOffset") String idOffset, + Pageable pageable); + + List findEdgesByTenantIdAndIdIn(String tenantId, List edgeIds); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java new file mode 100644 index 0000000000..3d323a4e65 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.edge.EdgeDao; +import org.thingsboard.server.dao.model.sql.EdgeEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +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; + +@Component +@SqlDao +public class JpaEdgeDao extends JpaAbstractSearchTextDao implements EdgeDao { + + @Autowired + private EdgeRepository edgeRepository; + + @Override + public List findByTenantIdAndPageLink(UUID tenantId, TextPageLink pageLink) { + return DaoUtil.convertDataList(edgeRepository + .findByTenantIdAndPageLink( + fromTimeUUID(tenantId), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds) { + return service.submit(() -> DaoUtil.convertDataList(edgeRepository.findEdgesByTenantIdAndIdIn(UUIDConverter.fromTimeUUID(tenantId), fromTimeUUIDs(edgeIds)))); + } + + @Override + protected Class getEntityClass() { + return EdgeEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return edgeRepository; + } + +} diff --git a/dao/src/main/resources/cassandra/schema-entities.cql b/dao/src/main/resources/cassandra/schema-entities.cql index 611c08d5ef..58654504dd 100644 --- a/dao/src/main/resources/cassandra/schema-entities.cql +++ b/dao/src/main/resources/cassandra/schema-entities.cql @@ -711,4 +711,21 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_ent 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); \ No newline at end of file + WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); + +CREATE TABLE IF NOT EXISTS thingsboard.edge ( + id timeuuid, + tenant_id timeuuid, + name text, + search_text text, + configuration text, + additional_info text, + PRIMARY KEY (id, tenant_id) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.edge_by_tenant_and_search_text AS + SELECT * + from thingsboard.edge + WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, search_text, id ) + WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); \ No newline at end of file diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 2903ca2fb1..c95c2f1036 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -243,3 +243,12 @@ CREATE TABLE IF NOT EXISTS entity_view ( search_text varchar(255), additional_info varchar ); + +CREATE TABLE IF NOT EXISTS edge ( + id varchar(31) NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + additional_info varchar, + configuration varchar(10000000), + name varchar(255), + search_text varchar(255), + tenant_id varchar(31) +); \ No newline at end of file diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql index 1bdc1a7ece..8946f77d6c 100644 --- a/dao/src/test/resources/sql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/drop-all-tables.sql @@ -20,3 +20,4 @@ DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; +DROP TABLE IF EXISTS edge; \ No newline at end of file diff --git a/ui/src/app/api/edge.service.js b/ui/src/app/api/edge.service.js new file mode 100644 index 0000000000..11bad6bc8e --- /dev/null +++ b/ui/src/app/api/edge.service.js @@ -0,0 +1,111 @@ +/* + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export default angular.module('thingsboard.api.edge', []) + .factory('edgeService', EdgeService) + .name; + +/*@ngInject*/ +function EdgeService($http, $q) { + + var service = { + getEdges: getEdges, + getEdgesByIds: getEdgesByIds, + getEdge: getEdge, + deleteEdge: deleteEdge, + saveEdge: saveEdge + }; + + return service; + + function getEdges(pageLink, config) { + var deferred = $q.defer(); + var url = '/api/edges?limit=' + pageLink.limit; + if (angular.isDefined(pageLink.textSearch)) { + url += '&textSearch=' + pageLink.textSearch; + } + if (angular.isDefined(pageLink.idOffset)) { + url += '&idOffset=' + pageLink.idOffset; + } + if (angular.isDefined(pageLink.textOffset)) { + url += '&textOffset=' + pageLink.textOffset; + } + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getEdgesByIds(edgeIds, config) { + var deferred = $q.defer(); + var ids = ''; + for (var i=0;i0) { + ids += ','; + } + ids += edgeIds[i]; + } + var url = '/api/edges?edgeIds=' + ids; + $http.get(url, config).then(function success(response) { + var entities = response.data; + entities.sort(function (entity1, entity2) { + var id1 = entity1.id.id; + var id2 = entity2.id.id; + var index1 = edgeIds.indexOf(id1); + var index2 = edgeIds.indexOf(id2); + return index1 - index2; + }); + deferred.resolve(entities); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getEdge(edgeId, config) { + var deferred = $q.defer(); + var url = '/api/edge/' + edgeId; + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } + + function saveEdge(edge) { + var deferred = $q.defer(); + var url = '/api/edge'; + $http.post(url, edge).then(function success(response) { + deferred.resolve(response.data); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } + + function deleteEdge(edgeId) { + var deferred = $q.defer(); + var url = '/api/edge/' + edgeId; + $http.delete(url).then(function success() { + deferred.resolve(); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } +} diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 27bf94a19f..d5a603f430 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -99,6 +99,7 @@ import thingsboardApiAlarm from './api/alarm.service'; import thingsboardApiAuditLog from './api/audit-log.service'; import thingsboardApiComponentDescriptor from './api/component-descriptor.service'; import thingsboardApiRuleChain from './api/rule-chain.service'; +import thingsboardApiEdge from './api/edge.service'; import AppConfig from './app.config'; import GlobalInterceptor from './global-interceptor.service'; @@ -157,6 +158,7 @@ angular.module('thingsboard', [ thingsboardApiAuditLog, thingsboardApiComponentDescriptor, thingsboardApiRuleChain, + thingsboardApiEdge, uiRouter]) .config(AppConfig) .factory('globalInterceptor', GlobalInterceptor) diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 37ccd91302..354cf9fccb 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -358,7 +358,8 @@ export default angular.module('thingsboard.types', []) alarm: "ALARM", rulechain: "RULE_CHAIN", rulenode: "RULE_NODE", - entityView: "ENTITY_VIEW" + entityView: "ENTITY_VIEW", + edge: "EDGE" }, importEntityColumnType: { name: { @@ -461,7 +462,13 @@ export default angular.module('thingsboard.types', []) "CURRENT_CUSTOMER": { type: 'entity.type-current-customer', list: 'entity.type-current-customer' - } + }, + "EDGE": { + type: 'entity.type-edge', + typePlural: 'entity.type-edges', + list: 'entity.list-of-edges', + nameStartsWith: 'entity.edge-name-starts-with' + }, }, entitySearchDirection: { from: "FROM", diff --git a/ui/src/app/edge/add-edge.tpl.html b/ui/src/app/edge/add-edge.tpl.html new file mode 100644 index 0000000000..7e4cc2d869 --- /dev/null +++ b/ui/src/app/edge/add-edge.tpl.html @@ -0,0 +1,46 @@ + + +
+ +
+

edge.add

+ +
+ + + +
+
+ + + +
+ +
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | translate }} + +
+
+ diff --git a/ui/src/app/edge/edge-card.tpl.html b/ui/src/app/edge/edge-card.tpl.html new file mode 100644 index 0000000000..4ce4e28176 --- /dev/null +++ b/ui/src/app/edge/edge-card.tpl.html @@ -0,0 +1,21 @@ + +
+
{{ vm.types.edgeType[vm.item.type].name | translate }}
+
{{vm.item.additionalInfo.description}}
+
diff --git a/ui/src/app/edge/edge-fieldset.tpl.html b/ui/src/app/edge/edge-fieldset.tpl.html new file mode 100644 index 0000000000..c4990c8b8b --- /dev/null +++ b/ui/src/app/edge/edge-fieldset.tpl.html @@ -0,0 +1,49 @@ + +{{ 'edge.export' | translate }} +{{ 'edge.delete' | translate }} + +
+ + + edge.copyId + +
+ + +
+ + + +
+
edge.name-required
+
+
+ + + + +
+
diff --git a/ui/src/app/edge/edge.controller.js b/ui/src/app/edge/edge.controller.js new file mode 100644 index 0000000000..efd999c7ae --- /dev/null +++ b/ui/src/app/edge/edge.controller.js @@ -0,0 +1,166 @@ +/* + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import addEdgeTemplate from './add-edge.tpl.html'; +import edgeCard from './edge-card.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export function EdgeCardController(types) { + + var vm = this; + + vm.types = types; +} + + +/*@ngInject*/ +export function EdgeController($rootScope, userService, edgeService, $state, $stateParams, + $document, $mdDialog, $q, $translate, types, securityTypes, userPermissionsService) { + + var edgeActionsList = []; + + var edgeGroupActionsList = []; + + var vm = this; + + vm.types = types; + + vm.edgeGridConfig = { + + resource: securityTypes.resource.edge, + + deleteItemTitleFunc: deleteEdgeTitle, + deleteItemContentFunc: deleteEdgeText, + deleteItemsTitleFunc: deleteEdgesTitle, + deleteItemsActionTitleFunc: deleteEdgesActionTitle, + deleteItemsContentFunc: deleteEdgesText, + + saveItemFunc: saveEdge, + + getItemTitleFunc: getEdgeTitle, + + itemCardController: 'EdgeCardController', + itemCardTemplateUrl: edgeCard, + parentCtl: vm, + + actionsList: edgeActionsList, + groupActionsList: edgeGroupActionsList, + + onGridInited: gridInited, + + addItemTemplateUrl: addEdgeTemplate, + + addItemText: function() { return $translate.instant('edge.add-edge-text') }, + noItemsText: function() { return $translate.instant('edge.no-edges-text') }, + itemDetailsText: function() { return $translate.instant('edge.edge-details') } + }; + + if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { + vm.edgeGridConfig.items = $stateParams.items; + } + + if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { + vm.edgeGridConfig.topIndex = $stateParams.topIndex; + } + + initController(); + + function initController() { + var fetchEdgesFunction = function (pageLink, edgeType) { + return edgeService.getEdges(pageLink, true, edgeType); + }; + var deleteEdgeFunction = function (edgeId) { + return edgeService.deleteEdge(edgeId); + }; + var refreshEdgesParamsFunction = function() { + return {"topIndex": vm.topIndex}; + }; + + edgeActionsList.push( + { + onAction: function ($event, item) { + vm.grid.deleteItem($event, item); + }, + name: function() { return $translate.instant('action.delete') }, + details: function() { return $translate.instant('edge.delete') }, + icon: "delete", + isEnabled: function() { + return userPermissionsService.hasGenericPermission(securityTypes.resource.edge, securityTypes.operation.delete); + } + } + ); + + edgeGroupActionsList.push( + { + onAction: function ($event) { + vm.grid.deleteItems($event); + }, + name: function() { return $translate.instant('edge.delete-edges') }, + details: deleteEdgesActionTitle, + icon: "delete" + } + ); + vm.edgeGridConfig.refreshParamsFunc = refreshEdgesParamsFunction; + vm.edgeGridConfig.fetchItemsFunc = fetchEdgesFunction; + vm.edgeGridConfig.deleteItemFunc = deleteEdgeFunction; + + } + + function deleteEdgeTitle(edge) { + return $translate.instant('edge.delete-edge-title', {edgeName: edge.name}); + } + + function deleteEdgeText() { + return $translate.instant('edge.delete-edge-text'); + } + + function deleteEdgesTitle(selectedCount) { + return $translate.instant('edge.delete-edges-title', {count: selectedCount}, 'messageformat'); + } + + function deleteEdgesActionTitle(selectedCount) { + return $translate.instant('edge.delete-edges-action-title', {count: selectedCount}, 'messageformat'); + } + + function deleteEdgesText () { + return $translate.instant('edge.delete-edges-text'); + } + + function gridInited(grid) { + vm.grid = grid; + } + + function getEdgeTitle(edge) { + return edge ? edge.name : ''; + } + + function saveEdge(edge) { + var deferred = $q.defer(); + edgeService.saveEdge(edge).then( + function success(savedEdge) { + $rootScope.$broadcast('edgeSaved'); + deferred.resolve(savedEdge); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } +} diff --git a/ui/src/app/edge/edge.directive.js b/ui/src/app/edge/edge.directive.js new file mode 100644 index 0000000000..2261421aa4 --- /dev/null +++ b/ui/src/app/edge/edge.directive.js @@ -0,0 +1,49 @@ +/* + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import edgeFieldsetTemplate from './edge-fieldset.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EdgeDirective($compile, $templateCache, $translate, $mdDialog, $document, toast, types) { + var linker = function (scope, element) { + var template = $templateCache.get(edgeFieldsetTemplate); + element.html(template); + + scope.types = types; + + scope.onEdgeIdCopied = function() { + toast.showSuccess($translate.instant('edge.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + + $compile(element.contents())(scope); + + }; + return { + restrict: "E", + link: linker, + scope: { + edge: '=', + isEdit: '=', + theForm: '=', + isCreate: '<', + onExportEdge: '&', + onDeleteEdge: '&' + } + }; +} diff --git a/ui/src/app/edge/edge.routes.js b/ui/src/app/edge/edge.routes.js new file mode 100644 index 0000000000..1c3199a085 --- /dev/null +++ b/ui/src/app/edge/edge.routes.js @@ -0,0 +1,46 @@ +/* + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import edgesTemplate from './edges.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EdgeRoutes($stateProvider) { + + $stateProvider + .state('home.edges', { + url: '/edges', + params: {'topIndex': 0}, + module: 'private', + auth: ['TENANT_ADMIN'], + views: { + "content@home": { + templateUrl: edgesTemplate, + controllerAs: 'vm', + controller: 'EdgeController' + } + }, + data: { + searchEnabled: true, + pageTitle: 'edge.edges' + }, + ncyBreadcrumb: { + label: '{"icon": "transform", "label": "edge.edges"}' + } + }); +} diff --git a/ui/src/app/edge/edges.tpl.html b/ui/src/app/edge/edges.tpl.html new file mode 100644 index 0000000000..9d296c4a5b --- /dev/null +++ b/ui/src/app/edge/edges.tpl.html @@ -0,0 +1,75 @@ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ui/src/app/edge/index.js b/ui/src/app/edge/index.js new file mode 100644 index 0000000000..604fba2053 --- /dev/null +++ b/ui/src/app/edge/index.js @@ -0,0 +1,25 @@ +/* + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import EdgeRoutes from './edge.routes'; +import {EdgeController, EdgeCardController} from './edge.controller'; +import EdgeDirective from './edge.directive'; + +export default angular.module('thingsboard.edge', []) + .config(EdgeRoutes) + .controller('EdgeController', EdgeController) + .controller('EdgeCardController', EdgeCardController) + .directive('tbEdge', EdgeDirective) + .name; diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js index c091e782f3..8d8bcee7d0 100644 --- a/ui/src/app/entity/entity-autocomplete.directive.js +++ b/ui/src/app/entity/entity-autocomplete.directive.js @@ -179,6 +179,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter scope.noEntitiesMatchingText = 'customer.no-customers-matching'; scope.entityRequiredText = 'customer.default-customer-required'; break; + case types.entityType.edge: + scope.selectEntityText = 'edge.select-edge'; + scope.entityText = 'edge.edge'; + scope.noEntitiesMatchingText = 'edge.no-edges-matching'; + scope.entityRequiredText = 'edge.edge-required'; + break; } if (scope.entity && scope.entity.id.entityType != scope.entityType) { scope.entity = null; diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js index 9619d71475..fc8082e54e 100644 --- a/ui/src/app/help/help-links.constant.js +++ b/ui/src/app/help/help-links.constant.js @@ -107,6 +107,7 @@ export default angular.module('thingsboard.help', []) widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm", widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static", + edges: helpBaseUrl + "/docs/user-guide/ui/edges" }, getRuleNodeLink: function(ruleNode) { if (ruleNode && ruleNode.component) { diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index c0ef817a3b..30cb9feebc 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -52,6 +52,7 @@ import thingsboardEntityView from '../entity-view'; import thingsboardWidgetLibrary from '../widget'; import thingsboardDashboard from '../dashboard'; import thingsboardRuleChain from '../rulechain'; +import thingsboardEdge from '../edge'; import thingsboardJsonForm from '../jsonform'; @@ -94,7 +95,8 @@ export default angular.module('thingsboard.home', [ thingsboardDashboardAutocomplete, thingsboardKvMap, thingsboardJsonObjectEdit, - thingsboardJsonContent + thingsboardJsonContent, + thingsboardEdge ]) .config(HomeRoutes) .controller('HomeController', HomeController) diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 006db27e14..95b4e22fb4 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -705,6 +705,33 @@ "column": "Column", "row": "Row" }, + "edge": { + "edge": "Edge", + "edges": "Edges", + "management": "Edge management", + "no-edges-matching": "No edges matching '{{entity}}' were found.", + "add": "Add Edge", + "view": "View Edge", + "no-edges-text": "No edges found", + "edge-details": "Edge details", + "add-edge-text": "Add new edge", + "delete": "Delete edge", + "delete-edges": "Delete edges", + "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", + "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", + "delete-edges-title": "Are you sure you want to edge { count, plural, 1 {1 edge} other {# edges} }?", + "delete-edges-action-title": "Delete { count, plural, 1 {1 edge} other {# edges} }", + "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "events": "Events", + "details": "Details", + "copyId": "Copy role Id", + "idCopiedMessage": "Edge Id has been copied to clipboard", + "permissions": "Permissions", + "edge-required": "Edge required", + }, "error": { "unable-to-connect": "Unable to connect to the server! Please check your internet connection.", "unhandled-error-code": "Unhandled error code: {{errorCode}}", @@ -798,6 +825,10 @@ "type-rulenodes": "Rule nodes", "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", "rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'", + "type-edge": "Edge", + "type-edges": "Edges", + "list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }", + "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "type-current-customer": "Current Customer", "search": "Search entities", "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected", diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js index 5aef2559fc..09bdea44c5 100644 --- a/ui/src/app/services/menu.service.js +++ b/ui/src/app/services/menu.service.js @@ -184,6 +184,12 @@ function Menu(userService, $state, $rootScope) { state: 'home.entityViews', icon: 'view_quilt' }, + { + name: 'edge.edges', + type: 'link', + state: 'home.edges', + icon: 'toys' + }, { name: 'widget.widget-library', type: 'link', @@ -254,6 +260,16 @@ function Menu(userService, $state, $rootScope) { } ] }, + { + name: 'edge.management', + places: [ + { + name: 'edge.edges', + icon: 'toys', + state: 'home.edges' + } + ] + }, { name: 'dashboard.management', places: [ @@ -306,6 +322,12 @@ function Menu(userService, $state, $rootScope) { state: 'home.entityViews', icon: 'view_quilt' }, + { + name: 'edge.edges', + type: 'link', + state: 'home.edges', + icon: 'toys' + }, { name: 'dashboard.dashboards', type: 'link', @@ -344,6 +366,16 @@ function Menu(userService, $state, $rootScope) { } ] }, + { + name: 'edge.management', + places: [ + { + name: 'edge.edges', + icon: 'toys', + state: 'home.edges' + } + ] + }, { name: 'dashboard.view-dashboards', places: [