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 9b00682d02..d4adebeccc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -307,7 +307,7 @@ public abstract class BaseController { } } - private void checkDevice(Device device) throws ThingsboardException { + protected void checkDevice(Device device) throws ThingsboardException { checkNotNull(device); checkTenantId(device.getTenantId()); if (device.getCustomerId() != null && !device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { @@ -380,14 +380,26 @@ public abstract class BaseController { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); Dashboard dashboard = dashboardService.findDashboardById(dashboardId); - checkDashboard(dashboard); + checkDashboard(dashboard, true); return dashboard; } catch (Exception e) { throw handleException(e, false); } } - private void checkDashboard(Dashboard dashboard) throws ThingsboardException { + DashboardInfo checkDashboardInfoId(DashboardId dashboardId) throws ThingsboardException { + try { + validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(dashboardId); + SecurityUser authUser = getCurrentUser(); + checkDashboard(dashboardInfo, authUser.getAuthority() != Authority.SYS_ADMIN); + return dashboardInfo; + } catch (Exception e) { + throw handleException(e, false); + } + } + + private void checkDashboard(DashboardInfo dashboard, boolean checkCustomerId) throws ThingsboardException { checkNotNull(dashboard); checkTenantId(dashboard.getTenantId()); SecurityUser authUser = getCurrentUser(); @@ -397,7 +409,8 @@ public abstract class BaseController { ThingsboardErrorCode.PERMISSION_DENIED); } } - if (dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + if (checkCustomerId && + dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { checkCustomerId(dashboard.getCustomerId()); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 3812610d20..2a6416ce98 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -41,6 +41,19 @@ public class DashboardController extends BaseController { return System.currentTimeMillis(); } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET) + @ResponseBody + public DashboardInfo getDashboardInfoById(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException { + checkParameter("dashboardId", strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + return checkDashboardInfoId(dashboardId); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) @ResponseBody @@ -132,6 +145,25 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = { "limit" }, method = RequestMethod.GET) + @ResponseBody + public TextPageData getTenantDashboards( + @PathVariable("tenantId") String strTenantId, + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + try { + TenantId tenantId = new TenantId(toUUID(strTenantId)); + checkTenantId(tenantId); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index bebab8b880..7cd381c57a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -24,19 +24,18 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.device.DeviceSearchQuery; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.exception.ThingsboardException; -import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.ArrayList; import java.util.List; -import java.util.UUID; +import java.util.stream.Collectors; @RestController @RequestMapping("/api") @@ -238,4 +237,28 @@ public class DeviceController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/devices", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getDeviceTypes()); + checkEntityId(query.getParameters().getEntityId()); + try { + List devices = checkNotNull(deviceService.findDevicesByQuery(query).get()); + devices = devices.stream().filter(device -> { + try { + checkDevice(device); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return devices; + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 6b312836b3..92e8655c72 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -28,6 +28,7 @@ public class Device extends SearchTextBased { private TenantId tenantId; private CustomerId customerId; private String name; + private String type; private JsonNode additionalInfo; public Device() { @@ -43,6 +44,7 @@ public class Device extends SearchTextBased { this.tenantId = device.getTenantId(); this.customerId = device.getCustomerId(); this.name = device.getName(); + this.type = device.getType(); this.additionalInfo = device.getAdditionalInfo(); } @@ -70,6 +72,14 @@ public class Device extends SearchTextBased { this.name = name; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public JsonNode getAdditionalInfo() { return additionalInfo; } @@ -90,6 +100,7 @@ public class Device extends SearchTextBased { result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode()); result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); return result; } @@ -118,6 +129,11 @@ public class Device extends SearchTextBased { return false; } else if (!name.equals(other.name)) return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; if (tenantId == null) { if (other.tenantId != null) return false; @@ -135,6 +151,8 @@ public class Device extends SearchTextBased { builder.append(customerId); builder.append(", name="); builder.append(name); + builder.append(", type="); + builder.append(type); builder.append(", additionalInfo="); builder.append(additionalInfo); builder.append(", createdTime="); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 8c860646bd..b0ebbfd5f0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -26,7 +26,9 @@ import org.thingsboard.server.common.data.page.TextPageLink; public interface DashboardService { public Dashboard findDashboardById(DashboardId dashboardId); - + + public DashboardInfo findDashboardInfoById(DashboardId dashboardId); + public Dashboard saveDashboard(Dashboard dashboard); public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 826404241b..cf554f3a62 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -64,6 +64,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard return getData(dashboardEntity); } + @Override + public DashboardInfo findDashboardInfoById(DashboardId dashboardId) { + log.trace("Executing findDashboardInfoById [{}]", dashboardId); + Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + DashboardInfoEntity dashboardInfoEntity = dashboardInfoDao.findById(dashboardId.getId()); + return getData(dashboardInfoEntity); + } + @Override public Dashboard saveDashboard(Dashboard dashboard) { log.trace("Executing saveDashboard [{}]", dashboard); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java new file mode 100644 index 0000000000..eb9d9dee3d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.dao.relation.EntityTypeFilter; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; + +@Data +public class DeviceSearchQuery { + + private RelationsSearchParameters parameters; + @Nullable + private String relationType; + @Nullable + private List deviceTypes; + + public EntityRelationsQuery toEntitySearchQuery() { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(parameters); + query.setFilters( + Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(EntityType.DEVICE)))); + return query; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 35d34968cf..4715435dec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -53,4 +53,7 @@ public interface DeviceService { ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); + + ListenableFuture> findDevicesByQuery(DeviceSearchQuery query); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 43949017f4..ab6fa4e7fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -24,11 +25,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.customer.CustomerDao; @@ -37,20 +41,20 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.CustomerEntity; import org.thingsboard.server.dao.model.DeviceEntity; import org.thingsboard.server.dao.model.TenantEntity; +import org.thingsboard.server.dao.relation.EntitySearchDirection; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; +import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; -import static org.thingsboard.server.dao.DaoUtil.convertDataList; -import static org.thingsboard.server.dao.DaoUtil.getData; -import static org.thingsboard.server.dao.DaoUtil.toUUIDs; +import static org.thingsboard.server.dao.DaoUtil.*; 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.validateIds; -import static org.thingsboard.server.dao.service.Validator.validatePageLink; +import static org.thingsboard.server.dao.service.Validator.*; @Service @Slf4j @@ -194,6 +198,32 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic new CustomerDevicesUnassigner(tenantId).removeEntitites(customerId); } + @Override + public ListenableFuture> findDevicesByQuery(DeviceSearchQuery query) { + ListenableFuture> relations = relationService.findByQuery(query.toEntitySearchQuery()); + ListenableFuture> devices = Futures.transform(relations, (AsyncFunction, List>) relations1 -> { + EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); + List> futures = new ArrayList<>(); + for (EntityRelation relation : relations1) { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + if (entityId.getEntityType() == EntityType.DEVICE) { + futures.add(findDeviceByIdAsync(new DeviceId(entityId.getId()))); + } + } + return Futures.successfulAsList(futures); + }); + + devices = Futures.transform(devices, new Function, List>() { + @Nullable + @Override + public List apply(@Nullable List deviceList) { + return deviceList.stream().filter(device -> query.getDeviceTypes().contains(device.getType())).collect(Collectors.toList()); + } + }); + + return devices; + } + private DataValidator deviceValidator = new DataValidator() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java index 740bf2ac44..69ed92cc1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java @@ -51,7 +51,10 @@ public final class DeviceEntity implements SearchTextEntity { @Column(name = DEVICE_NAME_PROPERTY) private String name; - + + @Column(name = DEVICE_TYPE_PROPERTY) + private String type; + @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; @@ -73,6 +76,7 @@ public final class DeviceEntity implements SearchTextEntity { this.customerId = device.getCustomerId().getId(); } this.name = device.getName(); + this.type = device.getType(); this.additionalInfo = device.getAdditionalInfo(); } @@ -108,6 +112,14 @@ public final class DeviceEntity implements SearchTextEntity { this.name = name; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public JsonNode getAdditionalInfo() { return additionalInfo; } @@ -138,6 +150,7 @@ public final class DeviceEntity implements SearchTextEntity { result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); return result; } @@ -171,6 +184,11 @@ public final class DeviceEntity implements SearchTextEntity { return false; } else if (!name.equals(other.name)) return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; if (tenantId == null) { if (other.tenantId != null) return false; @@ -190,6 +208,8 @@ public final class DeviceEntity implements SearchTextEntity { builder.append(customerId); builder.append(", name="); builder.append(name); + builder.append(", type="); + builder.append(type); builder.append(", additionalInfo="); builder.append(additionalInfo); builder.append("]"); @@ -207,6 +227,7 @@ public final class DeviceEntity implements SearchTextEntity { device.setCustomerId(new CustomerId(customerId)); } device.setName(name); + device.setType(type); device.setAdditionalInfo(additionalInfo); return device; } 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 2efbd5dfd7..0c68c39e33 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 @@ -120,6 +120,7 @@ public class ModelConstants { public static final String DEVICE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY; public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String DEVICE_NAME_PROPERTY = "name"; + public static final String DEVICE_TYPE_PROPERTY = "type"; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql index efaa987f5e..071697eac9 100644 --- a/dao/src/main/resources/schema.cql +++ b/dao/src/main/resources/schema.cql @@ -156,6 +156,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.device ( tenant_id timeuuid, customer_id timeuuid, name text, + type text, search_text text, additional_info text, PRIMARY KEY (id, tenant_id, customer_id) @@ -271,11 +272,11 @@ CREATE TABLE IF NOT EXISTS thingsboard.relation ( ) WITH CLUSTERING ORDER BY ( relation_type ASC, to_id ASC, to_type ASC); CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS -SELECT * -from thingsboard.relation -WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL -PRIMARY KEY ((to_id, to_type), relation_type, from_id, from_type) -WITH CLUSTERING ORDER BY ( relation_type ASC, from_id ASC, from_type ASC); + SELECT * + from thingsboard.relation + WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL + PRIMARY KEY ((to_id, to_type), relation_type, from_id, from_type) + WITH CLUSTERING ORDER BY ( relation_type ASC, from_id ASC, from_type ASC); CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle ( id timeuuid, diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java new file mode 100644 index 0000000000..3f5c55affd --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java @@ -0,0 +1,283 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import com.datastax.driver.core.utils.UUIDs; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.dao.relation.EntitySearchDirection; +import org.thingsboard.server.dao.relation.EntityTypeFilter; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class RelationServiceImplTest extends AbstractServiceTest { + + @Before + public void before() { + } + + @After + public void after() { + } + + @Test + public void testSaveRelation() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(UUIDs.timeBased()); + AssetId childId = new AssetId(UUIDs.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(saveRelation(relation)); + + Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get()); + + Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").get()); + } + + @Test + public void testDeleteRelation() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(UUIDs.timeBased()); + AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId subChildId = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + saveRelation(relationB); + + Assert.assertTrue(relationService.deleteRelation(relationA).get()); + + Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get()); + } + + @Test + public void testDeleteEntityRelations() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(UUIDs.timeBased()); + AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId subChildId = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + saveRelation(relationB); + + Assert.assertTrue(relationService.deleteEntityRelations(childId).get()); + + Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get()); + } + + @Test + public void testFindFrom() throws ExecutionException, InterruptedException { + AssetId parentA = new AssetId(UUIDs.timeBased()); + AssetId parentB = new AssetId(UUIDs.timeBased()); + AssetId childA = new AssetId(UUIDs.timeBased()); + AssetId childB = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE); + EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE); + + EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE); + EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE); + + saveRelation(relationA1); + saveRelation(relationA2); + + saveRelation(relationB1); + saveRelation(relationB2); + + List relations = relationService.findByFrom(parentA).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(EntityRelation.CONTAINS_TYPE, relation.getType()); + Assert.assertEquals(parentA, relation.getFrom()); + Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo())); + } + + relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(2, relations.size()); + + relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByFrom(parentB).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(EntityRelation.MANAGES_TYPE, relation.getType()); + Assert.assertEquals(parentB, relation.getFrom()); + Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo())); + } + + relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(0, relations.size()); + } + + private Boolean saveRelation(EntityRelation relationA1) throws ExecutionException, InterruptedException { + return relationService.saveRelation(relationA1).get(); + } + + @Test + public void testFindTo() throws ExecutionException, InterruptedException { + AssetId parentA = new AssetId(UUIDs.timeBased()); + AssetId parentB = new AssetId(UUIDs.timeBased()); + AssetId childA = new AssetId(UUIDs.timeBased()); + AssetId childB = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE); + EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE); + + EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE); + EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE); + + saveRelation(relationA1); + saveRelation(relationA2); + + saveRelation(relationB1); + saveRelation(relationB2); + + // Data propagation to views is async + Thread.sleep(3000); + + List relations = relationService.findByTo(childA).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(childA, relation.getTo()); + Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom())); + } + + relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(1, relations.size()); + + relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(1, relations.size()); + + relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByTo(childB).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(childB, relation.getTo()); + Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom())); + } + } + + @Test + public void testCyclicRecursiveRelation() throws ExecutionException, InterruptedException { + // A -> B -> C -> A + AssetId assetA = new AssetId(UUIDs.timeBased()); + AssetId assetB = new AssetId(UUIDs.timeBased()); + AssetId assetC = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); + EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + saveRelation(relationB); + saveRelation(relationC); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1)); + query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + List relations = relationService.findByQuery(query).get(); + Assert.assertEquals(3, relations.size()); + Assert.assertTrue(relations.contains(relationA)); + Assert.assertTrue(relations.contains(relationB)); + Assert.assertTrue(relations.contains(relationC)); + } + + @Test + public void testRecursiveRelation() throws ExecutionException, InterruptedException { + // A -> B -> [C,D] + AssetId assetA = new AssetId(UUIDs.timeBased()); + AssetId assetB = new AssetId(UUIDs.timeBased()); + AssetId assetC = new AssetId(UUIDs.timeBased()); + DeviceId deviceD = new DeviceId(UUIDs.timeBased()); + + EntityRelation relationAB = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE); + EntityRelation relationBC = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); + EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE); + + + saveRelation(relationAB); + saveRelation(relationBC); + saveRelation(relationBD); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1)); + query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + List relations = relationService.findByQuery(query).get(); + Assert.assertEquals(2, relations.size()); + Assert.assertTrue(relations.contains(relationAB)); + Assert.assertTrue(relations.contains(relationBC)); + } + + + @Test(expected = DataValidationException.class) + public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException { + EntityRelation relation = new EntityRelation(); + relation.setTo(new AssetId(UUIDs.timeBased())); + relation.setType(EntityRelation.CONTAINS_TYPE); + Assert.assertTrue(saveRelation(relation)); + } + + @Test(expected = DataValidationException.class) + public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException { + EntityRelation relation = new EntityRelation(); + relation.setFrom(new AssetId(UUIDs.timeBased())); + relation.setType(EntityRelation.CONTAINS_TYPE); + Assert.assertTrue(saveRelation(relation)); + } + + @Test(expected = DataValidationException.class) + public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException { + EntityRelation relation = new EntityRelation(); + relation.setFrom(new AssetId(UUIDs.timeBased())); + relation.setTo(new AssetId(UUIDs.timeBased())); + Assert.assertTrue(saveRelation(relation)); + } +} diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js index 35c14fb26f..53d5341139 100644 --- a/ui/src/app/api/attribute.service.js +++ b/ui/src/app/api/attribute.service.js @@ -25,6 +25,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) var service = { getEntityKeys: getEntityKeys, getEntityTimeseriesValues: getEntityTimeseriesValues, + getEntityAttributesValues: getEntityAttributesValues, getEntityAttributes: getEntityAttributes, subscribeForEntityAttributes: subscribeForEntityAttributes, unsubscribeForEntityAttributes: unsubscribeForEntityAttributes, @@ -81,6 +82,20 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) return deferred.promise; } + function getEntityAttributesValues(entityType, entityId, attributeScope, keys, config) { + var deferred = $q.defer(); + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/attributes/' + attributeScope; + if (keys && keys.length) { + url += '?keys=' + keys; + } + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + function processAttributes(attributes, query, deferred, successCallback, update, apply) { attributes = $filter('orderBy')(attributes, query.order); if (query.search != null) { @@ -200,15 +215,48 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) function saveEntityAttributes(entityType, entityId, attributeScope, attributes) { var deferred = $q.defer(); var attributesData = {}; + var deleteAttributes = []; for (var a=0; a -1) { + var deleteRelatedEntityPromise = deleteRelatedEntity(relationEntityId, deleteRelatedEntityTypes); + deleteRelatedEntitiesTasks.push(deleteRelatedEntityPromise); + } + } + deleteRelatedEntitiesTasks.push(deleteEntityPromise(entityId)); + $q.all(deleteRelatedEntitiesTasks).then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ) + } else { + deleteEntityPromise(entityId).then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + } + return deferred.promise; + } + + function moveEntity(entityId, prevParentId, targetParentId) { + var deferred = $q.defer(); + entityRelationService.deleteRelation(prevParentId.id, prevParentId.entityType, + types.entityRelationType.contains, entityId.id, entityId.entityType).then( + function success() { + var relation = { + from: targetParentId, + to: entityId, + type: types.entityRelationType.contains + }; + entityRelationService.saveRelation(relation).then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + function saveEntityPromise(entity) { + var entityType = entity.id.entityType; + if (!entity.id.id) { + delete entity.id; + } + if (entityType == types.entityType.asset) { + return assetService.saveAsset(entity); + } else if (entityType == types.entityType.device) { + return deviceService.saveDevice(entity); + } + } + + function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred) { + var entity = {}; + entity.id = relatedEntity.id; + entity.name = relatedEntity.name; + entity.type = relatedEntity.type; + saveEntityPromise(entity).then( + function success(entity) { + relatedEntity.id = entity.id; + var relation = { + from: parentEntityId, + to: relatedEntity.id, + type: types.entityRelationType.contains + }; + entityRelationService.saveRelation(relation).then( + function success() { + updateEntity(entity, relatedEntity, keys, deferred); + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + } + + function updateRelatedEntity(relatedEntity, keys, deferred) { + getEntityPromise(relatedEntity.id.entityType, relatedEntity.id.id, {ignoreLoading: true}).then( + function success(entity) { + updateEntity(entity, relatedEntity, keys, deferred); + }, + function fail() { + deferred.reject(); + } + ); + } + + function updateEntity(entity, relatedEntity, keys, deferred) { + if (!angular.equals(entity.name, relatedEntity.name) || !angular.equals(entity.type, relatedEntity.type)) { + entity.name = relatedEntity.name; + entity.type = relatedEntity.type; + saveEntityPromise(entity).then( + function success (entity) { + updateEntityAttributes(entity, relatedEntity, keys, deferred); + }, + function fail() { + deferred.reject(); + } + ); + } else { + updateEntityAttributes(entity, relatedEntity, keys, deferred); + } + } + + function updateEntityAttributes(entity, relatedEntity, keys, deferred) { + var attributes = []; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + attributes.push({key: key, value: relatedEntity[key]}); + } + attributeService.saveEntityAttributes(entity.id.entityType, entity.id.id, types.attributesScope.server.value, attributes) + .then( + function success() { + deferred.resolve(relatedEntity); + }, + function fail() { + deferred.reject(); + } + ); + } + + function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel) { + + var searchQuery = { + parameters: { + rootId: rootEntityId.id, + rootType: rootEntityId.entityType, + direction: types.entitySearchDirection.from + }, + relationType: types.entityRelationType.contains + }; + + if (maxLevel) { + searchQuery.parameters.maxLevel = maxLevel; + } else { + searchQuery.parameters.maxLevel = 1; + } + + if (entityType == types.entityType.asset) { + searchQuery.assetTypes = entitySubTypes; + } else if (entityType == types.entityType.device) { + searchQuery.deviceTypes = entitySubTypes; + } else { + return null; //Not supported + } + + return searchQuery; + } + + function constructEntity(entity, keys, typeTranslatePrefix) { + var deferred = $q.defer(); + if (typeTranslatePrefix) { + entity.typeName = $translate.instant(typeTranslatePrefix+'.'+entity.type); + } else { + entity.typeName = entity.type; + } + attributeService.getEntityAttributesValues(entity.id.entityType, entity.id.id, + types.attributesScope.server.value, keys.join(','), + {ignoreLoading: true}).then( + function success(attributes) { + if (attributes && attributes.length > 0) { + for (var i=0;i 0) { + return foundAttributes[0].value; + } else { + return null; + } + } + } \ No newline at end of file diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 4dc41f7d99..a5bb36c4ca 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -262,7 +262,13 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi function fetchAllowedDashboardIds() { var pageLink = {limit: 100}; - dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then( + var fetchDashboardsPromise; + if (currentUser.authority === 'TENANT_ADMIN') { + fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink); + } else { + fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink); + } + fetchDashboardsPromise.then( function success(result) { var dashboards = result.data; for (var d=0;d -1 && column > - 1) { + widgetLayout.row = row; + widgetLayout.col = column; + } else { + row = 0; + for (var w in layout.widgets) { + var existingLayout = layout.widgets[w]; + var wRow = existingLayout.row ? existingLayout.row : 0; + var wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1; + var bottom = wRow + wSizeY; + row = Math.max(row, bottom); + } + widgetLayout.row = row; + widgetLayout.col = 0; + } + + layout.widgets[widget.id] = widgetLayout; + } + + function removeWidgetFromLayout(dashboard, targetState, targetLayout, widgetId) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var state = states[targetState]; + var layout = state.layouts[targetLayout]; + delete layout.widgets[widgetId]; + removeUnusedWidgets(dashboard); + } + + function isSingleLayoutDashboard(dashboard) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var stateKeys = Object.keys(states); + if (stateKeys.length === 1) { + var state = states[stateKeys[0]]; + var layouts = state.layouts; + var layoutKeys = Object.keys(layouts); + if (layoutKeys.length === 1) { + return { + state: stateKeys[0], + layout: layoutKeys[0] + } + } + } + return null; + } + + function removeUnusedWidgets(dashboard) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var widgets = dashboardConfiguration.widgets; + for (var widgetId in widgets) { + var found = false; + for (var s in states) { + var state = states[s]; + for (var l in state.layouts) { + var layout = state.layouts[l]; + if (layout.widgets[widgetId]) { + found = true; + break; + } + } + } + if (!found) { + delete dashboardConfiguration.widgets[widgetId]; + } + + } + } + + function getWidgetsArray(dashboard) { + var widgetsArray = []; + var dashboardConfiguration = dashboard.configuration; + var widgets = dashboardConfiguration.widgets; + for (var widgetId in widgets) { + var widget = widgets[widgetId]; + widgetsArray.push(widget); + } + return widgetsArray; + } } diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index ae3556fea0..a8d8556f38 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -100,6 +100,14 @@ export default angular.module('thingsboard.types', []) tenant: "TENANT", customer: "CUSTOMER" }, + entitySearchDirection: { + from: "FROM", + to: "TO" + }, + entityRelationType: { + contains: "Contains", + manages: "Manages" + }, eventType: { alarm: { value: "ALARM", @@ -199,6 +207,9 @@ export default angular.module('thingsboard.types', []) systemBundleAlias: { charts: "charts", cards: "cards" + }, + translate: { + dashboardStatePrefix: "dashboardState.state." } } ).name; diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js index afa5f566ae..77e3c1c5bd 100644 --- a/ui/src/app/components/dashboard-autocomplete.directive.js +++ b/ui/src/app/components/dashboard-autocomplete.directive.js @@ -53,7 +53,15 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u promise = $q.when({data: []}); } } else { - promise = dashboardService.getTenantDashboards(pageLink); + if (userService.getAuthority() === 'SYS_ADMIN') { + if (scope.tenantId) { + promise = dashboardService.getTenantDashboardsByTenantId(scope.tenantId, pageLink); + } else { + promise = $q.when({data: []}); + } + } else { + promise = dashboardService.getTenantDashboards(pageLink); + } } promise.then(function success(result) { @@ -76,7 +84,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { - dashboardService.getDashboard(ngModelCtrl.$viewValue).then( + dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then( function success(dashboard) { scope.dashboard = dashboard; }, @@ -117,6 +125,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u link: linker, scope: { dashboardsScope: '@', + tenantId: '=', customerId: '=', theForm: '=?', tbRequired: '=?', diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index b20c03949f..f26121ccea 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -51,7 +51,9 @@ function Dashboard() { scope: true, bindToController: { widgets: '=', + widgetLayouts: '=?', aliasesInfo: '=', + stateController: '=', dashboardTimewindow: '=?', columns: '=', margins: '=', @@ -73,7 +75,8 @@ function Dashboard() { onInit: '&?', onInitFailed: '&?', dashboardStyle: '=?', - dashboardClass: '=?' + dashboardClass: '=?', + ignoreLoading: '=?' }, controller: DashboardController, controllerAs: 'vm', @@ -82,7 +85,7 @@ function Dashboard() { } /*@ngInject*/ -function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) { +function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types, utils) { var highlightedMode = false; var highlightedWidget = null; @@ -132,14 +135,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t updateMobileOpts(); + vm.widgetLayoutInfo = { + }; + vm.widgetItemMap = { + sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX', + sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY', + row: 'vm.widgetLayoutInfo[widget.id].row', + col: 'vm.widgetLayoutInfo[widget.id].col', + minSizeY: 'widget.minSizeY', + maxSizeY: 'widget.maxSizeY' + }; + + /*vm.widgetItemMap = { sizeX: 'vm.widgetSizeX(widget)', sizeY: 'vm.widgetSizeY(widget)', - row: 'widget.row', - col: 'widget.col', + row: 'vm.widgetRow(widget)', + col: 'vm.widgetCol(widget)', minSizeY: 'widget.minSizeY', maxSizeY: 'widget.maxSizeY' - }; + };*/ vm.isWidgetExpanded = false; vm.isHighlighted = isHighlighted; @@ -156,6 +171,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.widgetSizeX = widgetSizeX; vm.widgetSizeY = widgetSizeY; + vm.widgetRow = widgetRow; + vm.widgetCol = widgetCol; vm.widgetColor = widgetColor; vm.widgetBackgroundColor = widgetBackgroundColor; vm.widgetPadding = widgetPadding; @@ -173,6 +190,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.openWidgetContextMenu = openWidgetContextMenu; vm.getEventGridPosition = getEventGridPosition; + vm.reload = reload; vm.contextMenuItems = []; vm.contextMenuEvent = null; @@ -199,6 +217,45 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } }; + $scope.$watchCollection('vm.widgets', function () { + var ids = []; + for (var i=0;i - + ng-style="vm.dashboardStyle" + ng-show="((vm.loading() || vm.dashboardLoading) && !vm.isEdit) || vm.isResizing"> + + @@ -88,6 +90,7 @@ locals="{ visibleRect: vm.visibleRect, widget: widget, aliasesInfo: vm.aliasesInfo, + stateController: vm.stateController, isEdit: vm.isEdit, stDiff: vm.stDiff, dashboardTimewindow: vm.dashboardTimewindow, diff --git a/ui/src/app/components/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js new file mode 100644 index 0000000000..f93ded25ed --- /dev/null +++ b/ui/src/app/components/related-entity-autocomplete.directive.js @@ -0,0 +1,128 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 './related-entity-autocomplete.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import relatedEntityAutocompleteTemplate from './related-entity-autocomplete.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + + +export default angular.module('thingsboard.directives.relatedEntityAutocomplete', []) + .directive('tbRelatedEntityAutocomplete', RelatedEntityAutocomplete) + .name; + +/*@ngInject*/ +function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entityService) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(relatedEntityAutocompleteTemplate); + element.html(template); + + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; + scope.entity = null; + scope.entitySearchText = ''; + + scope.allEntities = null; + + scope.fetchEntities = function(searchText) { + var deferred = $q.defer(); + if (!scope.allEntities) { + entityService.getRelatedEntities(scope.rootEntityId, scope.entityType, scope.entitySubtypes, -1, []).then( + function success(entities) { + if (scope.excludeEntityId) { + var result = $filter('filter')(entities, {id: {id: scope.excludeEntityId.id} }, true); + result = $filter('filter')(result, {id: {entityType: scope.excludeEntityId.entityType} }, true); + if (result && result.length) { + var excludeEntity = result[0]; + var index = entities.indexOf(excludeEntity); + if (index > -1) { + entities.splice(index, 1); + } + } + } + scope.allEntities = entities; + filterEntities(searchText, deferred); + }, + function fail() { + deferred.reject(); + } + ); + } else { + filterEntities(searchText, deferred); + } + return deferred.promise; + } + + function filterEntities(searchText, deferred) { + var result = $filter('filter')(scope.allEntities, {name: searchText}); + deferred.resolve(result); + } + + scope.entitySearchTextChanged = function() { + } + + scope.updateView = function () { + if (!scope.disabled) { + ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id : null); + } + } + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + entityService.getRelatedEntity(ngModelCtrl.$viewValue).then( + function success(entity) { + scope.entity = entity; + }, + function fail() { + scope.entity = null; + } + ); + } else { + scope.entity = null; + } + } + + scope.$watch('entity', function () { + scope.updateView(); + }); + + scope.$watch('disabled', function () { + scope.updateView(); + }); + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + rootEntityId: '=', + entityType: '=', + entitySubtypes: '=', + excludeEntityId: '=?', + theForm: '=?', + tbRequired: '=?', + disabled:'=ngDisabled', + placeholderText: '@', + notFoundText: '@', + requiredText: '@' + } + }; +} diff --git a/ui/src/app/components/related-entity-autocomplete.scss b/ui/src/app/components/related-entity-autocomplete.scss new file mode 100644 index 0000000000..32df94f746 --- /dev/null +++ b/ui/src/app/components/related-entity-autocomplete.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-related-entity-autocomplete { + .tb-not-found { + display: block; + line-height: 1.5; + height: 48px; + } + .tb-entity-item { + display: block; + height: 48px; + } + li { + height: auto !important; + white-space: normal !important; + } +} diff --git a/ui/src/app/components/related-entity-autocomplete.tpl.html b/ui/src/app/components/related-entity-autocomplete.tpl.html new file mode 100644 index 0000000000..a5b50e9147 --- /dev/null +++ b/ui/src/app/components/related-entity-autocomplete.tpl.html @@ -0,0 +1,43 @@ + + + +
+ {{item.name}} +
+
+ +
+ {{ notFoundText | translate:{entity: entitySearchText} }} +
+
+
+
{{ requiredText | translate }}
+
+
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index 6c7c472e99..122889d4c1 100644 --- a/ui/src/app/components/widget-config.directive.js +++ b/ui/src/app/components/widget-config.directive.js @@ -89,58 +89,68 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { - scope.selectedTab = 0; - scope.title = ngModelCtrl.$viewValue.title; - scope.showTitle = ngModelCtrl.$viewValue.showTitle; - scope.dropShadow = angular.isDefined(ngModelCtrl.$viewValue.dropShadow) ? ngModelCtrl.$viewValue.dropShadow : true; - scope.enableFullscreen = angular.isDefined(ngModelCtrl.$viewValue.enableFullscreen) ? ngModelCtrl.$viewValue.enableFullscreen : true; - scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor; - scope.color = ngModelCtrl.$viewValue.color; - scope.padding = ngModelCtrl.$viewValue.padding; - scope.titleStyle = - angular.toJson(angular.isDefined(ngModelCtrl.$viewValue.titleStyle) ? ngModelCtrl.$viewValue.titleStyle : { - fontSize: '16px', - fontWeight: 400 - }, true); - scope.mobileOrder = ngModelCtrl.$viewValue.mobileOrder; - scope.mobileHeight = ngModelCtrl.$viewValue.mobileHeight; - scope.units = ngModelCtrl.$viewValue.units; - scope.decimals = ngModelCtrl.$viewValue.decimals; - scope.useDashboardTimewindow = angular.isDefined(ngModelCtrl.$viewValue.useDashboardTimewindow) ? - ngModelCtrl.$viewValue.useDashboardTimewindow : true; - scope.timewindow = ngModelCtrl.$viewValue.timewindow; - scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ? - ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value; - scope.legendConfig = ngModelCtrl.$viewValue.legendConfig; - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value - && scope.isDataEnabled) { - if (scope.datasources) { - scope.datasources.splice(0, scope.datasources.length); - } else { - scope.datasources = []; - } - if (ngModelCtrl.$viewValue.datasources) { - for (var i in ngModelCtrl.$viewValue.datasources) { - scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); + var config = ngModelCtrl.$viewValue.config; + var layout = ngModelCtrl.$viewValue.layout; + if (config) { + scope.selectedTab = 0; + scope.title = config.title; + scope.showTitle = config.showTitle; + scope.dropShadow = angular.isDefined(config.dropShadow) ? config.dropShadow : true; + scope.enableFullscreen = angular.isDefined(config.enableFullscreen) ? config.enableFullscreen : true; + scope.backgroundColor = config.backgroundColor; + scope.color = config.color; + scope.padding = config.padding; + scope.titleStyle = + angular.toJson(angular.isDefined(config.titleStyle) ? config.titleStyle : { + fontSize: '16px', + fontWeight: 400 + }, true); + scope.units = config.units; + scope.decimals = config.decimals; + scope.useDashboardTimewindow = angular.isDefined(config.useDashboardTimewindow) ? + config.useDashboardTimewindow : true; + scope.timewindow = config.timewindow; + scope.showLegend = angular.isDefined(config.showLegend) ? + config.showLegend : scope.widgetType === types.widgetType.timeseries.value; + scope.legendConfig = config.legendConfig; + if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value + && scope.isDataEnabled) { + if (scope.datasources) { + scope.datasources.splice(0, scope.datasources.length); + } else { + scope.datasources = []; } - } - } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) { - var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0]; - if (scope.entityAliases[aliasId]) { - scope.targetDeviceAlias.value = {id: aliasId, alias: scope.entityAliases[aliasId].alias, - entityType: scope.entityAliases[aliasId].entityType, entityId: scope.entityAliases[aliasId].entityId}; + if (config.datasources) { + for (var i in config.datasources) { + scope.datasources.push({value: config.datasources[i]}); + } + } + } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { + if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { + var aliasId = config.targetDeviceAliasIds[0]; + if (scope.entityAliases[aliasId]) { + scope.targetDeviceAlias.value = { + id: aliasId, + alias: scope.entityAliases[aliasId].alias, + entityType: scope.entityAliases[aliasId].entityType, + entityId: scope.entityAliases[aliasId].entityId + }; + } else { + scope.targetDeviceAlias.value = null; + } } else { scope.targetDeviceAlias.value = null; } - } else { - scope.targetDeviceAlias.value = null; } - } - scope.settings = ngModelCtrl.$viewValue.settings; + scope.settings = config.settings; - scope.updateSchemaForm(); + scope.updateSchemaForm(); + } + if (layout) { + scope.mobileOrder = layout.mobileOrder; + scope.mobileHeight = layout.mobileHeight; + } } }; @@ -163,19 +173,22 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.updateValidity = function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - var valid; - if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; - ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); - } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { - valid = value && value.datasources && value.datasources.length > 0; - ngModelCtrl.$setValidity('datasources', valid); - } - try { - angular.fromJson(scope.titleStyle); - ngModelCtrl.$setValidity('titleStyle', true); - } catch (e) { - ngModelCtrl.$setValidity('titleStyle', false); + var config = value.config; + if (config) { + var valid; + if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { + valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; + ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); + } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { + valid = config && config.datasources && config.datasources.length > 0; + ngModelCtrl.$setValidity('datasources', valid); + } + try { + angular.fromJson(scope.titleStyle); + ngModelCtrl.$setValidity('titleStyle', true); + } catch (e) { + ngModelCtrl.$setValidity('titleStyle', false); + } } } }; @@ -184,24 +197,30 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.title = scope.title; - value.showTitle = scope.showTitle; - value.dropShadow = scope.dropShadow; - value.enableFullscreen = scope.enableFullscreen; - value.backgroundColor = scope.backgroundColor; - value.color = scope.color; - value.padding = scope.padding; - try { - value.titleStyle = angular.fromJson(scope.titleStyle); - } catch (e) { - value.titleStyle = {}; + if (value.config) { + var config = value.config; + config.title = scope.title; + config.showTitle = scope.showTitle; + config.dropShadow = scope.dropShadow; + config.enableFullscreen = scope.enableFullscreen; + config.backgroundColor = scope.backgroundColor; + config.color = scope.color; + config.padding = scope.padding; + try { + config.titleStyle = angular.fromJson(scope.titleStyle); + } catch (e) { + config.titleStyle = {}; + } + config.units = scope.units; + config.decimals = scope.decimals; + config.useDashboardTimewindow = scope.useDashboardTimewindow; + config.showLegend = scope.showLegend; + } + if (value.layout) { + var layout = value.layout; + layout.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined; + layout.mobileHeight = scope.mobileHeight; } - value.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined; - value.mobileHeight = scope.mobileHeight; - value.units = scope.units; - value.decimals = scope.decimals; - value.useDashboardTimewindow = scope.useDashboardTimewindow; - value.showLegend = scope.showLegend; ngModelCtrl.$setViewValue(value); scope.updateValidity(); } @@ -210,39 +229,46 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.$watch('currentSettings', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.settings = scope.currentSettings; - ngModelCtrl.$setViewValue(value); + if (value.config) { + value.config.settings = scope.currentSettings; + ngModelCtrl.$setViewValue(value); + } } }, true); scope.$watch('timewindow', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.timewindow = scope.timewindow; - ngModelCtrl.$setViewValue(value); + if (value.config) { + value.config.timewindow = scope.timewindow; + ngModelCtrl.$setViewValue(value); + } } }, true); scope.$watch('legendConfig', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.legendConfig = scope.legendConfig; - ngModelCtrl.$setViewValue(value); + if (value.config) { + value.config.legendConfig = scope.legendConfig; + ngModelCtrl.$setViewValue(value); + } } }, true); scope.$watch('datasources', function () { - if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; - if (value.datasources) { - value.datasources.splice(0, value.datasources.length); + var config = value.config; + if (config.datasources) { + config.datasources.splice(0, config.datasources.length); } else { - value.datasources = []; + config.datasources = []; } if (scope.datasources) { for (var i in scope.datasources) { - value.datasources.push(scope.datasources[i].value); + config.datasources.push(scope.datasources[i].value); } } ngModelCtrl.$setViewValue(value); @@ -251,12 +277,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti }, true); scope.$watch('targetDeviceAlias.value', function () { - if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; + var config = value.config; if (scope.targetDeviceAlias.value) { - value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; + config.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; } else { - value.targetDeviceAliasIds = []; + config.targetDeviceAliasIds = []; } ngModelCtrl.$setViewValue(value); scope.updateValidity(); diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index ec5e7ed931..6fc4bba646 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -22,7 +22,7 @@ import Subscription from '../api/subscription'; /*@ngInject*/ export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService, datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, - dashboardTimewindowApi, widget, aliasesInfo, widgetType) { + dashboardTimewindowApi, widget, aliasesInfo, stateController, widgetType) { var vm = this; @@ -131,7 +131,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q }, utils: { formatValue: formatValue - } + }, + stateController: stateController }; var subscriptionContext = { diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js index de3bf959d0..2f23cf9f38 100644 --- a/ui/src/app/dashboard/add-widget.controller.js +++ b/ui/src/app/dashboard/add-widget.controller.js @@ -37,7 +37,13 @@ export default function AddWidgetController($scope, widgetService, entityService vm.fetchEntityKeys = fetchEntityKeys; vm.createEntityAlias = createEntityAlias; - vm.widgetConfig = vm.widget.config; + vm.widgetConfig = { + config: vm.widget.config, + layout: {} + }; + + vm.widgetConfig.layout.mobileOrder = vm.widget.config.mobileOrder; + vm.widgetConfig.layout.mobileHeight = vm.widget.config.mobileHeight; var settingsSchema = vm.widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; var dataKeySettingsSchema = vm.widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; @@ -85,7 +91,9 @@ export default function AddWidgetController($scope, widgetService, entityService function add () { if ($scope.theForm.$valid) { $scope.theForm.$setPristine(); - vm.widget.config = vm.widgetConfig; + vm.widget.config = vm.widgetConfig.config; + vm.widget.config.mobileOrder = vm.widgetConfig.layout.mobileOrder; + vm.widget.config.mobileHeight = vm.widgetConfig.layout.mobileHeight; $mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo}); } } diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js index ee107cc4be..dbe4cbe992 100644 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ b/ui/src/app/dashboard/dashboard-settings.controller.js @@ -16,7 +16,7 @@ import './dashboard-settings.scss'; /*@ngInject*/ -export default function DashboardSettingsController($scope, $mdDialog, gridSettings) { +export default function DashboardSettingsController($scope, $mdDialog, statesControllerService, settings, gridSettings) { var vm = this; @@ -25,32 +25,49 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti vm.imageAdded = imageAdded; vm.clearImage = clearImage; - vm.gridSettings = gridSettings || {}; + vm.settings = settings; + vm.gridSettings = gridSettings; + vm.stateControllers = statesControllerService.getStateControllers(); - if (angular.isUndefined(vm.gridSettings.showTitle)) { - vm.gridSettings.showTitle = true; - } + if (vm.settings) { + if (angular.isUndefined(vm.settings.stateControllerId)) { + vm.settings.stateControllerId = 'default'; + } - if (angular.isUndefined(vm.gridSettings.showEntitiesSelect)) { - vm.gridSettings.showEntitiesSelect = true; - } + if (angular.isUndefined(vm.settings.showTitle)) { + vm.settings.showTitle = true; + } - if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) { - vm.gridSettings.showDashboardTimewindow = true; - } + if (angular.isUndefined(vm.settings.titleColor)) { + vm.settings.titleColor = 'rgba(0,0,0,0.870588)'; + } - if (angular.isUndefined(vm.gridSettings.showDashboardExport)) { - vm.gridSettings.showDashboardExport = true; - } + if (angular.isUndefined(vm.settings.showDashboardsSelect)) { + vm.settings.showDashboardsSelect = true; + } - vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; - vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)'; - vm.gridSettings.columns = vm.gridSettings.columns || 24; - vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; - vm.hMargin = vm.gridSettings.margins[0]; - vm.vMargin = vm.gridSettings.margins[1]; + if (angular.isUndefined(vm.settings.showEntitiesSelect)) { + vm.settings.showEntitiesSelect = true; + } - vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; + if (angular.isUndefined(vm.settings.showDashboardTimewindow)) { + vm.settings.showDashboardTimewindow = true; + } + + if (angular.isUndefined(vm.settings.showDashboardExport)) { + vm.settings.showDashboardExport = true; + } + } + + if (vm.gridSettings) { + vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; + vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)'; + vm.gridSettings.columns = vm.gridSettings.columns || 24; + vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; + vm.hMargin = vm.gridSettings.margins[0]; + vm.vMargin = vm.gridSettings.margins[1]; + vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; + } function cancel() { $mdDialog.cancel(); @@ -76,7 +93,14 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti function save() { $scope.theForm.$setPristine(); - vm.gridSettings.margins = [vm.hMargin, vm.vMargin]; - $mdDialog.hide(vm.gridSettings); + if (vm.gridSettings) { + vm.gridSettings.margins = [vm.hMargin, vm.vMargin]; + } + $mdDialog.hide( + { + settings: vm.settings, + gridSettings: vm.gridSettings + } + ); } } diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html index 6ae47465d9..88fc66d651 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -19,7 +19,7 @@
-

dashboard.settings

+

{{vm.settings ? 'dashboard.settings' : 'layout.settings'}}

@@ -31,15 +31,53 @@
-
- {{ 'dashboard.display-title' | translate }} - +
+ + + + + {{stateControllerId}} + + + +
+ {{ 'dashboard.display-title' | translate }} + +
+
+
+ {{ 'dashboard.display-dashboards-selection' | translate }} + + {{ 'dashboard.display-entities-selection' | translate }} + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + {{ 'dashboard.display-dashboard-export' | translate }} + +
+
+
-
-
- {{ 'dashboard.display-entities-selection' | translate }} - - {{ 'dashboard.display-dashboard-timewindow' | translate }} - - {{ 'dashboard.display-dashboard-export' | translate }} - -
- - - -
-
dashboard.columns-count-required
-
dashboard.min-columns-count-message
-
dashboard.max-columns-count-message
-
-
- dashboard.widgets-margins -
- - - -
-
dashboard.horizontal-margin-required
-
dashboard.min-horizontal-margin-message
-
dashboard.max-horizontal-margin-message
-
-
- - - -
-
dashboard.vertical-margin-required
-
dashboard.min-vertical-margin-message
-
dashboard.max-vertical-margin-message
+ + + +
+
dashboard.columns-count-required
+
dashboard.min-columns-count-message
+
dashboard.max-columns-count-message
-
-
-
- -
-
-
dashboard.no-image
- -
-
- - - {{ 'action.remove' | translate }} - - - close - - -
-
- - + dashboard.widgets-margins +
+ + + +
+
dashboard.horizontal-margin-required
+
dashboard.min-horizontal-margin-message
+
dashboard.max-horizontal-margin-message
+
+
+ + + +
+
dashboard.vertical-margin-required
+
dashboard.min-vertical-margin-message
+
dashboard.max-vertical-margin-message
+
+
+
+
+
+ +
+
+
dashboard.no-image
+ +
+
+ + + {{ 'action.remove' | translate }} + + + close + + +
+
+ + +
+ + + + Fit width + Fit height + Cover + Contain + Original size + +
- - - - Fit width - Fit height - Cover - Contain - Original size - -
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index 79ee6aeeb4..e18ebb8859 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -16,22 +16,27 @@ /* eslint-disable import/no-unresolved, import/default */ import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; -import dashboardBackgroundTemplate from './dashboard-settings.tpl.html'; +import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; +import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; +import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; import addWidgetTemplate from './add-widget.tpl.html'; +import selectTargetLayoutTemplate from './layouts/select-target-layout.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ export default function DashboardController(types, dashboardUtils, widgetService, userService, dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope, - $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { + $scope, $element, $state, $stateParams, $mdDialog, $mdMedia, $timeout, $document, $q, $translate, $filter) { var vm = this; vm.user = userService.getCurrentUser(); vm.dashboard = null; vm.editingWidget = null; + vm.editingWidgetLayout = null; vm.editingWidgetOriginal = null; + vm.editingWidgetLayoutOriginal = null; vm.editingWidgetSubtitle = null; vm.forceDashboardMobileMode = false; vm.isAddingWidget = false; @@ -43,8 +48,6 @@ export default function DashboardController(types, dashboardUtils, widgetService vm.staticWidgetTypes = []; vm.widgetEditMode = $state.$current.data.widgetEditMode; vm.iframeMode = $rootScope.iframeMode; - vm.widgets = []; - vm.dashboardInitComplete = false; vm.isToolbarOpened = false; @@ -60,10 +63,33 @@ export default function DashboardController(types, dashboardUtils, widgetService } Object.defineProperty(vm, 'toolbarOpened', { - get: function() { return vm.isToolbarOpened || vm.isEdit; }, + get: function() { return !vm.widgetEditMode && ($scope.forceFullscreen || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); }, set: function() { } }); + vm.layouts = { + main: { + show: false, + layoutCtx: { + id: 'main', + widgets: [], + widgetLayouts: {}, + gridSettings: {}, + ignoreLoading: false + } + }, + right: { + show: false, + layoutCtx: { + id: 'right', + widgets: [], + widgetLayouts: {}, + gridSettings: {}, + ignoreLoading: false + } + } + }; + vm.openToolbar = function() { $timeout(function() { vm.isToolbarOpened = true; @@ -76,31 +102,78 @@ export default function DashboardController(types, dashboardUtils, widgetService }); } + vm.showCloseToolbar = function() { + return !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch(); + } + + vm.showRightLayoutSwitch = function() { + return vm.isMobile && vm.layouts.right.show; + } + + vm.toggleLayouts = function() { + vm.isRightLayoutOpened = !vm.isRightLayoutOpened; + } + + vm.openRightLayout = function() { + vm.isRightLayoutOpened = true; + } + + vm.isRightLayoutOpened = false; + vm.isMobile = !$mdMedia('gt-sm'); + + $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) { + vm.isMobile = !isGtSm; + }); + + vm.mainLayoutWidth = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') { + return '100%'; + } else { + return vm.layouts.right.show && !vm.isMobile ? '50%' : '100%'; + } + } + + vm.mainLayoutHeight = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') { + return '100%'; + } else { + return 'auto'; + } + } + + vm.rightLayoutWidth = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') { + return '100%'; + } else { + return vm.isMobile ? '100%' : '50%'; + } + } + + vm.rightLayoutHeight = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') { + return '100%'; + } else { + return 'auto'; + } + } + + vm.getServerTimeDiff = getServerTimeDiff; vm.addWidget = addWidget; vm.addWidgetFromType = addWidgetFromType; - vm.dashboardInited = dashboardInited; - vm.dashboardInitFailed = dashboardInitFailed; - vm.widgetMouseDown = widgetMouseDown; - vm.widgetClicked = widgetClicked; - vm.prepareDashboardContextMenu = prepareDashboardContextMenu; - vm.prepareWidgetContextMenu = prepareWidgetContextMenu; - vm.editWidget = editWidget; vm.exportDashboard = exportDashboard; - vm.exportWidget = exportWidget; vm.importWidget = importWidget; vm.isPublicUser = isPublicUser; vm.isTenantAdmin = isTenantAdmin; vm.isSystemAdmin = isSystemAdmin; - vm.loadDashboard = loadDashboard; - vm.getServerTimeDiff = getServerTimeDiff; - vm.noData = noData; vm.dashboardConfigurationError = dashboardConfigurationError; vm.showDashboardToolbar = showDashboardToolbar; vm.onAddWidgetClosed = onAddWidgetClosed; vm.onEditWidgetClosed = onEditWidgetClosed; + vm.openDashboardState = openDashboardState; vm.openEntityAliases = openEntityAliases; vm.openDashboardSettings = openDashboardSettings; - vm.removeWidget = removeWidget; + vm.manageDashboardLayouts = manageDashboardLayouts; + vm.manageDashboardStates = manageDashboardStates; vm.saveDashboard = saveDashboard; vm.saveWidget = saveWidget; vm.toggleDashboardEditMode = toggleDashboardEditMode; @@ -109,10 +182,56 @@ export default function DashboardController(types, dashboardUtils, widgetService vm.displayTitle = displayTitle; vm.displayExport = displayExport; vm.displayDashboardTimewindow = displayDashboardTimewindow; + vm.displayDashboardsSelect = displayDashboardsSelect; vm.displayEntitiesSelect = displayEntitiesSelect; vm.widgetsBundle; + vm.dashboardCtx = { + state: null, + stateController: { + openRightLayout: function() { + vm.openRightLayout(); + } + }, + onAddWidget: function(event, layoutCtx) { + addWidget(event, layoutCtx); + }, + onEditWidget: function(event, layoutCtx, widget) { + editWidget(event, layoutCtx, widget); + }, + onExportWidget: function(event, layoutCtx, widget) { + exportWidget(event, layoutCtx, widget); + }, + onWidgetMouseDown: function(event, layoutCtx, widget) { + widgetMouseDown(event, layoutCtx, widget); + }, + onWidgetClicked: function(event, layoutCtx, widget) { + widgetClicked(event, layoutCtx, widget); + }, + prepareDashboardContextMenu: function(layoutCtx) { + return prepareDashboardContextMenu(layoutCtx); + }, + prepareWidgetContextMenu: function(layoutCtx, widget) { + return prepareWidgetContextMenu(layoutCtx, widget); + }, + onRemoveWidget: function(event, layoutCtx, widget) { + removeWidget(event, layoutCtx, widget); + }, + copyWidget: function($event, layoutCtx, widget) { + copyWidget($event, layoutCtx, widget); + }, + copyWidgetReference: function($event, layoutCtx, widget) { + copyWidgetReference($event, layoutCtx, widget); + }, + pasteWidget: function($event, layoutCtx, pos) { + pasteWidget($event, layoutCtx, pos); + }, + pasteWidgetReference: function($event, layoutCtx, pos) { + pasteWidgetReference($event, layoutCtx, pos); + } + }; + $scope.$watch('vm.widgetsBundle', function (newVal, prevVal) { if (newVal !== prevVal && !vm.widgetEditMode) { loadWidgetLibrary(); @@ -132,6 +251,7 @@ export default function DashboardController(types, dashboardUtils, widgetService } }); + loadDashboard(); function loadWidgetLibrary() { vm.latestWidgetTypes = []; @@ -199,34 +319,29 @@ export default function DashboardController(types, dashboardUtils, widgetService } function loadDashboard() { - - var deferred = $q.defer(); - if (vm.widgetEditMode) { - $timeout(function () { - vm.dashboardConfiguration = { - timewindow: timeService.defaultTimewindow() - }; - vm.widgets = [{ - isSystemType: true, - bundleAlias: 'customWidgetBundle', - typeAlias: 'customWidget', - type: $rootScope.editWidgetInfo.type, - title: 'My widget', - sizeX: $rootScope.editWidgetInfo.sizeX * 2, - sizeY: $rootScope.editWidgetInfo.sizeY * 2, - row: 2, - col: 4, - config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig) - }]; - vm.widgets[0].config.title = vm.widgets[0].config.title || $rootScope.editWidgetInfo.widgetName; - deferred.resolve(); - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - parentScope.$root.$broadcast('widgetEditModeInited'); - parentScope.$root.$apply(); - }); + var widget = { + isSystemType: true, + bundleAlias: 'customWidgetBundle', + typeAlias: 'customWidget', + type: $rootScope.editWidgetInfo.type, + title: 'My widget', + sizeX: $rootScope.editWidgetInfo.sizeX * 2, + sizeY: $rootScope.editWidgetInfo.sizeY * 2, + row: 2, + col: 4, + config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig) + }; + widget.config.title = widget.config.title || $rootScope.editWidgetInfo.widgetName; + + vm.dashboard = dashboardUtils.createSingleWidgetDashboard(widget); + vm.dashboardConfiguration = vm.dashboard.configuration; + vm.dashboardCtx.dashboard = vm.dashboard; + vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; + var parentScope = $window.parent.angular.element($window.frameElement).scope(); + parentScope.$root.$broadcast('widgetEditModeInited'); + parentScope.$root.$apply(); } else { - dashboardService.getDashboard($stateParams.dashboardId) .then(function success(dashboard) { vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); @@ -236,34 +351,68 @@ export default function DashboardController(types, dashboardUtils, widgetService if (resolution.error && !isTenantAdmin()) { vm.configurationError = true; showAliasesResolutionError(resolution.error); - deferred.reject(); } else { - vm.aliasesInfo = resolution.aliasesInfo; vm.dashboardConfiguration = vm.dashboard.configuration; - vm.widgets = vm.dashboard.configuration.widgets; - deferred.resolve(); + vm.dashboardCtx.dashboard = vm.dashboard; + vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo; + vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; } } ); - }, function fail(e) { - deferred.reject(e); + }, function fail() { + vm.configurationError = true; }); - } - return deferred.promise; } - function dashboardInitFailed() { - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - parentScope.$emit('widgetEditModeInited'); - parentScope.$apply(); - vm.dashboardInitComplete = true; + function openDashboardState(state) { + var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state); + if (layoutsData) { + vm.dashboardCtx.state = state; + var layoutVisibilityChanged = false; + for (var l in vm.layouts) { + var layout = vm.layouts[l]; + var showLayout; + if (layoutsData[l]) { + showLayout = true; + } else { + showLayout = false; + } + if (layout.show != showLayout) { + layout.show = showLayout; + layoutVisibilityChanged = !vm.isMobile; + } + } + vm.isRightLayoutOpened = false; + updateLayouts(layoutVisibilityChanged); + } + + function updateLayouts(layoutVisibilityChanged) { + for (l in vm.layouts) { + layout = vm.layouts[l]; + if (layoutsData[l]) { + var layoutInfo = layoutsData[l]; + if (layout.layoutCtx.id === 'main') { + layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged); + } + updateLayout(layout, layoutInfo.widgets, layoutInfo.widgetLayouts, layoutInfo.gridSettings); + } else { + updateLayout(layout, [], {}, null); + } + } + } } - function dashboardInited(dashboard) { - vm.dashboardContainer = dashboard; - initHotKeys(); - vm.dashboardInitComplete = true; + function updateLayout(layout, widgets, widgetLayouts, gridSettings) { + if (gridSettings) { + layout.layoutCtx.gridSettings = gridSettings; + } + layout.layoutCtx.widgets = widgets; + layout.layoutCtx.widgetLayouts = widgetLayouts; + if (layout.show && layout.layoutCtx.ctrl) { + layout.layoutCtx.ctrl.reload(); + } + layout.layoutCtx.ignoreLoading = true; } function isPublicUser() { @@ -278,16 +427,12 @@ export default function DashboardController(types, dashboardUtils, widgetService return vm.user.authority === 'SYS_ADMIN'; } - function noData() { - return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0; - } - function dashboardConfigurationError() { - return vm.dashboardInitComplete && vm.configurationError; + return vm.configurationError; } function showDashboardToolbar() { - return vm.dashboardInitComplete; + return true; } function openEntityAliases($event) { @@ -298,7 +443,7 @@ export default function DashboardController(types, dashboardUtils, widgetService locals: { config: { entityAliases: angular.copy(vm.dashboard.configuration.entityAliases), - widgets: vm.widgets, + widgets: dashboardUtils.getWidgetsArray(vm.dashboard), isSingleEntityAlias: false, singleEntityAlias: null } @@ -315,54 +460,115 @@ export default function DashboardController(types, dashboardUtils, widgetService } function openDashboardSettings($event) { + var gridSettings = null; + var layoutKeys = dashboardUtils.isSingleLayoutDashboard(vm.dashboard); + if (layoutKeys) { + gridSettings = angular.copy(vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings) + } $mdDialog.show({ controller: 'DashboardSettingsController', controllerAs: 'vm', - templateUrl: dashboardBackgroundTemplate, + templateUrl: dashboardSettingsTemplate, locals: { - gridSettings: angular.copy(vm.dashboard.configuration.gridSettings) + settings: angular.copy(vm.dashboard.configuration.settings), + gridSettings: gridSettings }, parent: angular.element($document[0].body), skipHide: true, fullscreen: true, targetEvent: $event - }).then(function (gridSettings) { - var prevGridSettings = vm.dashboard.configuration.gridSettings; - var prevColumns = prevGridSettings ? prevGridSettings.columns : 24; - var ratio = gridSettings.columns / prevColumns; - var currentWidgets = angular.copy(vm.widgets); - vm.widgets = []; - vm.dashboard.configuration.gridSettings = gridSettings; - for (var w in currentWidgets) { - var widget = currentWidgets[w]; - widget.sizeX = Math.round(widget.sizeX * ratio); - widget.sizeY = Math.round(widget.sizeY * ratio); - widget.col = Math.round(widget.col * ratio); - widget.row = Math.round(widget.row * ratio); + }).then(function (data) { + vm.dashboard.configuration.settings = data.settings; + var gridSettings = data.gridSettings; + if (gridSettings) { + updateLayoutGrid(layoutKeys, gridSettings); } - vm.dashboard.configuration.widgets = currentWidgets; - vm.widgets = vm.dashboard.configuration.widgets; }, function () { }); } - function editWidget($event, widget) { + function manageDashboardLayouts($event) { + $mdDialog.show({ + controller: 'ManageDashboardLayoutsController', + controllerAs: 'vm', + templateUrl: manageDashboardLayoutsTemplate, + locals: { + layouts: angular.copy(vm.dashboard.configuration.states[vm.dashboardCtx.state].layouts) + }, + parent: angular.element($document[0].body), + skipHide: true, + fullscreen: true, + targetEvent: $event + }).then(function (layouts) { + updateLayouts(layouts); + }, function () { + }); + } + + function manageDashboardStates($event) { + var dashboardConfiguration = vm.dashboard.configuration; + var states = angular.copy(dashboardConfiguration.states); + + $mdDialog.show({ + controller: 'ManageDashboardStatesController', + controllerAs: 'vm', + templateUrl: manageDashboardStatesTemplate, + locals: { + states: states + }, + parent: angular.element($document[0].body), + skipHide: true, + fullscreen: true, + targetEvent: $event + }).then(function (states) { + updateStates(states); + }, function () { + }); + } + + function updateLayoutGrid(layoutKeys, gridSettings) { + var layout = vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; + var layoutCtx = vm.layouts[layoutKeys.layout]; + layoutCtx.widgets = []; + dashboardUtils.updateLayoutSettings(layout, gridSettings); + var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, layoutKeys.state); + layoutCtx.widgets = layoutsData[layoutKeys.layout].widgets; + } + + function updateLayouts(layouts) { + dashboardUtils.setLayouts(vm.dashboard, vm.dashboardCtx.state, layouts); + openDashboardState(vm.dashboardCtx.state); + } + + function updateStates(states) { + vm.dashboard.configuration.states = states; + dashboardUtils.removeUnusedWidgets(vm.dashboard); + var targetState = vm.dashboardCtx.state; + if (!vm.dashboard.configuration.states[targetState]) { + targetState = dashboardUtils.getRootStateId(vm.dashboardConfiguration.states); + } + openDashboardState(targetState); + } + + function editWidget($event, layoutCtx, widget) { $event.stopPropagation(); if (vm.editingWidgetOriginal === widget) { $timeout(onEditWidgetClosed()); } else { var transition = !vm.forceDashboardMobileMode; vm.editingWidgetOriginal = widget; + vm.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id]; vm.editingWidget = angular.copy(vm.editingWidgetOriginal); + vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal); + vm.editingLayoutCtx = layoutCtx; vm.editingWidgetSubtitle = widgetService.getInstantWidgetInfo(vm.editingWidget).widgetName; vm.forceDashboardMobileMode = true; vm.isEditingWidget = true; - - if (vm.dashboardContainer) { + if (layoutCtx) { var delayOffset = transition ? 350 : 0; var delay = transition ? 400 : 300; $timeout(function () { - vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, delay); + layoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, delay); }, delayOffset, false); } } @@ -372,82 +578,36 @@ export default function DashboardController(types, dashboardUtils, widgetService importExport.exportDashboard(vm.currentDashboardId); } - function exportWidget($event, widget) { + function exportWidget($event, layoutCtx, widget) { $event.stopPropagation(); - importExport.exportWidget(vm.dashboard, widget); + importExport.exportWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); } function importWidget($event) { $event.stopPropagation(); - importExport.importWidget($event, vm.dashboard, entityAliasesUpdated); + importExport.importWidget($event, vm.dashboard, vm.dashboardCtx.state, + selectTargetLayout, entityAliasesUpdated).then( + function success(importData) { + var widget = importData.widget; + var layoutId = importData.layoutId; + vm.layouts[layoutId].layoutCtx.widgets.push(widget); + } + ); } - function widgetMouseDown($event, widget) { + function widgetMouseDown($event, layoutCtx, widget) { if (vm.isEdit && !vm.isEditingWidget) { - vm.dashboardContainer.selectWidget(widget, 0); + layoutCtx.ctrl.selectWidget(widget, 0); } } - function widgetClicked($event, widget) { + function widgetClicked($event, layoutCtx, widget) { if (vm.isEditingWidget) { - editWidget($event, widget); + editWidget($event, layoutCtx, widget); } } - function isHotKeyAllowed(event) { - var target = event.target || event.srcElement; - var scope = angular.element(target).scope(); - return scope && scope.$parent !== $rootScope; - } - - function initHotKeys() { - $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) { - hotkeys.bindTo($scope) - .add({ - combo: 'ctrl+c', - description: translations['action.copy'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - copyWidget(event, widget); - } - } - } - }) - .add({ - combo: 'ctrl+v', - description: translations['action.paste'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - if (itembuffer.hasWidget()) { - event.preventDefault(); - pasteWidget(event); - } - } - } - }) - .add({ - combo: 'ctrl+x', - description: translations['action.delete'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - removeWidget(event, widget); - } - } - } - }); - }); - } - - function prepareDashboardContextMenu() { + function prepareDashboardContextMenu(layoutCtx) { var dashboardContextActions = []; if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { dashboardContextActions.push( @@ -468,28 +628,39 @@ export default function DashboardController(types, dashboardUtils, widgetService ); dashboardContextActions.push( { - action: pasteWidget, + action: function ($event) { + layoutCtx.ctrl.pasteWidget($event); + }, enabled: itembuffer.hasWidget(), value: "action.paste", icon: "content_paste", shortcut: "M-V" } ); + dashboardContextActions.push( + { + action: function ($event) { + layoutCtx.ctrl.pasteWidgetReference($event); + }, + enabled: itembuffer.canPasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id), + value: "action.paste-reference", + icon: "content_paste", + shortcut: "M-I" + } + ); + } return dashboardContextActions; } - function pasteWidget($event) { - var pos = vm.dashboardContainer.getEventGridPosition($event); - itembuffer.pasteWidget(vm.dashboard, pos, entityAliasesUpdated); - } - - function prepareWidgetContextMenu() { + function prepareWidgetContextMenu(layoutCtx) { var widgetContextActions = []; if (vm.isEdit && !vm.isEditingWidget) { widgetContextActions.push( { - action: editWidget, + action: function (event, widget) { + editWidget(event, layoutCtx, widget); + }, enabled: true, value: "action.edit", icon: "edit" @@ -498,7 +669,9 @@ export default function DashboardController(types, dashboardUtils, widgetService if (!vm.widgetEditMode) { widgetContextActions.push( { - action: copyWidget, + action: function (event, widget) { + copyWidget(event, layoutCtx, widget); + }, enabled: true, value: "action.copy", icon: "content_copy", @@ -507,7 +680,20 @@ export default function DashboardController(types, dashboardUtils, widgetService ); widgetContextActions.push( { - action: removeWidget, + action: function (event, widget) { + copyWidgetReference(event, layoutCtx, widget); + }, + enabled: true, + value: "action.copy-reference", + icon: "content_copy", + shortcut: "M-R" + } + ); + widgetContextActions.push( + { + action: function (event, widget) { + removeWidget(event, layoutCtx, widget); + }, enabled: true, value: "action.delete", icon: "clear", @@ -519,8 +705,12 @@ export default function DashboardController(types, dashboardUtils, widgetService return widgetContextActions; } - function copyWidget($event, widget) { - itembuffer.copyWidget(vm.dashboard, widget); + function copyWidget($event, layoutCtx, widget) { + itembuffer.copyWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); + } + + function copyWidgetReference($event, layoutCtx, widget) { + itembuffer.copyWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); } function helpLinkIdForWidgetType() { @@ -549,36 +739,45 @@ export default function DashboardController(types, dashboardUtils, widgetService } function displayTitle() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) { - return vm.dashboard.configuration.gridSettings.showTitle; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showTitle)) { + return vm.dashboard.configuration.settings.showTitle; } else { return true; } } function displayExport() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardExport)) { - return vm.dashboard.configuration.gridSettings.showDashboardExport; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showDashboardExport)) { + return vm.dashboard.configuration.settings.showDashboardExport; } else { return true; } } function displayDashboardTimewindow() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardTimewindow)) { - return vm.dashboard.configuration.gridSettings.showDashboardTimewindow; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showDashboardTimewindow)) { + return vm.dashboard.configuration.settings.showDashboardTimewindow; + } else { + return true; + } + } + + function displayDashboardsSelect() { + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showDashboardsSelect)) { + return vm.dashboard.configuration.settings.showDashboardsSelect; } else { return true; } } function displayEntitiesSelect() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showEntitiesSelect)) { - return vm.dashboard.configuration.gridSettings.showEntitiesSelect; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showEntitiesSelect)) { + return vm.dashboard.configuration.settings.showEntitiesSelect; } else { return true; } @@ -588,32 +787,40 @@ export default function DashboardController(types, dashboardUtils, widgetService if (widgetForm.$dirty) { widgetForm.$setPristine(); vm.editingWidget = angular.copy(vm.editingWidgetOriginal); + vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal); } } function saveWidget(widgetForm) { widgetForm.$setPristine(); var widget = angular.copy(vm.editingWidget); - var index = vm.widgets.indexOf(vm.editingWidgetOriginal); - vm.widgets[index] = widget; + var widgetLayout = angular.copy(vm.editingWidgetLayout); + var id = vm.editingWidgetOriginal.id; + var index = vm.editingLayoutCtx.widgets.indexOf(vm.editingWidgetOriginal); + vm.dashboardConfiguration.widgets[id] = widget; vm.editingWidgetOriginal = widget; - vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, 0); + vm.editingWidgetLayoutOriginal = widgetLayout; + vm.editingLayoutCtx.widgets[index] = widget; + vm.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; + vm.editingLayoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, 0); } function onEditWidgetClosed() { vm.editingWidgetOriginal = null; vm.editingWidget = null; + vm.editingWidgetLayoutOriginal = null; + vm.editingWidgetLayout = null; + vm.editingLayoutCtx = null; vm.editingWidgetSubtitle = null; vm.isEditingWidget = false; - if (vm.dashboardContainer) { - vm.dashboardContainer.resetHighlight(); - } + resetHighlight(); vm.forceDashboardMobileMode = false; } - function addWidget() { + function addWidget(event, layoutCtx) { loadWidgetLibrary(); vm.isAddingWidget = true; + vm.addingLayoutCtx = layoutCtx; } function onAddWidgetClosed() { @@ -623,6 +830,33 @@ export default function DashboardController(types, dashboardUtils, widgetService vm.staticWidgetTypes = []; } + function selectTargetLayout($event) { + var deferred = $q.defer(); + var layouts = vm.dashboardConfiguration.states[vm.dashboardCtx.state].layouts; + var layoutIds = Object.keys(layouts); + if (layoutIds.length > 1) { + $mdDialog.show({ + controller: 'SelectTargetLayoutController', + controllerAs: 'vm', + templateUrl: selectTargetLayoutTemplate, + parent: angular.element($document[0].body), + fullscreen: true, + skipHide: true, + targetEvent: $event + }).then( + function success(layoutId) { + deferred.resolve(layoutId); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(layoutIds[0]); + } + return deferred.promise; + } + function addWidgetFromType(event, widget) { vm.onAddWidgetClosed(); vm.isAddingWidget = false; @@ -642,17 +876,22 @@ export default function DashboardController(types, dashboardUtils, widgetService config: config }; + function addWidgetToLayout(widget, layoutId) { + dashboardUtils.addWidgetToLayout(vm.dashboard, vm.dashboardCtx.state, layoutId, widget); + vm.layouts[layoutId].layoutCtx.widgets.push(widget); + } + function addWidget(widget) { - var columns = 24; - if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { - columns = vm.dashboard.configuration.gridSettings.columns; - } - if (columns != 24) { - var ratio = columns / 24; - widget.sizeX *= ratio; - widget.sizeY *= ratio; + if (vm.addingLayoutCtx) { + addWidgetToLayout(widget, vm.addingLayoutCtx.id); + vm.addingLayoutCtx = null; + } else { + selectTargetLayout(event).then( + function success(layoutId) { + addWidgetToLayout(widget, layoutId); + } + ); } - vm.widgets.push(widget); } if (widgetTypeInfo.useCustomDatasources) { @@ -664,7 +903,7 @@ export default function DashboardController(types, dashboardUtils, widgetService templateUrl: addWidgetTemplate, locals: { dashboard: vm.dashboard, - aliasesInfo: vm.aliasesInfo, + aliasesInfo: vm.dashboardCtx.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo }, @@ -678,17 +917,17 @@ export default function DashboardController(types, dashboardUtils, widgetService } }).then(function (result) { var widget = result.widget; - vm.aliasesInfo = result.aliasesInfo; + vm.dashboardCtx.aliasesInfo = result.aliasesInfo; addWidget(widget); }, function (rejection) { - vm.aliasesInfo = rejection.aliasesInfo; + vm.dashboardCtx.aliasesInfo = rejection.aliasesInfo; }); } } ); } - function removeWidget(event, widget) { + function removeWidget(event, layoutCtx, widget) { var title = widget.config.title; if (!title || title.length === 0) { title = widgetService.getInstantWidgetInfo(widget).widgetName; @@ -701,37 +940,66 @@ export default function DashboardController(types, dashboardUtils, widgetService .cancel($translate.instant('action.no')) .ok($translate.instant('action.yes')); $mdDialog.show(confirm).then(function () { - vm.widgets.splice(vm.widgets.indexOf(widget), 1); + var index = layoutCtx.widgets.indexOf(widget); + if (index > -1) { + layoutCtx.widgets.splice(index, 1); + dashboardUtils.removeWidgetFromLayout(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget.id); + } }); } + function pasteWidget(event, layoutCtx, pos) { + itembuffer.pasteWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos, entityAliasesUpdated).then( + function (widget) { + if (widget) { + layoutCtx.widgets.push(widget); + } + } + ); + } + + function pasteWidgetReference(event, layoutCtx, pos) { + itembuffer.pasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos).then( + function (widget) { + if (widget) { + layoutCtx.widgets.push(widget); + } + } + ); + } + function setEditMode(isEdit, revert) { vm.isEdit = isEdit; if (vm.isEdit) { - if (vm.widgetEditMode) { - vm.prevWidgets = angular.copy(vm.widgets); - } else { - vm.prevDashboard = angular.copy(vm.dashboard); - } + vm.prevDashboard = angular.copy(vm.dashboard); + vm.prevDashboardState = vm.dashboardCtx.state; } else { if (vm.widgetEditMode) { if (revert) { - vm.widgets = vm.prevWidgets; + vm.dashboard = vm.prevDashboard; } } else { - if (vm.dashboardContainer) { - vm.dashboardContainer.resetHighlight(); - } + resetHighlight(); if (revert) { vm.dashboard = vm.prevDashboard; - vm.widgets = vm.dashboard.configuration.widgets; vm.dashboardConfiguration = vm.dashboard.configuration; + openDashboardState(vm.prevDashboardState); entityAliasesUpdated(); } } } } + function resetHighlight() { + for (var l in vm.layouts) { + if (vm.layouts[l].layoutCtx) { + if (vm.layouts[l].layoutCtx.ctrl) { + vm.layouts[l].layoutCtx.ctrl.resetHighlight(); + } + } + } + } + function toggleDashboardEditMode() { setEditMode(!vm.isEdit, true); } @@ -756,20 +1024,26 @@ export default function DashboardController(types, dashboardUtils, widgetService } function entityAliasesUpdated() { + var deferred = $q.defer(); entityService.processEntityAliases(vm.dashboard.configuration.entityAliases) .then( function(resolution) { if (resolution.aliasesInfo) { - vm.aliasesInfo = resolution.aliasesInfo; + vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo; } + deferred.resolve(); } ); + return deferred.promise; } function notifyDashboardUpdated() { if (vm.widgetEditMode) { var parentScope = $window.parent.angular.element($window.frameElement).scope(); - var widget = vm.widgets[0]; + var widget = vm.layouts.main.layoutCtx.widgets[0]; + var layout = vm.layouts.main.layoutCtx.widgetLayouts[widget.id]; + widget.sizeX = layout.sizeX; + widget.sizeY = layout.sizeY; parentScope.$root.$broadcast('widgetEditUpdated', widget); parentScope.$root.$apply(); } else { diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js index e9fe1f24ea..92bb36220f 100644 --- a/ui/src/app/dashboard/dashboard.routes.js +++ b/ui/src/app/dashboard/dashboard.routes.js @@ -66,7 +66,8 @@ export default function DashboardRoutes($stateProvider) { } }) .state('home.dashboards.dashboard', { - url: '/:dashboardId', + url: '/:dashboardId?state', + reloadOnSearch: false, module: 'private', auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], views: { @@ -86,7 +87,8 @@ export default function DashboardRoutes($stateProvider) { } }) .state('home.customers.dashboards.dashboard', { - url: '/:dashboardId', + url: '/:dashboardId?state', + reloadOnSearch: false, module: 'private', auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], views: { diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss index 8f50ca295f..bc5ec56a33 100644 --- a/ui/src/app/dashboard/dashboard.scss +++ b/ui/src/app/dashboard/dashboard.scss @@ -63,7 +63,7 @@ tb-details-sidenav.tb-widget-details-sidenav { section.tb-dashboard-toolbar { position: absolute; top: 0px; - left: -100%; + left: 0px; z-index: 3; pointer-events: none; &.tb-dashboard-toolbar-opened { @@ -118,6 +118,27 @@ section.tb-dashboard-toolbar { .close-action { margin-right: -18px; } + .md-fab-action-item { + width: 100%; + height: 46px; + .tb-dashboard-action-panels { + height: 46px; + flex-direction: row-reverse; + .tb-dashboard-action-panel { + height: 46px; + flex-direction: row-reverse; + div { + height: 46px; + } + md-select { + pointer-events: all; + } + tb-states-component { + pointer-events: all; + } + } + } + } } } } @@ -133,6 +154,19 @@ section.tb-dashboard-toolbar { margin-top: 0px; @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2) .2s); } + .tb-dashboard-layouts { + md-backdrop { + z-index: 1; + } + #tb-main-layout { + + } + #tb-right-layout { + md-sidenav { + z-index: 1; + } + } + } } /***************************** diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index 3286f8d867..078daabd52 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -16,16 +16,10 @@ --> + hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom">
- @@ -37,77 +31,100 @@ - - - {{ 'dashboard.close-toolbar' | translate }} - - arrow_forward - - - - - - - - {{ 'dashboard.export' | translate }} - - file_download - - - - - - - - {{ 'entity.aliases' | translate }} - - devices_other - - - - {{ 'dashboard.settings' | translate }} - - settings - - - +
+
+ + + {{ 'dashboard.close-toolbar' | translate }} + + arrow_forward + + + + + {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }} + + + + + + + + + {{ 'dashboard.export' | translate }} + + file_download + + + + + + + + {{ 'entity.aliases' | translate }} + + devices_other + + + + {{ 'dashboard.settings' | translate }} + + settings + + + +
+
+
+ + + {{ 'dashboard.manage-states' | translate }} + + layers + + + + {{ 'layout.manage' | translate }} + + view_compact + +
+
+ + + + +
+
+
-
- - dashboard.no-widgets - - - add - {{ 'dashboard.add-widget' | translate }} - -
@@ -116,46 +133,47 @@
+ ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">

{{ vm.dashboard.title }}

- - + +
-
- - +
+ + +
+ + + +
@@ -286,7 +305,7 @@ - @@ -296,7 +315,7 @@ @@ -308,7 +327,7 @@
-