thingsboard-developers
Changes
dao/src/main/resources/schema.cql 11(+6 -5)
ui/src/app/api/attribute.service.js 62(+55 -7)
ui/src/app/api/dashboard.service.js 35(+34 -1)
ui/src/app/api/device.service.js 18(+17 -1)
ui/src/app/api/entity.service.js 322(+311 -11)
ui/src/app/api/user.service.js 15(+11 -4)
ui/src/app/app.config.js 2(+1 -1)
ui/src/app/common/dashboard-utils.service.js 372(+351 -21)
ui/src/app/common/types.constant.js 11(+11 -0)
ui/src/app/components/dashboard.directive.js 187(+163 -24)
ui/src/app/components/widget-config.directive.js 205(+116 -89)
ui/src/app/dashboard/dashboard.controller.js 686(+480 -206)
ui/src/app/dashboard/dashboard.scss 36(+35 -1)
ui/src/app/dashboard/dashboard.tpl.html 241(+130 -111)
ui/src/app/dashboard/dashboard-settings.tpl.html 215(+121 -94)
ui/src/app/dashboard/index.js 10(+7 -3)
ui/src/app/dashboard/layouts/index.js 25(+25 -0)
ui/src/app/dashboard/states/index.js 29(+29 -0)
ui/src/app/locale/locale.constant.js 39(+38 -1)
ui/src/app/locale/translate-handler.js 26(+26 -0)
ui/src/app/services/item-buffer.service.js 186(+138 -48)
ui/src/app/user/user.controller.js 12(+9 -3)
ui/src/app/user/user.directive.js 4(+4 -0)
ui/src/app/user/user-fieldset.tpl.html 13(+11 -2)
ui/src/scss/main.scss 18(+17 -1)
Details
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 9b00682..d4adebe 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 3812610..2a6416c 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<DashboardInfo> 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 bebab8b..7cd381c 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<Device> findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException {
+ checkNotNull(query);
+ checkNotNull(query.getParameters());
+ checkNotNull(query.getDeviceTypes());
+ checkEntityId(query.getParameters().getEntityId());
+ try {
+ List<Device> 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 6b31283..92e8655 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<DeviceId> {
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<DeviceId> {
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<DeviceId> {
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<DeviceId> {
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<DeviceId> {
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<DeviceId> {
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 8c86064..b0ebbfd 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 8264042..cf554f3 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
@@ -65,6 +65,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
}
@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);
dashboardValidator.validate(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 0000000..eb9d9de
--- /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<String> 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 35d3496..4715435 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<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
+
+ ListenableFuture<List<Device>> 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 4394901..ab6fa4e 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<List<Device>> findDevicesByQuery(DeviceSearchQuery query) {
+ ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
+ ListenableFuture<List<Device>> devices = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Device>>) relations1 -> {
+ EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
+ List<ListenableFuture<Device>> 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<Device>, List<Device>>() {
+ @Nullable
+ @Override
+ public List<Device> apply(@Nullable List<Device> deviceList) {
+ return deviceList.stream().filter(device -> query.getDeviceTypes().contains(device.getType())).collect(Collectors.toList());
+ }
+ });
+
+ return devices;
+ }
+
private DataValidator<Device> deviceValidator =
new DataValidator<Device>() {
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 740bf2a..69ed92c 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<Device> {
@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<Device> {
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<Device> {
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<Device> {
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<Device> {
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<Device> {
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> {
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 2efbd5d..0c68c39 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";
dao/src/main/resources/schema.cql 11(+6 -5)
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index efaa987..071697e 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 0000000..3f5c55a
--- /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<EntityRelation> 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<EntityRelation> 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<EntityRelation> 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<EntityRelation> 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));
+ }
+}
ui/src/app/api/attribute.service.js 62(+55 -7)
diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js
index 35c14fb..53d5341 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<attributes.length;a++) {
- attributesData[attributes[a].key] = attributes[a].value;
+ if (angular.isDefined(attributes[a].value) && attributes[a].value !== null) {
+ attributesData[attributes[a].key] = attributes[a].value;
+ } else {
+ deleteAttributes.push(attributes[a]);
+ }
+ }
+ var deleteEntityAttributesPromise;
+ if (deleteAttributes.length) {
+ deleteEntityAttributesPromise = deleteEntityAttributes(entityType, entityId, attributeScope, deleteAttributes);
+ }
+ if (Object.keys(attributesData).length) {
+ var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
+ $http.post(url, attributesData).then(function success(response) {
+ if (deleteEntityAttributesPromise) {
+ deleteEntityAttributesPromise.then(
+ function success() {
+ deferred.resolve(response.data);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ )
+ } else {
+ deferred.resolve(response.data);
+ }
+ }, function fail() {
+ deferred.reject();
+ });
+ } else if (deleteEntityAttributesPromise) {
+ deleteEntityAttributesPromise.then(
+ function success() {
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ )
+ } else {
+ deferred.resolve();
}
- var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
- $http.post(url, attributesData).then(function success(response) {
- deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
- });
return deferred.promise;
}
ui/src/app/api/dashboard.service.js 35(+34 -1)
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index b2a7897..743fbe9 100644
--- a/ui/src/app/api/dashboard.service.js
+++ b/ui/src/app/api/dashboard.service.js
@@ -24,6 +24,8 @@ function DashboardService($http, $q, $location, customerService) {
getCustomerDashboards: getCustomerDashboards,
getServerTimeDiff: getServerTimeDiff,
getDashboard: getDashboard,
+ getDashboardInfo: getDashboardInfo,
+ getTenantDashboardsByTenantId: getTenantDashboardsByTenantId,
getTenantDashboards: getTenantDashboards,
deleteDashboard: deleteDashboard,
saveDashboard: saveDashboard,
@@ -34,6 +36,26 @@ function DashboardService($http, $q, $location, customerService) {
return service;
+ function getTenantDashboardsByTenantId(tenantId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/' + tenantId + '/dashboards?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
function getTenantDashboards(pageLink) {
var deferred = $q.defer();
var url = '/api/tenant/dashboards?limit=' + pageLink.limit;
@@ -94,7 +116,7 @@ function DashboardService($http, $q, $location, customerService) {
var deferred = $q.defer();
var url = '/api/dashboard/serverTime';
var ct1 = Date.now();
- $http.get(url, null).then(function success(response) {
+ $http.get(url, { ignoreLoading: true }).then(function success(response) {
var ct2 = Date.now();
var st = response.data;
var stDiff = Math.ceil(st - (ct1+ct2)/2);
@@ -116,6 +138,17 @@ function DashboardService($http, $q, $location, customerService) {
return deferred.promise;
}
+ function getDashboardInfo(dashboardId) {
+ var deferred = $q.defer();
+ var url = '/api/dashboard/info/' + dashboardId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
function saveDashboard(dashboard) {
var deferred = $q.defer();
var url = '/api/dashboard';
ui/src/app/api/device.service.js 18(+17 -1)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index 6f15363..d12d025 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -40,7 +40,8 @@ function DeviceService($http, $q, attributeService, customerService, types) {
saveDeviceAttributes: saveDeviceAttributes,
deleteDeviceAttributes: deleteDeviceAttributes,
sendOneWayRpcCommand: sendOneWayRpcCommand,
- sendTwoWayRpcCommand: sendTwoWayRpcCommand
+ sendTwoWayRpcCommand: sendTwoWayRpcCommand,
+ findByQuery: findByQuery
}
return service;
@@ -270,4 +271,19 @@ function DeviceService($http, $q, attributeService, customerService, types) {
return deferred.promise;
}
+ function findByQuery(query, ignoreErrors, config) {
+ var deferred = $q.defer();
+ var url = '/api/devices';
+ if (!config) {
+ config = {};
+ }
+ config = Object.assign(config, { ignoreErrors: ignoreErrors });
+ $http.post(url, query, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
}
ui/src/app/api/entity.service.js 322(+311 -11)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 4904a6a..43c537c 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -20,9 +20,9 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
.name;
/*@ngInject*/
-function EntityService($http, $q, userService, deviceService,
+function EntityService($http, $q, $filter, $translate, userService, deviceService,
assetService, tenantService, customerService,
- ruleService, pluginService, types, utils) {
+ ruleService, pluginService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@@ -31,7 +31,12 @@ function EntityService($http, $q, userService, deviceService,
processEntityAliases: processEntityAliases,
getEntityKeys: getEntityKeys,
checkEntityAlias: checkEntityAlias,
- createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
+ createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
+ getRelatedEntities: getRelatedEntities,
+ saveRelatedEntity: saveRelatedEntity,
+ getRelatedEntity: getRelatedEntity,
+ deleteRelatedEntity: deleteRelatedEntity,
+ moveEntity: moveEntity
};
return service;
@@ -64,14 +69,18 @@ function EntityService($http, $q, userService, deviceService,
function getEntity(entityType, entityId, config) {
var deferred = $q.defer();
var promise = getEntityPromise(entityType, entityId, config);
- promise.then(
- function success(result) {
- deferred.resolve(result);
- },
- function fail() {
- deferred.reject();
- }
- );
+ if (promise) {
+ promise.then(
+ function success(result) {
+ deferred.resolve(result);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.reject();
+ }
return deferred.promise;
}
@@ -474,4 +483,295 @@ function EntityService($http, $q, userService, deviceService,
}
}
+ function getRelatedEntities(rootEntityId, entityType, entitySubTypes, maxLevel, keys, typeTranslatePrefix) {
+ var deferred = $q.defer();
+
+ var entitySearchQuery = constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel);
+ if (!entitySearchQuery) {
+ deferred.reject();
+ } else {
+ var findByQueryPromise;
+ if (entityType == types.entityType.asset) {
+ findByQueryPromise = assetService.findByQuery(entitySearchQuery, true, {ignoreLoading: true});
+ } else if (entityType == types.entityType.device) {
+ findByQueryPromise = deviceService.findByQuery(entitySearchQuery, true, {ignoreLoading: true});
+ }
+ findByQueryPromise.then(
+ function success(entities) {
+ var entitiesTasks = [];
+ for (var i=0;i<entities.length;i++) {
+ var entity = entities[i];
+ var entityPromise = constructEntity(entity, keys, typeTranslatePrefix);
+ entitiesTasks.push(entityPromise);
+ }
+ $q.all(entitiesTasks).then(
+ function success(entities) {
+ deferred.resolve(entities);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function saveRelatedEntity(relatedEntity, parentEntityId, keys) {
+ var deferred = $q.defer();
+ if (relatedEntity.id.id) {
+ updateRelatedEntity(relatedEntity, keys, deferred);
+ } else {
+ addRelatedEntity(relatedEntity, parentEntityId, keys, deferred);
+ }
+ return deferred.promise;
+ }
+
+ function getRelatedEntity(entityId, keys, typeTranslatePrefix) {
+ var deferred = $q.defer();
+ getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then(
+ function success(entity) {
+ constructEntity(entity, keys, typeTranslatePrefix).then(
+ function success(relatedEntity) {
+ deferred.resolve(relatedEntity);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function deleteEntityPromise(entityId) {
+ if (entityId.entityType == types.entityType.asset) {
+ return assetService.deleteAsset(entityId.id);
+ } else if (entityId.entityType == types.entityType.device) {
+ return deviceService.deleteDevice(entityId.id);
+ }
+ }
+
+ function deleteRelatedEntity(entityId, deleteRelatedEntityTypes) {
+ var deferred = $q.defer();
+ if (deleteRelatedEntityTypes) {
+ var deleteRelatedEntitiesTasks = [];
+ entityRelationService.findByFrom(entityId.id, entityId.entityType).then(
+ function success(entityRelations) {
+ for (var i=0;i<entityRelations.length;i++) {
+ var entityRelation = entityRelations[i];
+ var relationEntityId = entityRelation.to;
+ if (deleteRelatedEntityTypes.length == 0 || deleteRelatedEntityTypes.indexOf(relationEntityId.entityType) > -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<keys.length;i++) {
+ var key = keys[i];
+ entity[key] = getAttributeValue(attributes, key);
+ }
+ }
+ deferred.resolve(entity);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAttributeValue(attributes, key) {
+ var foundAttributes = $filter('filter')(attributes, {key: key}, true);
+ if (foundAttributes.length > 0) {
+ return foundAttributes[0].value;
+ } else {
+ return null;
+ }
+ }
+
}
\ No newline at end of file
ui/src/app/api/user.service.js 15(+11 -4)
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 4dc41f7..a5bb36c 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<dashboards.length;d++) {
@@ -296,7 +302,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
if (userForceFullscreen()) {
$rootScope.forceFullscreen = true;
}
- if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
+ if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' ||
+ currentUser.authority === 'CUSTOMER_USER')) {
fetchAllowedDashboardIds();
} else {
deferred.resolve();
@@ -436,7 +443,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) {
- if (currentUser.authority === 'CUSTOMER_USER') {
+ if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
if (to.name === 'home.profile') {
if (userHasProfile()) {
@@ -458,7 +465,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
function gotoDefaultPlace(params) {
if (currentUser && isAuthenticated()) {
var place = 'home.links';
- if (currentUser.authority === 'CUSTOMER_USER') {
+ if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
if (userHasDefaultDashboard()) {
place = 'home.dashboards.dashboard';
params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
ui/src/app/app.config.js 2(+1 -1)
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index 7f7e7a5..fef3273 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -49,7 +49,7 @@ export default function AppConfig($provide,
$translateProvider.useSanitizeValueStrategy('sce');
$translateProvider.preferredLanguage('en_US');
$translateProvider.useLocalStorage();
- $translateProvider.useMissingTranslationHandlerLog();
+ $translateProvider.useMissingTranslationHandler('tbMissingTranslationHandler');
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
addLocaleKorean(locales);
ui/src/app/common/dashboard-utils.service.js 372(+351 -21)
diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
index 0bc73d1..57317b5 100644
--- a/ui/src/app/common/dashboard-utils.service.js
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -19,10 +19,22 @@ export default angular.module('thingsboard.dashboardUtils', [])
.name;
/*@ngInject*/
-function DashboardUtils(types, timeService) {
+function DashboardUtils(types, utils, timeService) {
var service = {
- validateAndUpdateDashboard: validateAndUpdateDashboard
+ validateAndUpdateDashboard: validateAndUpdateDashboard,
+ getRootStateId: getRootStateId,
+ createSingleWidgetDashboard: createSingleWidgetDashboard,
+ getStateLayoutsData: getStateLayoutsData,
+ createDefaultState: createDefaultState,
+ createDefaultLayoutData: createDefaultLayoutData,
+ setLayouts: setLayouts,
+ updateLayoutSettings: updateLayoutSettings,
+ addWidgetToLayout: addWidgetToLayout,
+ removeWidgetFromLayout: removeWidgetFromLayout,
+ isSingleLayoutDashboard: isSingleLayoutDashboard,
+ removeUnusedWidgets: removeUnusedWidgets,
+ getWidgetsArray: getWidgetsArray
};
return service;
@@ -69,39 +81,357 @@ function DashboardUtils(types, timeService) {
widget.config.datasources = [];
}
widget.config.datasources.forEach(function(datasource) {
- if (datasource.type === 'device') {
- datasource.type = types.datasourceType.entity;
- }
- if (datasource.deviceAliasId) {
- datasource.entityAliasId = datasource.deviceAliasId;
- delete datasource.deviceAliasId;
- }
+ if (datasource.type === 'device') {
+ datasource.type = types.datasourceType.entity;
+ }
+ if (datasource.deviceAliasId) {
+ datasource.entityAliasId = datasource.deviceAliasId;
+ delete datasource.deviceAliasId;
+ }
});
+ return widget;
+ }
+
+ function createDefaultLayoutData() {
+ return {
+ widgets: {},
+ gridSettings: {
+ backgroundColor: '#eeeeee',
+ color: 'rgba(0,0,0,0.870588)',
+ columns: 24,
+ margins: [10, 10],
+ backgroundSizeMode: '100%'
+ }
+ };
+ }
+
+ function createDefaultLayouts() {
+ return {
+ 'main': createDefaultLayoutData()
+ };
+ }
+
+ function createDefaultState(name, root) {
+ return {
+ name: name,
+ root: root,
+ layouts: createDefaultLayouts()
+ }
}
function validateAndUpdateDashboard(dashboard) {
if (!dashboard.configuration) {
- dashboard.configuration = {
- widgets: [],
- entityAliases: {}
- };
+ dashboard.configuration = {};
}
if (angular.isUndefined(dashboard.configuration.widgets)) {
- dashboard.configuration.widgets = [];
+ dashboard.configuration.widgets = {};
+ } else if (angular.isArray(dashboard.configuration.widgets)) {
+ var widgetsMap = {};
+ dashboard.configuration.widgets.forEach(function (widget) {
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ widgetsMap[widget.id] = validateAndUpdateWidget(widget);
+ });
+ dashboard.configuration.widgets = widgetsMap;
}
- dashboard.configuration.widgets.forEach(function(widget) {
- validateAndUpdateWidget(widget);
- });
+ if (angular.isUndefined(dashboard.configuration.states)) {
+ dashboard.configuration.states = {
+ 'default': createDefaultState('Default', true)
+ };
+
+ var mainLayout = dashboard.configuration.states['default'].layouts['main'];
+ for (var id in dashboard.configuration.widgets) {
+ var widget = dashboard.configuration.widgets[id];
+ mainLayout.widgets[id] = {
+ sizeX: widget.sizeX,
+ sizeY: widget.sizeY,
+ row: widget.row,
+ col: widget.col,
+ };
+ }
+ } else {
+ var states = dashboard.configuration.states;
+ var rootFound = false;
+ for (var stateId in states) {
+ var state = states[stateId];
+ if (angular.isUndefined(state.root)) {
+ state.root = false;
+ } else if (state.root) {
+ rootFound = true;
+ }
+ }
+ if (!rootFound) {
+ var firstStateId = Object.keys(states)[0];
+ states[firstStateId].root = true;
+ }
+ }
+ dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
+
if (angular.isUndefined(dashboard.configuration.timewindow)) {
dashboard.configuration.timewindow = timeService.defaultTimewindow();
}
+ if (angular.isUndefined(dashboard.configuration.settings)) {
+ dashboard.configuration.settings = {};
+ dashboard.configuration.settings.stateControllerId = 'default';
+ dashboard.configuration.settings.showTitle = true;
+ dashboard.configuration.settings.showDashboardsSelect = true;
+ dashboard.configuration.settings.showEntitiesSelect = true;
+ dashboard.configuration.settings.showDashboardTimewindow = true;
+ dashboard.configuration.settings.showDashboardExport = true;
+ } else {
+ if (angular.isUndefined(dashboard.configuration.settings.stateControllerId)) {
+ dashboard.configuration.settings.stateControllerId = 'default';
+ }
+ }
if (angular.isDefined(dashboard.configuration.gridSettings)) {
- if (angular.isDefined(dashboard.configuration.gridSettings.showDevicesSelect)) {
- dashboard.configuration.gridSettings.showEntitiesSelect = dashboard.configuration.gridSettings.showDevicesSelect;
- delete dashboard.configuration.gridSettings.showDevicesSelect;
+ var gridSettings = dashboard.configuration.gridSettings;
+ if (angular.isDefined(gridSettings.showTitle)) {
+ dashboard.configuration.settings.showTitle = gridSettings.showTitle;
+ delete gridSettings.showTitle;
+ }
+ if (angular.isDefined(gridSettings.titleColor)) {
+ dashboard.configuration.settings.titleColor = gridSettings.titleColor;
+ delete gridSettings.titleColor;
+ }
+ if (angular.isDefined(gridSettings.showDevicesSelect)) {
+ dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect;
+ delete gridSettings.showDevicesSelect;
+ }
+ if (angular.isDefined(gridSettings.showEntitiesSelect)) {
+ dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect;
+ delete gridSettings.showEntitiesSelect;
+ }
+ if (angular.isDefined(gridSettings.showDashboardTimewindow)) {
+ dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow;
+ delete gridSettings.showDashboardTimewindow;
+ }
+ if (angular.isDefined(gridSettings.showDashboardExport)) {
+ dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport;
+ delete gridSettings.showDashboardExport;
}
+ dashboard.configuration.states['default'].layouts['main'].gridSettings = gridSettings;
+ delete dashboard.configuration.gridSettings;
}
- dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
return dashboard;
}
+
+ function getRootStateId(states) {
+ for (var stateId in states) {
+ var state = states[stateId];
+ if (state.root) {
+ return stateId;
+ }
+ }
+ return Object.keys(states)[0];
+ }
+
+ function createSingleWidgetDashboard(widget) {
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ var dashboard = {};
+ dashboard = validateAndUpdateDashboard(dashboard);
+ dashboard.configuration.widgets[widget.id] = widget;
+ dashboard.configuration.states['default'].layouts['main'].widgets[widget.id] = {
+ sizeX: widget.sizeX,
+ sizeY: widget.sizeY,
+ row: widget.row,
+ col: widget.col,
+ };
+ return dashboard;
+ }
+
+ function getStateLayoutsData(dashboard, targetState) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ if (state) {
+ var allWidgets = dashboardConfiguration.widgets;
+ var result = {};
+ for (var l in state.layouts) {
+ var layout = state.layouts[l];
+ if (layout) {
+ result[l] = {
+ widgets: [],
+ widgetLayouts: {},
+ gridSettings: {}
+ }
+ for (var id in layout.widgets) {
+ result[l].widgets.push(allWidgets[id]);
+ }
+ result[l].widgetLayouts = layout.widgets;
+ result[l].gridSettings = layout.gridSettings;
+ }
+ }
+ return result;
+ } else {
+ return null;
+ }
+ }
+
+ function setLayouts(dashboard, targetState, newLayouts) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ var addedCount = 0;
+ var removedCount = 0;
+ for (var l in state.layouts) {
+ if (!newLayouts[l]) {
+ removedCount++;
+ }
+ }
+ for (l in newLayouts) {
+ if (!state.layouts[l]) {
+ addedCount++;
+ }
+ }
+ state.layouts = newLayouts;
+ var layoutsCount = Object.keys(state.layouts).length;
+ var newColumns;
+ if (addedCount) {
+ for (l in state.layouts) {
+ newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount;
+ state.layouts[l].gridSettings.columns = newColumns;
+ }
+ }
+ if (removedCount) {
+ for (l in state.layouts) {
+ newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount;
+ state.layouts[l].gridSettings.columns = newColumns;
+ }
+ }
+ removeUnusedWidgets(dashboard);
+ }
+
+ function updateLayoutSettings(layout, gridSettings) {
+ var prevGridSettings = layout.gridSettings;
+ var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
+ var ratio = gridSettings.columns / prevColumns;
+ layout.gridSettings = gridSettings;
+ for (var w in layout.widgets) {
+ var widget = layout.widgets[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);
+ }
+ }
+
+ function addWidgetToLayout(dashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ var layout = state.layouts[targetLayout];
+ var layoutCount = Object.keys(state.layouts).length;
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ if (!dashboardConfiguration.widgets[widget.id]) {
+ dashboardConfiguration.widgets[widget.id] = widget;
+ }
+ var widgetLayout = {
+ sizeX: originalSize ? originalSize.sizeX : widget.sizeX,
+ sizeY: originalSize ? originalSize.sizeY : widget.sizeY,
+ mobileOrder: widget.config.mobileOrder,
+ mobileHeight: widget.config.mobileHeight
+ };
+
+ if (angular.isUndefined(originalColumns)) {
+ originalColumns = 24;
+ }
+
+ var gridSettings = layout.gridSettings;
+ var columns = 24;
+ if (gridSettings && gridSettings.columns) {
+ columns = gridSettings.columns;
+ }
+
+ columns = columns * layoutCount;
+
+ if (columns != originalColumns) {
+ var ratio = columns / originalColumns;
+ widgetLayout.sizeX *= ratio;
+ widgetLayout.sizeY *= ratio;
+ }
+
+ if (row > -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;
+ }
}
ui/src/app/common/types.constant.js 11(+11 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index ae3556f..a8d8556 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;
ui/src/app/components/dashboard.directive.js 187(+163 -24)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index b20c039..f26121c 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<vm.widgets.length;i++) {
+ var widget = vm.widgets[i];
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ ids.push(widget.id);
+ var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
+ if (!layoutInfoObject) {
+ layoutInfoObject = {
+ widget: widget
+ };
+ Object.defineProperty(layoutInfoObject, 'sizeX', {
+ get: function() { return widgetSizeX(this.widget) },
+ set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
+ });
+ Object.defineProperty(layoutInfoObject, 'sizeY', {
+ get: function() { return widgetSizeY(this.widget) },
+ set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
+ });
+ Object.defineProperty(layoutInfoObject, 'row', {
+ get: function() { return widgetRow(this.widget) },
+ set: function(newRow) { setWidgetRow(this.widget, newRow)}
+ });
+ Object.defineProperty(layoutInfoObject, 'col', {
+ get: function() { return widgetCol(this.widget) },
+ set: function(newCol) { setWidgetCol(this.widget, newCol)}
+ });
+ vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
+ }
+ }
+ for (var widgetId in vm.widgetLayoutInfo) {
+ if (ids.indexOf(widgetId) === -1) {
+ delete vm.widgetLayoutInfo[widgetId];
+ }
+ }
+ });
+
//TODO: widgets visibility
/*gridsterParent.scroll(function () {
updateVisibleRect();
@@ -279,6 +336,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
$scope.$on('gridster-resized', function (event, sizes, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
+ vm.isResizing = false;
//TODO: widgets visibility
//updateVisibleRect(false, true);
}
@@ -302,20 +360,22 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
});
+ function widgetOrder(widget) {
+ var order;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ order = vm.widgetLayouts[widget.id].mobileOrder;
+ } else if (widget.config.mobileOrder) {
+ order = widget.config.mobileOrder;
+ } else {
+ order = widget.row;
+ }
+ return order;
+ }
+
$scope.$on('widgetPositionChanged', function () {
vm.widgets.sort(function (widget1, widget2) {
- var row1;
- var row2;
- if (angular.isDefined(widget1.config.mobileOrder)) {
- row1 = widget1.config.mobileOrder;
- } else {
- row1 = widget1.row;
- }
- if (angular.isDefined(widget2.config.mobileOrder)) {
- row2 = widget2.config.mobileOrder;
- } else {
- row2 = widget2.row;
- }
+ var row1 = widgetOrder(widget1);
+ var row2 = widgetOrder(widget2);
var res = row1 - row2;
if (res === 0) {
res = widget1.col - widget2.col;
@@ -326,6 +386,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
loadStDiff();
+ function reload() {
+ loadStDiff();
+ }
+
function loadStDiff() {
if (vm.getStDiff) {
var promise = vm.getStDiff();
@@ -568,18 +632,89 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function widgetSizeX(widget) {
- return widget.sizeX;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].sizeX;
+ } else {
+ return widget.sizeX;
+ }
+ }
+
+ function setWidgetSizeX(widget, sizeX) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].sizeX = sizeX;
+ } else {
+ widget.sizeX = sizeX;
+ }
+ }
}
function widgetSizeY(widget) {
if (vm.gridsterOpts.isMobile) {
- if (widget.config.mobileHeight) {
- return widget.config.mobileHeight;
+ var mobileHeight;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ mobileHeight = vm.widgetLayouts[widget.id].mobileHeight;
+ }
+ if (!mobileHeight && widget.config.mobileHeight) {
+ mobileHeight = widget.config.mobileHeight;
+ }
+ if (mobileHeight) {
+ return mobileHeight;
} else {
return widget.sizeY * 24 / vm.gridsterOpts.columns;
}
} else {
- return widget.sizeY;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].sizeY;
+ } else {
+ return widget.sizeY;
+ }
+ }
+ }
+
+ function setWidgetSizeY(widget, sizeY) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].sizeY = sizeY;
+ } else {
+ widget.sizeY = sizeY;
+ }
+ }
+ }
+
+ function widgetRow(widget) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].row;
+ } else {
+ return widget.row;
+ }
+ }
+
+ function setWidgetRow(widget, row) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].row = row;
+ } else {
+ widget.row = row;
+ }
+ }
+ }
+
+ function widgetCol(widget) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].col;
+ } else {
+ return widget.col;
+ }
+ }
+
+ function setWidgetCol(widget, col) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].col = col;
+ } else {
+ widget.col = col;
+ }
}
}
@@ -653,7 +788,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
var maxRows = vm.gridsterOpts.maxRows;
for (var i = 0; i < vm.widgets.length; i++) {
var w = vm.widgets[i];
- var bottom = w.row + w.sizeY;
+ var bottom = widgetRow(w) + widgetSizeY(w);
maxRows = Math.max(maxRows, bottom);
}
vm.gridsterOpts.maxRows = Math.max(maxRows, vm.gridsterOpts.maxRows);
@@ -662,7 +797,11 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
function dashboardLoaded() {
$timeout(function () {
- $scope.$watch('vm.dashboardTimewindow', function () {
+ if (vm.dashboardTimewindowWatch) {
+ vm.dashboardTimewindowWatch();
+ vm.dashboardTimewindowWatch = null;
+ }
+ vm.dashboardTimewindowWatch = $scope.$watch('vm.dashboardTimewindow', function () {
$scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow);
}, true);
adoptMaxRows();
@@ -678,7 +817,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function loading() {
- return $rootScope.loading;
+ return !vm.ignoreLoading && $rootScope.loading;
}
}
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 3d11e46..69934ac 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -16,8 +16,10 @@
-->
<md-content flex layout="column" class="tb-progress-cover" layout-align="center center"
- ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
- <md-progress-circular md-mode="indeterminate" ng-disabled="!vm.loading() && !vm.dashboardLoading || vm.isEdit" class="md-warn" md-diameter="100"></md-progress-circular>
+ ng-style="vm.dashboardStyle"
+ ng-show="((vm.loading() || vm.dashboardLoading) && !vm.isEdit) || vm.isResizing">
+ <md-progress-circular md-mode="indeterminate" ng-disabled="(!vm.loading() && !vm.dashboardLoading || vm.isEdit) && !vm.isResizing" class="md-warn" md-diameter="100">
+ </md-progress-circular>
</md-content>
<md-menu md-position-mode="target target" tb-mousepoint-menu>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
@@ -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/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js
index afa5f56..77e3c1c 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/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js
new file mode 100644
index 0000000..f93ded2
--- /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 0000000..32df94f
--- /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 0000000..a5b50e9
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.tpl.html
@@ -0,0 +1,43 @@
+<!--
+
+ 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.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+ ng-disabled="disabled"
+ md-input-name="entity"
+ ng-model="entity"
+ md-selected-item="entity"
+ md-search-text="entitySearchText"
+ md-search-text-change="entitySearchTextChanged()"
+ md-items="item in fetchEntities(entitySearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ md-floating-label="{{ placeholderText | translate }}"
+ md-menu-class="tb-related-entity-autocomplete">
+ <md-item-template>
+ <div class="tb-entity-item">
+ <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
+ </div>
+ </md-item-template>
+ <md-not-found>
+ <div class="tb-not-found">
+ <span>{{ notFoundText | translate:{entity: entitySearchText} }}</span>
+ </div>
+ </md-not-found>
+ <div ng-messages="theForm.entity.$error">
+ <div ng-message="required">{{ requiredText | translate }}</div>
+ </div>
+</md-autocomplete>
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index ec5e7ed..6fc4bba 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 = {
ui/src/app/components/widget-config.directive.js 205(+116 -89)
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index 6c7c472..122889d 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/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index de3bf95..2f23cf9 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});
}
}
ui/src/app/dashboard/dashboard.controller.js 686(+480 -206)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 79ee6ae..e18ebb8 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 e9fe1f2..92bb362 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: {
ui/src/app/dashboard/dashboard.scss 36(+35 -1)
diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss
index 8f50ca2..bc5ec56 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;
+ }
+ }
+ }
}
/*****************************
ui/src/app/dashboard/dashboard.tpl.html 241(+130 -111)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 3286f8d..078daab 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,16 +16,10 @@
-->
<md-content flex tb-expand-fullscreen="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-button-id="dashboard-expand-button"
- hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom"
- ng-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
- 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
- 'background-repeat': 'no-repeat',
- 'background-attachment': 'scroll',
- 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
- 'background-position': '0% 0%'}">
+ hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom">
<section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
- <md-fab-toolbar md-open="vm.toolbarOpened"
+ <md-fab-toolbar ng-show="!vm.widgetEditMode" md-open="vm.toolbarOpened"
md-direction="left">
<md-fab-trigger class="align-with-text">
<md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.openToolbar()">
@@ -37,77 +31,100 @@
</md-fab-trigger>
<md-toolbar>
<md-fab-actions class="md-toolbar-tools">
- <md-button ng-show="!vm.isEdit" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
- <md-tooltip md-direction="bottom">
- {{ 'dashboard.close-toolbar' | translate }}
- </md-tooltip>
- <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
- </md-button>
- <md-button id="dashboard-expand-button"
- aria-label="{{ 'fullscreen.fullscreen' | translate }}"
- class="md-icon-button">
- </md-button>
- <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
- </tb-user-menu>
- <md-button ng-show="vm.isEdit || vm.displayExport()"
- aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
- ng-click="vm.exportDashboard($event)">
- <md-tooltip md-direction="bottom">
- {{ 'dashboard.export' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
- </md-button>
- <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
- is-toolbar
- direction="left"
- tooltip-direction="bottom" aggregation
- ng-model="vm.dashboardConfiguration.timewindow">
- </tb-timewindow>
- <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
- tooltip-direction="bottom"
- ng-model="vm.aliasesInfo.entityAliases"
- entity-aliases-info="vm.aliasesInfo.entityAliasesInfo">
- </tb-aliases-entity-select>
- <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
- ng-click="vm.openEntityAliases($event)">
- <md-tooltip md-direction="bottom">
- {{ 'entity.aliases' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
- </md-button>
- <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
- ng-click="vm.openDashboardSettings($event)">
- <md-tooltip md-direction="bottom">
- {{ 'dashboard.settings' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
- </md-button>
- <tb-dashboard-select ng-show="!vm.isEdit && !vm.widgetEditMode"
- ng-model="vm.currentDashboardId"
- dashboards-scope="{{vm.currentDashboardScope}}"
- customer-id="vm.currentCustomerId">
- </tb-dashboard-select>
+ <div class="tb-dashboard-action-panels" flex layout="row" layout-align="start center">
+ <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="start center">
+ <md-button ng-show="vm.showCloseToolbar()" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.close-toolbar' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
+ </md-button>
+ <md-button ng-show="vm.showRightLayoutSwitch()" aria-label="switch-layouts" class="md-icon-button" ng-click="vm.toggleLayouts()">
+ <ng-md-icon icon="{{vm.isRightLayoutOpened ? 'arrow_back' : 'menu'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+ <md-tooltip md-direction="bottom">
+ {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button id="dashboard-expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button">
+ </md-button>
+ <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
+ </tb-user-menu>
+ <md-button ng-show="vm.isEdit || vm.displayExport()"
+ aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
+ ng-click="vm.exportDashboard($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.export' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
+ </md-button>
+ <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
+ is-toolbar
+ direction="left"
+ tooltip-direction="bottom" aggregation
+ ng-model="vm.dashboardConfiguration.timewindow">
+ </tb-timewindow>
+ <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
+ tooltip-direction="bottom"
+ ng-model="vm.dashboardCtx.aliasesInfo.entityAliases"
+ entity-aliases-info="vm.dashboardCtx.aliasesInfo.entityAliasesInfo">
+ </tb-aliases-entity-select>
+ <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
+ ng-click="vm.openEntityAliases($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'entity.aliases' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
+ </md-button>
+ <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
+ ng-click="vm.openDashboardSettings($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.settings' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
+ </md-button>
+ <tb-dashboard-select ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
+ ng-model="vm.currentDashboardId"
+ dashboards-scope="{{vm.currentDashboardScope}}"
+ customer-id="vm.currentCustomerId">
+ </tb-dashboard-select>
+ </div>
+ <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="end center">
+ <div layout="row" layout-align="start center" ng-show="vm.isEdit">
+ <md-button aria-label="{{ 'dashboard.manage-states' | translate }}" class="md-icon-button"
+ ng-click="vm.manageDashboardStates($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.manage-states' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'dashboard.manage-states' | translate }}" class="material-icons">layers</md-icon>
+ </md-button>
+ <md-button aria-label="{{ 'layout.manage' | translate }}" class="md-icon-button"
+ ng-click="vm.manageDashboardLayouts($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'layout.manage' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'layout.manage' | translate }}" class="material-icons">view_compact</md-icon>
+ </md-button>
+ </div>
+ <div layout="row" layout-align="start center">
+ <tb-states-component ng-if="vm.isEdit" states-controller-id="'default'"
+ dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+ </tb-states-component>
+ <tb-states-component ng-if="!vm.isEdit" states-controller-id="vm.dashboardConfiguration.settings.stateControllerId"
+ dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+ </tb-states-component>
+ </div>
+ </div>
+ </div>
</md-fab-actions>
</md-toolbar>
</md-fab-toolbar>
</section>
<section class="tb-dashboard-container tb-absolute-fill"
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
- <section ng-show="!loading && vm.noData()" layout-align="center center"
- ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
- ng-class="{'tb-padded' : !vm.widgetEditMode}"
- style="text-transform: uppercase; display: flex; z-index: 1;"
- class="md-headline tb-absolute-fill">
- <span translate ng-if="!vm.isEdit">
- dashboard.no-widgets
- </span>
- <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">
- <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
- {{ 'dashboard.add-widget' | translate }}
- </md-button>
- </section>
<section ng-show="!loading && vm.dashboardConfigurationError()" layout-align="center center"
- ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
+ ng-style="{'color': vm.dashboard.configuration.settings.titleColor}"
ng-class="{'tb-padded' : !vm.widgetEditMode}"
style="text-transform: uppercase; display: flex; z-index: 1;"
class="md-headline tb-absolute-fill">
@@ -116,46 +133,47 @@
</span>
</section>
<section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center"
- ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
+ ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">
<h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
<md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
- <label translate ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">dashboard.title</label>
- <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}" required name="title" ng-model="vm.dashboard.title">
+ <label translate ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">dashboard.title</label>
+ <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.settings.titleColor}" required name="title" ng-model="vm.dashboard.title">
</md-input-container>
</section>
- <div class="tb-absolute-fill"
+ <div class="tb-absolute-fill tb-dashboard-layouts" layout="{{vm.forceDashboardMobileMode ? 'column' : 'row'}}"
ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
- <tb-dashboard
- dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
- 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
- 'background-repeat': 'no-repeat',
- 'background-attachment': 'scroll',
- 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
- 'background-position': '0% 0%'}"
- widgets="vm.widgets"
- columns="vm.dashboard.configuration.gridSettings.columns"
- margins="vm.dashboard.configuration.gridSettings.margins"
- aliases-info="vm.aliasesInfo"
- dashboard-timewindow="vm.dashboardConfiguration.timewindow"
- is-edit="vm.isEdit"
- is-mobile="vm.forceDashboardMobileMode"
- is-mobile-disabled="vm.widgetEditMode"
- is-edit-action-enabled="vm.isEdit"
- is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
- is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
- on-edit-widget="vm.editWidget(event, widget)"
- on-export-widget="vm.exportWidget(event, widget)"
- on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
- on-widget-clicked="vm.widgetClicked(event, widget)"
- on-widget-context-menu="vm.widgetContextMenu(event, widget)"
- prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
- prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
- on-remove-widget="vm.removeWidget(event, widget)"
- load-widgets="vm.loadDashboard()"
- get-st-diff="vm.getServerTimeDiff()"
- on-init="vm.dashboardInited(dashboard)"
- on-init-failed="vm.dashboardInitFailed(e)">
- </tb-dashboard>
+ <div ng-show="vm.layouts.main.show"
+ id="tb-main-layout"
+ ng-style="{width: vm.mainLayoutWidth(),
+ height: vm.mainLayoutHeight()}">
+ <tb-dashboard-layout layout-ctx="vm.layouts.main.layoutCtx"
+ dashboard-ctx="vm.dashboardCtx"
+ is-edit="vm.isEdit"
+ is-mobile="vm.forceDashboardMobileMode"
+ widget-edit-mode="vm.widgetEditMode"
+ get-st-diff="vm.getServerTimeDiff()">
+ </tb-dashboard-layout>
+ </div>
+ <md-sidenav ng-if="vm.layouts.right.show"
+ id="tb-right-layout"
+ class="md-sidenav-right"
+ ng-style="{minWidth: vm.rightLayoutWidth(),
+ maxWidth: vm.rightLayoutWidth(),
+ height: vm.rightLayoutHeight(),
+ zIndex: 1}"
+ md-component-id="right-dashboard-layout"
+ aria-label="Right dashboard layout"
+ md-is-open="!vm.isMobile || vm.isRightLayoutOpened"
+ md-is-locked-open="!vm.isMobile">
+ <tb-dashboard-layout style="height: 100%;"
+ layout-ctx="vm.layouts.right.layoutCtx"
+ dashboard-ctx="vm.dashboardCtx"
+ is-edit="vm.isEdit"
+ is-mobile="vm.forceDashboardMobileMode"
+ widget-edit-mode="vm.widgetEditMode"
+ get-st-diff="vm.getServerTimeDiff()">
+ </tb-dashboard-layout>
+ </md-sidenav>
</div>
<tb-details-sidenav class="tb-widget-details-sidenav"
header-title="{{vm.editingWidget.config.title}}"
@@ -173,8 +191,9 @@
<form name="vm.widgetForm" ng-if="vm.isEditingWidget">
<tb-edit-widget
dashboard="vm.dashboard"
- aliases-info="vm.aliasesInfo"
+ aliases-info="vm.dashboardCtx.aliasesInfo"
widget="vm.editingWidget"
+ widget-layout="vm.editingWidgetLayout"
the-form="vm.widgetForm">
</tb-edit-widget>
</form>
@@ -286,7 +305,7 @@
</md-button>
</md-fab-actions>
</md-fab-speed-dial>
- <md-button ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
+ <md-button ng-if="(vm.isTenantAdmin() || vm.isSystemAdmin()) && !forceFullscreen" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.saveDashboard()">
@@ -296,7 +315,7 @@
<ng-md-icon icon="done"></ng-md-icon>
</md-button>
<md-button ng-show="!vm.isAddingWidget && !loading"
- ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-disabled="loading"
+ ng-if="(vm.isTenantAdmin() || vm.isSystemAdmin()) && !forceFullscreen" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.edit-mode' | translate }}"
ng-click="vm.toggleDashboardEditMode()">
@@ -308,7 +327,7 @@
</md-button>
</section>
</section>
- <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
+ <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">
<span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ vm.thingsboardVersion }}</a></span>
</section>
</md-content>
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index ee107cc..dbe4cbe 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
+ }
+ );
}
}
ui/src/app/dashboard/dashboard-settings.tpl.html 215(+121 -94)
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index 6ae4746..88fc66d 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -19,7 +19,7 @@
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
- <h2 translate>dashboard.settings</h2>
+ <h2 translate>{{vm.settings ? 'dashboard.settings' : 'layout.settings'}}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
@@ -31,15 +31,53 @@
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
- <div layout="row" layout-align="start center">
- <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
- ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
- </md-checkbox>
+ <div ng-show="vm.settings">
+ <md-input-container class="md-block">
+ <label translate>dashboard.state-controller</label>
+ <md-select aria-label="{{ 'dashboard.state-controller' | translate }}" ng-model="vm.settings.stateControllerId">
+ <md-option ng-repeat="(stateControllerId, stateController) in vm.stateControllers" ng-value="stateControllerId">
+ {{stateControllerId}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <div layout="row" layout-align="start center">
+ <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
+ ng-model="vm.settings.showTitle">{{ 'dashboard.display-title' | translate }}
+ </md-checkbox>
+ <div flex
+ ng-required="false"
+ md-color-picker
+ ng-model="vm.settings.titleColor"
+ label="{{ 'dashboard.title-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0, 0, 0, 0.870588)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ </div>
+ <div layout="row" layout-align="start center">
+ <md-checkbox flex aria-label="{{ 'dashboard.display-dashboards-selection' | translate }}"
+ ng-model="vm.settings.showDashboardsSelect">{{ 'dashboard.display-dashboards-selection' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-entities-selection' | translate }}"
+ ng-model="vm.settings.showEntitiesSelect">{{ 'dashboard.display-entities-selection' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
+ ng-model="vm.settings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
+ ng-model="vm.settings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
+ </md-checkbox>
+ </div>
+ </div>
+ <div ng-show="vm.gridSettings">
<div flex
ng-required="false"
md-color-picker
- ng-model="vm.gridSettings.titleColor"
- label="{{ 'dashboard.title-color' | translate }}"
+ ng-model="vm.gridSettings.color"
+ label="{{ 'layout.color' | translate }}"
icon="format_color_fill"
default="rgba(0, 0, 0, 0.870588)"
md-color-clear-button="false"
@@ -47,98 +85,87 @@
md-color-generic-palette="false"
md-color-history="false"
></div>
- </div>
- <div layout="row" layout-align="start center">
- <md-checkbox flex aria-label="{{ 'dashboard.display-entities-selection' | translate }}"
- ng-model="vm.gridSettings.showEntitiesSelect">{{ 'dashboard.display-entities-selection' | translate }}
- </md-checkbox>
- <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
- ng-model="vm.gridSettings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
- </md-checkbox>
- <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
- ng-model="vm.gridSettings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
- </md-checkbox>
- </div>
- <md-input-container class="md-block">
- <label translate>dashboard.columns-count</label>
- <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
- max="1000" />
- <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
- <div ng-message="required" translate>dashboard.columns-count-required</div>
- <div ng-message="min" translate>dashboard.min-columns-count-message</div>
- <div ng-message="max">dashboard.max-columns-count-message</div>
- </div>
- </md-input-container>
- <small translate>dashboard.widgets-margins</small>
- <div flex layout="row">
- <md-input-container flex class="md-block">
- <label translate>dashboard.horizontal-margin</label>
- <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
- max="50" />
- <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
- <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
- <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
- <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
- </div>
- </md-input-container>
- <md-input-container flex class="md-block">
- <label translate>dashboard.vertical-margin</label>
- <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
- max="50" />
- <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
- <div ng-message="required" translate>dashboard.vertical-margin-required</div>
- <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
- <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+ <md-input-container class="md-block">
+ <label translate>dashboard.columns-count</label>
+ <input ng-required="vm.gridSettings" type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
+ max="1000" />
+ <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.columns-count-required</div>
+ <div ng-message="min" translate>dashboard.min-columns-count-message</div>
+ <div ng-message="max">dashboard.max-columns-count-message</div>
</div>
</md-input-container>
- </div>
- <div flex
- ng-required="false"
- md-color-picker
- ng-model="vm.gridSettings.backgroundColor"
- label="{{ 'dashboard.background-color' | translate }}"
- icon="format_color_fill"
- default="rgba(0,0,0,0)"
- md-color-clear-button="false"
- open-on-input="true"
- md-color-generic-palette="false"
- md-color-history="false"
- ></div>
- <div class="tb-container">
- <label class="tb-label" translate>dashboard.background-image</label>
- <div flow-init="{singleFile:true}"
- flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
- <div class="tb-image-preview-container">
- <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
- <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
- </div>
- <div class="tb-image-clear-container">
- <md-button ng-click="vm.clearImage()"
- class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'action.remove' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
- close
- </md-icon>
- </md-button>
- </div>
- <div class="alert tb-flow-drop" flow-drop>
- <label for="select" translate>dashboard.drop-image</label>
- <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+ <small translate>dashboard.widgets-margins</small>
+ <div flex layout="row">
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.horizontal-margin</label>
+ <input ng-required="vm.gridSettings" type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.vertical-margin</label>
+ <input ng-required="vm.gridSettings" type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.vertical-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+ </div>
+ </md-input-container>
+ </div>
+ <div flex
+ ng-required="false"
+ md-color-picker
+ ng-model="vm.gridSettings.backgroundColor"
+ label="{{ 'dashboard.background-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0,0,0,0)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ <div class="tb-container">
+ <label class="tb-label" translate>dashboard.background-image</label>
+ <div flow-init="{singleFile:true}"
+ flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
+ <div class="tb-image-preview-container">
+ <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
+ <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
+ </div>
+ <div class="tb-image-clear-container">
+ <md-button ng-click="vm.clearImage()"
+ class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="select" translate>dashboard.drop-image</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+ </div>
</div>
</div>
+ <md-input-container class="md-block">
+ <label translate>dashboard.background-size-mode</label>
+ <md-select ng-model="vm.gridSettings.backgroundSizeMode" placeholder="{{ 'dashboard.background-size-mode' | translate }}">
+ <md-option value="100%">Fit width</md-option>
+ <md-option value="auto 100%">Fit height</md-option>
+ <md-option value="cover">Cover</md-option>
+ <md-option value="contain">Contain</md-option>
+ <md-option value="auto">Original size</md-option>
+ </md-select>
+ </md-input-container>
</div>
- <md-input-container class="md-block">
- <label translate>dashboard.background-size-mode</label>
- <md-select ng-model="vm.gridSettings.backgroundSizeMode" placeholder="{{ 'dashboard.background-size-mode' | translate }}">
- <md-option value="100%">Fit width</md-option>
- <md-option value="auto 100%">Fit height</md-option>
- <md-option value="cover">Cover</md-option>
- <md-option value="contain">Contain</md-option>
- <md-option value="auto">Original size</md-option>
- </md-select>
- </md-input-container>
</fieldset>
</div>
</md-dialog-content>
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 1256065..7cba4ee 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -34,7 +34,10 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
scope.widget.isSystemType).then(
function(widgetInfo) {
scope.$applyAsync(function(scope) {
- scope.widgetConfig = scope.widget.config;
+ scope.widgetConfig = {
+ config: scope.widget.config,
+ layout: scope.widgetLayout
+ };
var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
scope.isDataEnabled = !widgetInfo.useCustomDatasources;
@@ -58,6 +61,12 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
}
});
+ scope.$watch('widgetLayout', function () {
+ if (scope.widgetLayout && scope.widgetConfig) {
+ scope.widgetConfig.layout = scope.widgetLayout;
+ }
+ });
+
scope.fetchEntityKeys = function (entityAliasId, query, type) {
var entityAlias = scope.aliasesInfo.entityAliases[entityAliasId];
if (entityAlias && entityAlias.entityId) {
@@ -117,6 +126,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
dashboard: '=',
aliasesInfo: '=',
widget: '=',
+ widgetLayout: '=',
theForm: '='
}
};
ui/src/app/dashboard/index.js 10(+7 -3)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index c8ba734..73a97a1 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -16,7 +16,6 @@
import './dashboard.scss';
import uiRouter from 'angular-ui-router';
-import gridster from 'angular-gridster';
import thingsboardGrid from '../components/grid.directive';
import thingsboardApiWidget from '../api/widget.service';
@@ -26,6 +25,7 @@ import thingsboardApiCustomer from '../api/customer.service';
import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
import thingsboardWidgetConfig from '../components/widget-config.directive';
import thingsboardDashboardSelect from '../components/dashboard-select.directive';
+import thingsboardRelatedEntityAutocomplete from '../components/related-entity-autocomplete.directive';
import thingsboardDashboard from '../components/dashboard.directive';
import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
@@ -33,6 +33,8 @@ import thingsboardSocialsharePanel from '../components/socialshare-panel.directi
import thingsboardTypes from '../common/types.constant';
import thingsboardItemBuffer from '../services/item-buffer.service';
import thingsboardImportExport from '../import-export';
+import dashboardLayouts from './layouts';
+import dashboardStates from './states';
import DashboardRoutes from './dashboard.routes';
import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
@@ -46,7 +48,6 @@ import EditWidgetDirective from './edit-widget.directive';
export default angular.module('thingsboard.dashboard', [
uiRouter,
- gridster.name,
thingsboardTypes,
thingsboardItemBuffer,
thingsboardImportExport,
@@ -58,10 +59,13 @@ export default angular.module('thingsboard.dashboard', [
thingsboardDetailsSidenav,
thingsboardWidgetConfig,
thingsboardDashboardSelect,
+ thingsboardRelatedEntityAutocomplete,
thingsboardDashboard,
thingsboardExpandFullscreen,
thingsboardWidgetsBundleSelect,
- thingsboardSocialsharePanel
+ thingsboardSocialsharePanel,
+ dashboardLayouts,
+ dashboardStates
])
.config(DashboardRoutes)
.controller('DashboardsController', DashboardsController)
diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.directive.js b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js
new file mode 100644
index 0000000..18926c3
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js
@@ -0,0 +1,277 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardLayoutTemplate from './dashboard-layout.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardLayout() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ layoutCtx: '=',
+ dashboardCtx: '=',
+ isEdit: '=',
+ isMobile: '=',
+ widgetEditMode: '=',
+ getStDiff: '&?'
+ },
+ controller: DashboardLayoutController,
+ controllerAs: 'vm',
+ templateUrl: dashboardLayoutTemplate
+ };
+}
+
+/*@ngInject*/
+function DashboardLayoutController($scope, $rootScope, $translate, $window, hotkeys, itembuffer) {
+
+ var vm = this;
+
+ vm.noData = noData;
+ vm.addWidget = addWidget;
+ vm.editWidget = editWidget;
+ vm.exportWidget = exportWidget;
+ vm.widgetMouseDown = widgetMouseDown;
+ vm.widgetClicked = widgetClicked;
+ vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
+ vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
+ vm.removeWidget = removeWidget;
+ vm.pasteWidget = pasteWidget;
+ vm.pasteWidgetReference = pasteWidgetReference;
+ vm.dashboardInited = dashboardInited;
+ vm.dashboardInitFailed = dashboardInitFailed;
+
+ vm.reload = function() {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.reload();
+ }
+ };
+
+ vm.setResizing = function(resizing) {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.isResizing = resizing;
+ }
+ }
+
+ vm.resetHighlight = function() {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.resetHighlight();
+ }
+ };
+
+ vm.highlightWidget = function(widget, delay) {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.highlightWidget(widget, delay);
+ }
+ };
+
+ vm.selectWidget = function(widget, delay) {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.selectWidget(widget, delay);
+ }
+ };
+
+ vm.dashboardInitComplete = false;
+
+ initHotKeys();
+
+ $scope.$on('$destroy', function() {
+ vm.dashboardContainer = null;
+ });
+
+ $scope.$watch('vm.layoutCtx', function () {
+ if (vm.layoutCtx) {
+ vm.layoutCtx.ctrl = vm;
+ }
+ });
+
+ function noData() {
+ return vm.dashboardInitComplete && vm.layoutCtx &&
+ vm.layoutCtx.widgets && vm.layoutCtx.widgets.length == 0;
+ }
+
+ function addWidget($event) {
+ if (vm.dashboardCtx.onAddWidget) {
+ vm.dashboardCtx.onAddWidget($event, vm.layoutCtx);
+ }
+ }
+
+ function editWidget($event, widget) {
+ if (vm.dashboardCtx.onEditWidget) {
+ vm.dashboardCtx.onEditWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function exportWidget($event, widget) {
+ if (vm.dashboardCtx.onExportWidget) {
+ vm.dashboardCtx.onExportWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function widgetMouseDown($event, widget) {
+ if (vm.dashboardCtx.onWidgetMouseDown) {
+ vm.dashboardCtx.onWidgetMouseDown($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function widgetClicked($event, widget) {
+ if (vm.dashboardCtx.onWidgetClicked) {
+ vm.dashboardCtx.onWidgetClicked($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function prepareDashboardContextMenu() {
+ if (vm.dashboardCtx.prepareDashboardContextMenu) {
+ return vm.dashboardCtx.prepareDashboardContextMenu(vm.layoutCtx);
+ }
+ }
+
+ function prepareWidgetContextMenu(widget) {
+ if (vm.dashboardCtx.prepareWidgetContextMenu) {
+ return vm.dashboardCtx.prepareWidgetContextMenu(vm.layoutCtx, widget);
+ }
+ }
+
+ function removeWidget($event, widget) {
+ if (vm.dashboardCtx.onRemoveWidget) {
+ vm.dashboardCtx.onRemoveWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function dashboardInitFailed() {
+ var parentScope = $window.parent.angular.element($window.frameElement).scope();
+ parentScope.$emit('widgetEditModeInited');
+ parentScope.$apply();
+ vm.dashboardInitComplete = true;
+ }
+
+ function dashboardInited(dashboardContainer) {
+ vm.dashboardContainer = dashboardContainer;
+ vm.dashboardInitComplete = true;
+ }
+
+ 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+r',
+ description: translations['action.copy-reference'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ var widget = vm.dashboardContainer.getSelectedWidget();
+ if (widget) {
+ event.preventDefault();
+ copyWidgetReference(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+i',
+ description: translations['action.paste-reference'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ if (itembuffer.canPasteWidgetReference(vm.dashboardCtx.dashboard,
+ vm.dashboardCtx.state, vm.layoutCtx.id)) {
+ event.preventDefault();
+ pasteWidgetReference(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();
+ vm.dashboardCtx.onRemoveWidget(event, vm.layoutCtx, widget);
+ }
+ }
+ }
+ });
+ });
+ }
+
+ function copyWidget($event, widget) {
+ if (vm.dashboardCtx.copyWidget) {
+ vm.dashboardCtx.copyWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function copyWidgetReference($event, widget) {
+ if (vm.dashboardCtx.copyWidgetReference) {
+ vm.dashboardCtx.copyWidgetReference($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function pasteWidget($event) {
+ var pos = vm.dashboardContainer.getEventGridPosition($event);
+ if (vm.dashboardCtx.pasteWidget) {
+ vm.dashboardCtx.pasteWidget($event, vm.layoutCtx, pos);
+ }
+ }
+
+ function pasteWidgetReference($event) {
+ var pos = vm.dashboardContainer.getEventGridPosition($event);
+ if (vm.dashboardCtx.pasteWidgetReference) {
+ vm.dashboardCtx.pasteWidgetReference($event, vm.layoutCtx, pos);
+ }
+ }
+
+}
diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
new file mode 100644
index 0000000..ea84858
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
@@ -0,0 +1,69 @@
+<!--
+
+ 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.
+
+-->
+<md-content style="position: relative; width: 100%; height: 100%;"
+ ng-style="{'background-color': vm.layoutCtx.gridSettings.backgroundColor,
+ 'background-image': 'url('+vm.layoutCtx.gridSettings.backgroundImageUrl+')',
+ 'background-repeat': 'no-repeat',
+ 'background-attachment': 'scroll',
+ 'background-size': vm.layoutCtx.gridSettings.backgroundSizeMode || '100%',
+ 'background-position': '0% 0%'}">
+ <section ng-show="!loading && vm.noData()" layout-align="center center"
+ ng-style="{'color': vm.layoutCtx.gridSettings.color}"
+ style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;"
+ class="md-headline tb-absolute-fill">
+ <span translate ng-if="!vm.isEdit">
+ dashboard.no-widgets
+ </span>
+ <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget({event: $event})">
+ <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
+ {{ 'dashboard.add-widget' | translate }}
+ </md-button>
+ </section>
+ <tb-dashboard
+ dashboard-style="{'background-color': vm.layoutCtx.gridSettings.backgroundColor,
+ 'background-image': 'url('+vm.layoutCtx.gridSettings.backgroundImageUrl+')',
+ 'background-repeat': 'no-repeat',
+ 'background-attachment': 'scroll',
+ 'background-size': vm.layoutCtx.gridSettings.backgroundSizeMode || '100%',
+ 'background-position': '0% 0%'}"
+ widgets="vm.layoutCtx.widgets"
+ widget-layouts="vm.layoutCtx.widgetLayouts"
+ columns="vm.layoutCtx.gridSettings.columns"
+ margins="vm.layoutCtx.gridSettings.margins"
+ aliases-info="vm.dashboardCtx.aliasesInfo"
+ state-controller="vm.dashboardCtx.stateController"
+ dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow"
+ is-edit="vm.isEdit"
+ is-mobile="vm.isMobile"
+ is-mobile-disabled="vm.widgetEditMode"
+ is-edit-action-enabled="vm.isEdit"
+ is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
+ is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
+ on-edit-widget="vm.editWidget(event, widget)"
+ on-export-widget="vm.exportWidget(event, widget)"
+ on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
+ on-widget-clicked="vm.widgetClicked(event, widget)"
+ prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
+ prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
+ on-remove-widget="vm.removeWidget(event, widget)"
+ get-st-diff="vm.getStDiff()"
+ on-init="vm.dashboardInited(dashboard)"
+ on-init-failed="vm.dashboardInitFailed(e)"
+ ignore-loading="vm.layoutCtx.ignoreLoading">
+ </tb-dashboard>
+</md-content>
ui/src/app/dashboard/layouts/index.js 25(+25 -0)
diff --git a/ui/src/app/dashboard/layouts/index.js b/ui/src/app/dashboard/layouts/index.js
new file mode 100644
index 0000000..6315565
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/index.js
@@ -0,0 +1,25 @@
+/*
+ * 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 ManageDashboardLayoutsController from './manage-dashboard-layouts.controller';
+import SelectTargetLayoutController from './select-target-layout.controller';
+import DashboardLayoutDirective from './dashboard-layout.directive';
+
+export default angular.module('thingsboard.dashboard.layouts', [])
+ .controller('ManageDashboardLayoutsController', ManageDashboardLayoutsController)
+ .controller('SelectTargetLayoutController', SelectTargetLayoutController)
+ .directive('tbDashboardLayout', DashboardLayoutDirective)
+ .name;
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
new file mode 100644
index 0000000..1ccf48f
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardSettingsTemplate from '../dashboard-settings.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ManageDashboardLayoutsController($scope, $mdDialog, $document, dashboardUtils, layouts) {
+
+ var vm = this;
+
+ vm.openLayoutSettings = openLayoutSettings;
+ vm.cancel = cancel;
+ vm.save = save;
+
+ vm.layouts = layouts;
+ vm.displayLayouts = {
+ main: angular.isDefined(vm.layouts.main),
+ right: angular.isDefined(vm.layouts.right)
+ }
+
+ for (var l in vm.displayLayouts) {
+ if (!vm.layouts[l]) {
+ vm.layouts[l] = dashboardUtils.createDefaultLayoutData();
+ }
+ }
+
+ function openLayoutSettings($event, layoutId) {
+ var gridSettings = angular.copy(vm.layouts[layoutId].gridSettings);
+ $mdDialog.show({
+ controller: 'DashboardSettingsController',
+ controllerAs: 'vm',
+ templateUrl: dashboardSettingsTemplate,
+ locals: {
+ settings: null,
+ gridSettings: gridSettings
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (data) {
+ var gridSettings = data.gridSettings;
+ if (gridSettings) {
+ dashboardUtils.updateLayoutSettings(vm.layouts[layoutId], gridSettings);
+ }
+ $scope.theForm.$setDirty();
+ }, function () {
+ });
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ for (var l in vm.displayLayouts) {
+ if (!vm.displayLayouts[l]) {
+ if (vm.layouts[l]) {
+ delete vm.layouts[l];
+ }
+ }
+ }
+ $mdDialog.hide(vm.layouts);
+ }
+}
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
new file mode 100644
index 0000000..d4d95db
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
@@ -0,0 +1,65 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'layout.manage' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'layout.manage' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div layout="row" layout-align="start center">
+ <md-checkbox ng-disabled="true" flex aria-label="{{ 'layout.main' | translate }}"
+ ng-model="vm.displayLayouts.main">{{ 'layout.main' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'layout.right' | translate }}"
+ ng-model="vm.displayLayouts.right">{{ 'layout.right' | translate }}
+ </md-checkbox>
+ </div>
+ <div layout="row" layout-align="start center">
+ <md-button flex ng-show="vm.displayLayouts.main"
+ class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.openLayoutSettings($event, 'main')">
+ <span translate>layout.main</span>
+ </md-button>
+ <md-button flex ng-show="vm.displayLayouts.right"
+ class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.openLayoutSettings($event, 'right')">
+ <span translate>layout.right</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/layouts/select-target-layout.controller.js b/ui/src/app/dashboard/layouts/select-target-layout.controller.js
new file mode 100644
index 0000000..1af289a
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/select-target-layout.controller.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function SelectTargetLayoutController($scope, $mdDialog) {
+
+ var vm = this;
+
+ vm.cancel = cancel;
+ vm.selectLayout = selectLayout;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function selectLayout($event, layoutId) {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(layoutId);
+ }
+}
diff --git a/ui/src/app/dashboard/layouts/select-target-layout.tpl.html b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html
new file mode 100644
index 0000000..f69f487
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'layout.select' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'layout.select' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div layout="row" layout-align="start center">
+ <md-button flex class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.selectLayout($event, 'main')">
+ <span translate>layout.main</span>
+ </md-button>
+ <md-button flex class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.selectLayout($event, 'right')">
+ <span translate>layout.right</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
new file mode 100644
index 0000000..86eb9ec
--- /dev/null
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function DashboardStateDialogController($scope, $mdDialog, $filter, dashboardUtils, isAdd, allStates, state) {
+
+ var vm = this;
+
+ vm.isAdd = isAdd;
+ vm.allStates = allStates;
+ vm.state = state;
+
+ vm.stateIdTouched = false;
+
+ if (vm.isAdd) {
+ vm.state = dashboardUtils.createDefaultState('', false);
+ vm.state.id = '';
+ vm.prevStateId = '';
+ } else {
+ vm.state = state;
+ vm.prevStateId = vm.state.id;
+ }
+
+ vm.cancel = cancel;
+ vm.save = save;
+
+ $scope.$watch("vm.state.name", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.state.name != null) {
+ checkStateName();
+ }
+ });
+
+ $scope.$watch("vm.state.id", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.state.id != null) {
+ checkStateId();
+ }
+ });
+
+ function checkStateName() {
+ if (!vm.stateIdTouched && vm.isAdd) {
+ vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_");
+ }
+ var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true);
+ if (result && result.length && result[0].id !== vm.prevStateId) {
+ $scope.theForm.name.$setValidity('stateExists', false);
+ } else {
+ $scope.theForm.name.$setValidity('stateExists', true);
+ }
+ }
+
+ function checkStateId() {
+ var result = $filter('filter')(vm.allStates, {id: vm.state.id}, true);
+ if (result && result.length && result[0].id !== vm.prevStateId) {
+ $scope.theForm.stateId.$setValidity('stateExists', false);
+ } else {
+ $scope.theForm.stateId.$setValidity('stateExists', true);
+ }
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ vm.state.id = vm.state.id.trim();
+ $mdDialog.hide(vm.state);
+ }
+}
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
new file mode 100644
index 0000000..fa45d7a
--- /dev/null
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog class="dashboard-state" aria-label="{{'dashboard.state' | translate }}" style="min-width: 600px;">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2>{{ (vm.isAdd ? 'dashboard.add-state' : 'dashboard.edit-state') | translate }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>dashboard.state-name</label>
+ <input name="name" required ng-model="vm.state.name">
+ <div ng-messages="theForm.name.$error">
+ <div ng-message="required" translate>dashboard.state-name-required</div>
+ <div ng-message="stateExists" translate>dashboard.state-name-exists</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>dashboard.state-id</label>
+ <input name="stateId" ng-model="vm.state.id"
+ ng-change="vm.stateIdTouched = true"
+ ng-pattern="/^[a-zA-Z0-9_]*$/">
+ <div ng-messages="theForm.stateId.$error">
+ <div ng-message="required" translate>dashboard.state-id-required</div>
+ <div ng-message="stateExists" translate>dashboard.state-id-exists</div>
+ <div ng-message="pattern" translate>dashboard.invalid-state-id-format</div>
+ </div>
+ </md-input-container>
+ <md-checkbox flex aria-label="{{ 'dashboard.is-root-state' | translate }}"
+ ng-model="vm.state.root">{{ 'dashboard.is-root-state' | translate }}
+ </md-checkbox>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ vm.isAdd ? 'Add' : 'Save' }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
+ Cancel
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js
new file mode 100644
index 0000000..782f59e
--- /dev/null
+++ b/ui/src/app/dashboard/states/default-state-controller.js
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) {
+
+ var vm = this;
+
+ vm.inited = false;
+
+ vm.openState = openState;
+ vm.updateState = updateState;
+ vm.navigatePrevState = navigatePrevState;
+ vm.getStateId = getStateId;
+ vm.getStateParams = getStateParams;
+
+ vm.getStateName = getStateName;
+
+ vm.displayStateSelection = displayStateSelection;
+
+ function openState(id, params) {
+ if (vm.states && vm.states[id]) {
+ if (!params) {
+ params = {};
+ }
+ var newState = {
+ id: id,
+ params: params
+ }
+ //append new state
+ vm.stateObject[0] = newState;
+ gotoState(vm.stateObject[0].id, true);
+ }
+ }
+
+ function updateState(id, params) {
+ if (vm.states && vm.states[id]) {
+ if (!params) {
+ params = {};
+ }
+ var newState = {
+ id: id,
+ params: params
+ }
+ //replace with new state
+ vm.stateObject[0] = newState;
+ gotoState(vm.stateObject[0].id, true);
+ }
+ }
+
+ function navigatePrevState(index) {
+ if (index < vm.stateObject.length-1) {
+ vm.stateObject.splice(index+1, vm.stateObject.length-index-1);
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ }
+
+ function getStateId() {
+ return vm.stateObject[vm.stateObject.length-1].id;
+ }
+
+ function getStateParams() {
+ return vm.stateObject[vm.stateObject.length-1].params;
+ }
+
+ function getStateName(id, state) {
+ var result = '';
+ var translationId = types.translate.dashboardStatePrefix + id;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ result = translation;
+ } else {
+ result = state.name;
+ }
+ return result;
+ }
+
+ function parseState(stateJson) {
+ var result;
+ if (stateJson) {
+ try {
+ result = angular.fromJson(stateJson);
+ } catch (e) {
+ result = [ { id: null, params: {} } ];
+ }
+ }
+ if (!result) {
+ result = [];
+ }
+ if (!result.length) {
+ result[0] = { id: null, params: {} }
+ }
+ if (!result[0].id) {
+ result[0].id = dashboardUtils.getRootStateId(vm.states);
+ }
+ return result;
+ }
+
+ $scope.$watch('vm.states', function() {
+ if (vm.states) {
+ if (!vm.inited) {
+ vm.inited = true;
+ init();
+ }
+ }
+ });
+
+ function displayStateSelection() {
+ return vm.states && Object.keys(vm.states).length > 1;
+ }
+
+ function init() {
+ var initialState = $stateParams.state;
+ vm.stateObject = parseState(initialState);
+
+ gotoState(vm.stateObject[0].id, false);
+
+ $scope.$watchCollection(function(){
+ return $state.params;
+ }, function(){
+ var currentState = $state.params.state;
+ vm.stateObject = parseState(currentState);
+ });
+
+ $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function() {
+ if (vm.stateObject[0].id !== vm.dashboardCtrl.dashboardCtx.state) {
+ stopWatchStateObject();
+ vm.stateObject[0].id = vm.dashboardCtrl.dashboardCtx.state;
+ updateLocation();
+ watchStateObject();
+ }
+ });
+ watchStateObject();
+ }
+
+ function stopWatchStateObject() {
+ if (vm.stateObjectWatcher) {
+ vm.stateObjectWatcher();
+ vm.stateObjectWatcher = null;
+ }
+ }
+
+ function watchStateObject() {
+ vm.stateObjectWatcher = $scope.$watch('vm.stateObject', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && newVal) {
+ gotoState(vm.stateObject[0].id, true);
+ }
+ }, true);
+ }
+
+ function gotoState(stateId, update) {
+ if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
+ vm.dashboardCtrl.openDashboardState(stateId);
+ if (update) {
+ updateLocation();
+ }
+ }
+ }
+
+ function updateLocation() {
+ if (vm.stateObject[0].id) {
+ $location.search({state : angular.toJson(vm.stateObject)});
+ }
+ }
+
+
+
+}
diff --git a/ui/src/app/dashboard/states/default-state-controller.tpl.html b/ui/src/app/dashboard/states/default-state-controller.tpl.html
new file mode 100644
index 0000000..9de1563
--- /dev/null
+++ b/ui/src/app/dashboard/states/default-state-controller.tpl.html
@@ -0,0 +1,22 @@
+<!--
+
+ 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.
+
+-->
+<md-select ng-show="vm.displayStateSelection()" aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.stateObject[0].id">
+ <md-option ng-repeat="(stateId, state) in vm.states" ng-value="stateId">
+ {{vm.getStateName(stateId, state)}}
+ </md-option>
+</md-select>
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
new file mode 100644
index 0000000..3eaf453
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -0,0 +1,243 @@
+/*
+ * 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 './entity-state-controller.scss';
+
+/*@ngInject*/
+export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, types, dashboardUtils, entityService) {
+
+ var vm = this;
+
+ vm.inited = false;
+
+ vm.openState = openState;
+ vm.updateState = updateState;
+ vm.navigatePrevState = navigatePrevState;
+ vm.getStateId = getStateId;
+ vm.getStateParams = getStateParams;
+
+ vm.getStateName = getStateName;
+
+ vm.selectedStateIndex = -1;
+
+ function openState(id, params) {
+ if (vm.states && vm.states[id]) {
+ resolveEntity(params).then(
+ function success(entityName) {
+ params.entityName = entityName;
+ var newState = {
+ id: id,
+ params: params
+ }
+ //append new state
+ vm.stateObject.push(newState);
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ );
+ }
+ }
+
+ function updateState(id, params) {
+ if (vm.states && vm.states[id]) {
+ resolveEntity(params).then(
+ function success(entityName) {
+ params.entityName = entityName;
+ var newState = {
+ id: id,
+ params: params
+ }
+ //replace with new state
+ vm.stateObject[vm.stateObject.length - 1] = newState;
+ gotoState(vm.stateObject[vm.stateObject.length - 1].id, true);
+ }
+ );
+ }
+ }
+
+ function navigatePrevState(index) {
+ if (index < vm.stateObject.length-1) {
+ vm.stateObject.splice(index+1, vm.stateObject.length-index-1);
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ }
+
+ function getStateId() {
+ return vm.stateObject[vm.stateObject.length-1].id;
+ }
+
+ function getStateParams() {
+ return vm.stateObject[vm.stateObject.length-1].params;
+ }
+
+ function getStateName(index) {
+ var result = '';
+ if (vm.stateObject[index]) {
+ var params = vm.stateObject[index].params;
+ if (params && params.entityName) {
+ result = params.entityName;
+ } else {
+ var id = vm.stateObject[index].id;
+ var translationId = types.translate.dashboardStatePrefix + id;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ result = translation;
+ } else {
+ result = vm.states[vm.stateObject[index].id].name;
+ }
+ }
+ }
+ return result;
+ }
+
+ function resolveEntity(params) {
+ var deferred = $q.defer();
+ if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
+ entityService.getEntity(params.entityId.entityType, params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).then(
+ function success(entity) {
+ var entityName = entityService.entityName(params.entityId.entityType, entity);
+ deferred.resolve(entityName);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function parseState(stateJson) {
+ var result;
+ if (stateJson) {
+ try {
+ result = angular.fromJson(stateJson);
+ } catch (e) {
+ result = [ { id: null, params: {} } ];
+ }
+ }
+ if (!result) {
+ result = [];
+ }
+ if (!result.length) {
+ result[0] = { id: null, params: {} }
+ }
+ if (!result[0].id) {
+ result[0].id = dashboardUtils.getRootStateId(vm.states);
+ }
+ return result;
+ }
+
+ $scope.$watch('vm.states', function() {
+ if (vm.states) {
+ if (!vm.inited) {
+ vm.inited = true;
+ init();
+ }
+ }
+ });
+
+ function init() {
+ var initialState = $stateParams.state;
+ vm.stateObject = parseState(initialState);
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, false);
+
+ $scope.$watchCollection(function() {
+ return $state.params;
+ }, function(){
+ var currentState = $state.params.state;
+ vm.stateObject = parseState(currentState);
+ });
+
+ $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function() {
+ if (vm.stateObject[vm.stateObject.length-1].id !== vm.dashboardCtrl.dashboardCtx.state) {
+ stopWatchStateObject();
+ vm.stateObject[vm.stateObject.length-1].id = vm.dashboardCtrl.dashboardCtx.state;
+ updateLocation();
+ watchStateObject();
+ }
+ });
+
+ watchStateObject();
+
+ if (vm.dashboardCtrl.isMobile) {
+ watchSelectedStateIndex();
+ }
+
+ $scope.$watch('vm.dashboardCtrl.isMobile', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ if (vm.dashboardCtrl.isMobile) {
+ watchSelectedStateIndex();
+ } else {
+ stopWatchSelectedStateIndex();
+ }
+ }
+ });
+
+ }
+
+ function stopWatchStateObject() {
+ if (vm.stateObjectWatcher) {
+ vm.stateObjectWatcher();
+ vm.stateObjectWatcher = null;
+ }
+ }
+
+ function watchStateObject() {
+ vm.stateObjectWatcher = $scope.$watch('vm.stateObject', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && newVal) {
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ }, true);
+ }
+
+ function stopWatchSelectedStateIndex() {
+ if (vm.selectedStateIndexWatcher) {
+ vm.selectedStateIndexWatcher();
+ vm.selectedStateIndexWatcher = null;
+ }
+ }
+
+ function watchSelectedStateIndex() {
+ vm.selectedStateIndexWatcher = $scope.$watch('vm.selectedStateIndex', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ navigatePrevState(vm.selectedStateIndex);
+ }
+ });
+ }
+
+ function gotoState(stateId, update) {
+ if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
+ vm.dashboardCtrl.openDashboardState(stateId);
+ if (update) {
+ updateLocation();
+ }
+ }
+ }
+
+ function updateLocation() {
+ if (vm.stateObject[vm.stateObject.length-1].id) {
+ $location.search({state : angular.toJson(vm.stateObject)});
+ }
+ }
+
+
+
+}
diff --git a/ui/src/app/dashboard/states/entity-state-controller.scss b/ui/src/app/dashboard/states/entity-state-controller.scss
new file mode 100644
index 0000000..2d79157
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.scss
@@ -0,0 +1,33 @@
+/**
+ * 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.
+ */
+
+.entity-state-controller {
+ .state-divider {
+ font-size: 28px;
+ padding-left: 15px;
+ padding-right: 15px;
+ }
+ .state-entry {
+ font-size: 22px;
+ outline: none;
+ }
+ md-select {
+ .md-text {
+ font-size: 22px;
+ font-weight: bold;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/states/entity-state-controller.tpl.html b/ui/src/app/dashboard/states/entity-state-controller.tpl.html
new file mode 100644
index 0000000..4139be6
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+ 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.
+
+-->
+<div class="entity-state-controller">
+ <div ng-if="!vm.dashboardCtrl.isMobile || vm.stateObject.length===1" layout="row" layout-align="start center">
+ <div layout="row" layout-align="start center" ng-repeat="state in vm.stateObject track by $index">
+ <span class='state-divider' ng-if="$index"> > </span>
+ <span class='state-entry' ng-style="{fontWeight: $last ? 'bold' : 'normal',
+ cursor: $last ? 'default' : 'pointer'}" ng-click="vm.navigatePrevState($index)">
+ {{vm.getStateName($index)}}
+ </span>
+ </div>
+ </div>
+ <md-select ng-if="vm.dashboardCtrl.isMobile && vm.stateObject.length > 1" aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.selectedStateIndex">
+ <md-option ng-repeat="state in vm.stateObject track by $index" ng-value="$index">
+ {{vm.getStateName($index)}}
+ </md-option>
+ </md-select>
+</div>
\ No newline at end of file
ui/src/app/dashboard/states/index.js 29(+29 -0)
diff --git a/ui/src/app/dashboard/states/index.js b/ui/src/app/dashboard/states/index.js
new file mode 100644
index 0000000..ce59e38
--- /dev/null
+++ b/ui/src/app/dashboard/states/index.js
@@ -0,0 +1,29 @@
+/*
+ * 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 ManageDashboardStatesController from './manage-dashboard-states.controller';
+import DashboardStateDialogController from './dashboard-state-dialog.controller';
+import SelectTargetStateController from './select-target-state.controller';
+import StatesComponentDirective from './states-component.directive';
+import StatesControllerService from './states-controller.service';
+
+export default angular.module('thingsboard.dashboard.states', [])
+ .controller('ManageDashboardStatesController', ManageDashboardStatesController)
+ .controller('DashboardStateDialogController', DashboardStateDialogController)
+ .controller('SelectTargetStateController', SelectTargetStateController)
+ .directive('tbStatesComponent', StatesComponentDirective)
+ .factory('statesControllerService', StatesControllerService)
+ .name;
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
new file mode 100644
index 0000000..452e94f
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
@@ -0,0 +1,198 @@
+/*
+ * 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 './manage-dashboard-states.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardStateDialogTemplate from './dashboard-state-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ManageDashboardStatesController($scope, $mdDialog, $filter, $document, $translate, states) {
+
+ var vm = this;
+
+ vm.allStates = [];
+ for (var id in states) {
+ var state = states[id];
+ state.id = id;
+ vm.allStates.push(state);
+ }
+
+ vm.states = [];
+ vm.statesCount = 0;
+
+ vm.query = {
+ order: 'name',
+ limit: 5,
+ page: 1,
+ search: null
+ };
+
+ vm.enterFilterMode = enterFilterMode;
+ vm.exitFilterMode = exitFilterMode;
+ vm.onReorder = onReorder;
+ vm.onPaginate = onPaginate;
+ vm.addState = addState;
+ vm.editState = editState;
+ vm.deleteState = deleteState;
+
+ vm.cancel = cancel;
+ vm.save = save;
+
+ $scope.$watch("vm.query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+ updateStates();
+ }
+ });
+
+ updateStates ();
+
+ function updateStates () {
+ var result = $filter('orderBy')(vm.allStates, vm.query.order);
+ if (vm.query.search != null) {
+ result = $filter('filter')(result, {$: vm.query.search});
+ }
+ vm.statesCount = result.length;
+ var startIndex = vm.query.limit * (vm.query.page - 1);
+ vm.states = result.slice(startIndex, startIndex + vm.query.limit);
+ }
+
+ function enterFilterMode () {
+ vm.query.search = '';
+ }
+
+ function exitFilterMode () {
+ vm.query.search = null;
+ updateStates();
+ }
+
+ function onReorder () {
+ updateStates();
+ }
+
+ function onPaginate () {
+ updateStates();
+ }
+
+ function addState ($event) {
+ openStateDialog($event, null, true);
+ }
+
+ function editState ($event, alertRule) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ openStateDialog($event, alertRule, false);
+ }
+
+ function openStateDialog($event, state, isAdd) {
+ var prevStateId = null;
+ if (!isAdd) {
+ prevStateId = state.id;
+ }
+ $mdDialog.show({
+ controller: 'DashboardStateDialogController',
+ controllerAs: 'vm',
+ templateUrl: dashboardStateDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {isAdd: isAdd, allStates: vm.allStates, state: angular.copy(state)},
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (state) {
+ saveState(state, prevStateId);
+ updateStates();
+ });
+ }
+
+ function getStateIndex(id) {
+ var result = $filter('filter')(vm.allStates, {id: id});
+ if (result && result.length) {
+ return vm.allStates.indexOf(result[0]);
+ }
+ return -1;
+ }
+
+ function saveState(state, prevStateId) {
+ if (prevStateId) {
+ var index = getStateIndex(prevStateId);
+ if (index > -1) {
+ vm.allStates[index] = state;
+ }
+ } else {
+ vm.allStates.push(state);
+ }
+ if (state.root) {
+ for (var i=0; i < vm.allStates.length; i++) {
+ var otherState = vm.allStates[i];
+ if (otherState.id !== state.id) {
+ otherState.root = false;
+ }
+ }
+ }
+ $scope.theForm.$setDirty();
+ }
+
+ function deleteState ($event, state) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (state) {
+ var title = $translate.instant('dashboard.delete-state-title');
+ var content = $translate.instant('dashboard.delete-state-text', {stateName: state.name});
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(title)
+ .htmlContent(content)
+ .ariaLabel(title)
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+
+ confirm._options.skipHide = true;
+ confirm._options.fullscreen = true;
+
+ $mdDialog.show(confirm).then(function () {
+ var index = getStateIndex(state.id);
+ if (index > -1) {
+ vm.allStates.splice(index, 1);
+ }
+ $scope.theForm.$setDirty();
+ updateStates();
+ });
+
+
+ }
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ var savedStates = {};
+ for (var i=0;i<vm.allStates.length;i++) {
+ var state = vm.allStates[i];
+ var id = state.id;
+ delete state.id;
+ savedStates[id] = state;
+ }
+ $mdDialog.hide(savedStates);
+ }
+}
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.scss b/ui/src/app/dashboard/states/manage-dashboard-states.scss
new file mode 100644
index 0000000..53e6724
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.scss
@@ -0,0 +1,34 @@
+/**
+ * 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.
+ */
+
+.manage-dashboard-states {
+ table.md-table {
+ tbody {
+ tr {
+ td {
+ &.tb-action-cell {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ min-width: 100px;
+ max-width: 100px;
+ width: 100px;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
new file mode 100644
index 0000000..151c05a
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
@@ -0,0 +1,127 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.manage-states' | translate }}" style="min-width: 600px;">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'dashboard.manage-states' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div class="manage-dashboard-states" layout="column">
+ <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search === null">
+ <div class="md-toolbar-tools">
+ <span translate>dashboard.states</span>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.addState($event)">
+ <md-icon>add</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.add-state' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+ <md-icon>search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.search-states' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-input-container md-theme="tb-search-input" flex>
+ <label> </label>
+ <input ng-model="vm.query.search" placeholder="{{ 'dashboard.search-states' | translate }}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-table-container>
+ <table md-table>
+ <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+ <tr md-row>
+ <th md-column md-order-by="name"><span translate>dashboard.state-name</span></th>
+ <th md-column md-order-by="id"><span translate>dashboard.state-id</span></th>
+ <th md-column md-order-by="root"><span translate>dashboard.is-root-state</span></th>
+ <th md-column><span> </span></th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr md-row md-select="state" ng-disabled="state.root" md-select-id="id" md-auto-select ng-repeat="state in vm.states">
+ <td md-cell>{{state.name}}</td>
+ <td md-cell>{{state.id}}</td>
+ <td md-cell>
+ <md-checkbox aria-label="{{'dashboard.is-root-state' | translate }}"
+ disabled ng-model="state.root">
+ </md-checkbox>
+ </td>
+ <td md-cell class="tb-action-cell">
+ <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
+ ng-click="vm.editState($event, state)">
+ <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.edit-state' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-show="!state.root" class="md-icon-button" aria-label="Delete" ng-click="vm.deleteState($event, state)">
+ <md-icon aria-label="Delete" class="material-icons">delete</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.delete-state' | translate }}
+ </md-tooltip>
+ </md-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </md-table-container>
+ <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
+ md-page="vm.query.page" md-total="{{vm.statesCount}}"
+ md-on-paginate="vm.onPaginate" md-page-select>
+ </md-table-pagination>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/select-target-state.controller.js b/ui/src/app/dashboard/states/select-target-state.controller.js
new file mode 100644
index 0000000..fa62eef
--- /dev/null
+++ b/ui/src/app/dashboard/states/select-target-state.controller.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function SelectTargetStateController($scope, $mdDialog, dashboardUtils, states) {
+
+ var vm = this;
+ vm.states = states;
+ vm.stateId = dashboardUtils.getRootStateId(vm.states);
+
+ vm.cancel = cancel;
+ vm.save = save;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.stateId);
+ }
+}
diff --git a/ui/src/app/dashboard/states/select-target-state.tpl.html b/ui/src/app/dashboard/states/select-target-state.tpl.html
new file mode 100644
index 0000000..c874a07
--- /dev/null
+++ b/ui/src/app/dashboard/states/select-target-state.tpl.html
@@ -0,0 +1,50 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.select-state' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'dashboard.select-state' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <md-select required aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.stateId">
+ <md-option ng-repeat="(stateId, state) in vm.states" ng-value="stateId">
+ {{state.name}}
+ </md-option>
+ </md-select>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/states-component.directive.js b/ui/src/app/dashboard/states/states-component.directive.js
new file mode 100644
index 0000000..fb5e77c
--- /dev/null
+++ b/ui/src/app/dashboard/states/states-component.directive.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function StatesComponent($compile, $templateCache, $controller, statesControllerService) {
+
+ var linker = function (scope, element) {
+
+ function destroyStateController() {
+ if (scope.statesController && angular.isFunction(scope.statesController.$onDestroy)) {
+ scope.statesController.$onDestroy();
+ }
+ }
+
+ function init() {
+
+ var stateController = scope.dashboardCtrl.dashboardCtx.stateController;
+
+ stateController.openState = function(id, params) {
+ if (scope.statesController) {
+ scope.statesController.openState(id, params);
+ }
+ }
+
+ stateController.updateState = function(id, params) {
+ if (scope.statesController) {
+ scope.statesController.updateState(id, params);
+ }
+ }
+
+ stateController.navigatePrevState = function(index) {
+ if (scope.statesController) {
+ scope.statesController.navigatePrevState(index);
+ }
+ }
+
+ stateController.getStateId = function() {
+ if (scope.statesController) {
+ return scope.statesController.getStateId();
+ } else {
+ return '';
+ }
+ }
+
+ stateController.getStateParams = function() {
+ if (scope.statesController) {
+ return scope.statesController.getStateParams();
+ } else {
+ return {};
+ }
+ }
+ }
+
+ scope.$on('$destroy', function callOnDestroyHook() {
+ destroyStateController();
+ });
+
+ scope.$watch('scope.dashboardCtrl', function() {
+ if (scope.dashboardCtrl.dashboardCtx) {
+ init();
+ }
+ })
+
+ scope.$watch('statesControllerId', function(newValue) {
+ if (newValue) {
+ if (scope.statesController) {
+ destroyStateController();
+ }
+ var statesControllerInfo = statesControllerService.getStateController(scope.statesControllerId);
+ if (!statesControllerInfo) {
+ //fallback to default
+ statesControllerInfo = statesControllerService.getStateController('default');
+ }
+ var template = $templateCache.get(statesControllerInfo.templateUrl);
+ element.html(template);
+ var locals = {};
+ angular.extend(locals, {$scope: scope, $element: element});
+ var controller = $controller(statesControllerInfo.controller, locals, true, 'vm');
+ controller.instance = controller();
+ scope.statesController = controller.instance;
+ scope.statesController.dashboardCtrl = scope.dashboardCtrl;
+ scope.statesController.states = scope.states;
+ $compile(element.contents())(scope);
+ }
+ });
+
+ scope.$watch('states', function() {
+ if (scope.statesController) {
+ scope.statesController.states = scope.states;
+ }
+ });
+
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ statesControllerId: '=',
+ dashboardCtrl: '=',
+ states: '='
+ }
+ };
+}
diff --git a/ui/src/app/dashboard/states/states-controller.service.js b/ui/src/app/dashboard/states/states-controller.service.js
new file mode 100644
index 0000000..e4c1f29
--- /dev/null
+++ b/ui/src/app/dashboard/states/states-controller.service.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import defaultStateControllerTemplate from './default-state-controller.tpl.html';
+import entityStateControllerTemplate from './entity-state-controller.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import DefaultStateController from './default-state-controller';
+import EntityStateController from './entity-state-controller';
+
+/*@ngInject*/
+export default function StatesControllerService() {
+
+ var statesControllers = {};
+ statesControllers['default'] = {
+ controller: DefaultStateController,
+ templateUrl: defaultStateControllerTemplate
+ };
+ statesControllers['entity'] = {
+ controller: EntityStateController,
+ templateUrl: entityStateControllerTemplate
+ };
+
+ var service = {
+ registerStatesController: registerStatesController,
+ getStateControllers: getStateControllers,
+ getStateController: getStateController
+ };
+
+ return service;
+
+ function registerStatesController(id, stateControllerInfo) {
+ statesControllers[id] = stateControllerInfo;
+ }
+
+ function getStateControllers() {
+ return statesControllers;
+ }
+
+ function getStateController(id) {
+ return statesControllers[id];
+ }
+
+}
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
index 0e07cb4..e5822a4 100644
--- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -13,8 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import selectTargetStateTemplate from '../../dashboard/states/select-target-state.tpl.html';
+import selectTargetLayoutTemplate from '../../dashboard/layouts/select-target-layout.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
/*@ngInject*/
-export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
var vm = this;
@@ -31,22 +39,87 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
$mdDialog.cancel();
}
- function add() {
- $scope.theForm.$setPristine();
+ function selectTargetState($event, dashboard) {
+ var deferred = $q.defer();
+ var states = dashboard.configuration.states;
+ var stateIds = Object.keys(states);
+ if (stateIds.length > 1) {
+ $mdDialog.show({
+ controller: 'SelectTargetStateController',
+ controllerAs: 'vm',
+ templateUrl: selectTargetStateTemplate,
+ parent: angular.element($document[0].body),
+ locals: {
+ states: states
+ },
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: $event
+ }).then(
+ function success(stateId) {
+ deferred.resolve(stateId);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+
+ } else {
+ deferred.resolve(stateIds[0]);
+ }
+ return deferred.promise;
+ }
+
+ function selectTargetLayout($event, dashboard, targetState) {
+ var deferred = $q.defer();
+ var layouts = dashboard.configuration.states[targetState].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 add($event) {
if (vm.addToDashboardType === 0) {
dashboardService.getDashboard(vm.dashboardId).then(
function success(dashboard) {
- addWidgetToDashboard(dashboard);
+ selectTargetState($event, dashboard).then(
+ function(targetState) {
+ selectTargetLayout($event, dashboard, targetState).then(
+ function(targetLayout) {
+ addWidgetToDashboard(dashboard, targetState, targetLayout);
+ }
+ );
+ }
+ );
},
function fail() {}
);
} else {
- addWidgetToDashboard(vm.newDashboard);
+ addWidgetToDashboard(vm.newDashboard, 'default', 'main');
}
}
- function addWidgetToDashboard(theDashboard) {
+ function addWidgetToDashboard(theDashboard, targetState, targetLayout) {
var aliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
@@ -60,13 +133,25 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
entityList: [entityId]
}
};
- theDashboard = itembuffer.addWidgetToDashboard(theDashboard, vm.widget, aliasesInfo, null, 48, -1, -1);
- dashboardService.saveDashboard(theDashboard).then(
- function success(dashboard) {
- $mdDialog.hide();
- if (vm.openDashboard) {
- $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id});
- }
+ itembuffer.addWidgetToDashboard(theDashboard, targetState, targetLayout, vm.widget, aliasesInfo, null, 48, null, -1, -1).then(
+ function(theDashboard) {
+ dashboardService.saveDashboard(theDashboard).then(
+ function success(dashboard) {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide();
+ if (vm.openDashboard) {
+ var stateParams = {
+ dashboardId: dashboard.id.id
+ }
+ var stateIds = Object.keys(dashboard.configuration.states);
+ var stateIndex = stateIds.indexOf(targetState);
+ if (stateIndex > 0) {
+ stateParams.state = angular.toJson([ {id: targetState, params: {}} ]);
+ }
+ $state.go('home.dashboards.dashboard', stateParams);
+ }
+ }
+ );
}
);
}
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
index eb16077..d7fe890 100644
--- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
@@ -16,7 +16,7 @@
-->
<md-dialog aria-label="{{ 'attribute.add-widget-to-dashboard' | translate }}" style="min-width: 400px;">
- <form name="theForm" ng-submit="vm.add()">
+ <form name="theForm" ng-submit="vm.add($event)">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>attribute.add-widget-to-dashboard</h2>
diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js
index 86cd676..57bbcd3 100644
--- a/ui/src/app/global-interceptor.service.js
+++ b/ui/src/app/global-interceptor.service.js
@@ -174,7 +174,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
}
}
- if (unhandled) {
+ if (unhandled && !ignoreErrors) {
if (rejection.data && !rejection.data.message) {
getToast().showError(rejection.data);
} else if (rejection.data && rejection.data.message) {
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 4b89a99..af04687 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -332,8 +332,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
// Widget functions
- function exportWidget(dashboard, widget) {
- var widgetItem = itembuffer.prepareWidgetItem(dashboard, widget);
+ function exportWidget(dashboard, sourceState, sourceLayout, widget) {
+ var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
var name = widgetItem.widget.config.title;
name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(widgetItem), name + '.json');
@@ -355,6 +355,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
}
}
}
+ return aliasesInfo;
}
function prepareEntityAlias(aliasInfo) {
@@ -379,21 +380,24 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
entityType = aliasInfo.entityType;
}
return {
- alias: aliasInfo.aliasName,
+ aliasName: aliasInfo.aliasName,
entityType: entityType,
entityFilter: entityFilter
};
}
- function importWidget($event, dashboard, onAliasesUpdate) {
+ function importWidget($event, dashboard, targetState, targetLayoutFunction, onAliasesUpdateFunction) {
+ var deferred = $q.defer();
openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
function success(widgetItem) {
if (!validateImportedWidget(widgetItem)) {
toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
+ deferred.reject();
} else {
var widget = widgetItem.widget;
var aliasesInfo = prepareAliasesInfo(widgetItem.aliasesInfo);
var originalColumns = widgetItem.originalColumns;
+ var originalSize = widgetItem.originalSize;
var datasourceAliases = aliasesInfo.datasourceAliases;
var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
@@ -439,25 +443,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
targetDeviceAliases[datasourceIndex].entityFilter = entityAlias.entityFilter;
}
}
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
},
- function fail() {}
+ function fail() {
+ deferred.reject();
+ }
);
} else {
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
}
}
);
} else {
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
}
} else {
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
}
}
},
- function fail() {}
+ function fail() {
+ deferred.reject();
+ }
);
+ return deferred.promise;
}
function validateImportedWidget(widgetItem) {
@@ -476,8 +489,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
- function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
- itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
+ function addImportedWidget(dashboard, targetState, targetLayoutFunction, event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred) {
+ targetLayoutFunction(event).then(
+ function success(targetLayout) {
+ itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then(
+ function() {
+ deferred.resolve(
+ {
+ widget: widget,
+ layoutId: targetLayout
+ }
+ );
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
}
// Dashboard functions
ui/src/app/locale/locale.constant.js 39(+38 -1)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 1fc45f7..7f52eac 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -14,7 +14,10 @@
* limitations under the License.
*/
+import ThingsboardMissingTranslateHandler from './translate-handler';
+
export default angular.module('thingsboard.locale', [])
+ .factory('tbMissingTranslationHandler', ThingsboardMissingTranslateHandler)
.constant('locales',
{
'en_US': {
@@ -62,6 +65,8 @@ export default angular.module('thingsboard.locale', [])
"undo": "Undo",
"copy": "Copy",
"paste": "Paste",
+ "copy-reference": "Copy reference",
+ "paste-reference": "Paste reference",
"import": "Import",
"export": "Export",
"share-via": "Share via {{provider}}"
@@ -324,6 +329,7 @@ export default angular.module('thingsboard.locale', [])
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
"display-title": "Display dashboard title",
"title-color": "Title color",
+ "display-dashboards-selection": "Display dashboards selection",
"display-entities-selection": "Display entities selection",
"display-dashboard-timewindow": "Display timewindow",
"display-dashboard-export": "Display export",
@@ -350,7 +356,29 @@ export default angular.module('thingsboard.locale', [])
"public": "Public",
"public-link": "Public link",
"copy-public-link": "Copy public link",
- "public-link-copied-message": "Dashboard public link has been copied to clipboard"
+ "public-link-copied-message": "Dashboard public link has been copied to clipboard",
+ "manage-states": "Manage dashboard states",
+ "states": "Dashboard states",
+ "search-states": "Search dashboard states",
+ "selected-states": "{ count, select, 1 {1 dashboard state} other {# dashboard states} } selected",
+ "edit-state": "Edit dashboard state",
+ "delete-state": "Delete dashboard state",
+ "add-state": "Add dashboard state",
+ "state": "Dashboard state",
+ "state-name": "Name",
+ "state-name-required": "Dashboard state name is required.",
+ "state-name-exists": "Dashboard state with the same name is already exists.",
+ "state-id": "State Id",
+ "state-id-required": "Dashboard state id is required.",
+ "state-id-exists": "Dashboard state with the same id is already exists.",
+ "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.",
+ "is-root-state": "Root state",
+ "delete-state-title": "Delete dashboard state",
+ "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?",
+ "show-details": "Show details",
+ "hide-details": "Hide details",
+ "select-state": "Select target state",
+ "state-controller": "State controller"
},
"datakey": {
"settings": "Settings",
@@ -569,6 +597,15 @@ export default angular.module('thingsboard.locale', [])
"no-return-error": "Function must return value!",
"return-type-mismatch": "Function must return value of '{{type}}' type!"
},
+ "layout": {
+ "layout": "Layout",
+ "manage": "Manage layouts",
+ "settings": "Layout settings",
+ "color": "Color",
+ "main": "Main",
+ "right": "Right",
+ "select": "Select target layout"
+ },
"legend": {
"position": "Legend position",
"show-max": "Show max value",
ui/src/app/locale/translate-handler.js 26(+26 -0)
diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js
new file mode 100644
index 0000000..d227041
--- /dev/null
+++ b/ui/src/app/locale/translate-handler.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function ThingsboardMissingTranslateHandler($log, types) {
+
+ return function (translationId) {
+ if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) {
+ $log.warn('Translation for ' + translationId + ' doesn\'t exist');
+ }
+ };
+
+}
\ No newline at end of file
ui/src/app/services/item-buffer.service.js 186(+138 -48)
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 4b54b4d..afb63bf 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,15 +24,19 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name;
/*@ngInject*/
-function ItemBuffer(bufferStore, types, dashboardUtils) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
const WIDGET_ITEM = "widget_item";
+ const WIDGET_REFERENCE = "widget_reference";
var service = {
prepareWidgetItem: prepareWidgetItem,
copyWidget: copyWidget,
+ copyWidgetReference: copyWidgetReference,
hasWidget: hasWidget,
+ canPasteWidgetReference: canPasteWidgetReference,
pasteWidget: pasteWidget,
+ pasteWidgetReference: pasteWidgetReference,
addWidgetToDashboard: addWidgetToDashboard
}
@@ -66,16 +70,42 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
};
}
- function prepareWidgetItem(dashboard, widget) {
+ function getOriginalColumns(dashboard, sourceState, sourceLayout) {
+ var originalColumns = 24;
+ var gridSettings = null;
+ var state = dashboard.configuration.states[sourceState];
+ var layoutCount = Object.keys(state.layouts).length;
+ if (state) {
+ var layout = state.layouts[sourceLayout];
+ if (layout) {
+ gridSettings = layout.gridSettings;
+
+ }
+ }
+ if (gridSettings &&
+ gridSettings.columns) {
+ originalColumns = gridSettings.columns;
+ }
+ originalColumns = originalColumns * layoutCount;
+ return originalColumns;
+ }
+
+ function getOriginalSize(dashboard, sourceState, sourceLayout, widget) {
+ var layout = dashboard.configuration.states[sourceState].layouts[sourceLayout];
+ var widgetLayout = layout.widgets[widget.id];
+ return {
+ sizeX: widgetLayout.sizeX,
+ sizeY: widgetLayout.sizeY
+ }
+ }
+
+ function prepareWidgetItem(dashboard, sourceState, sourceLayout, widget) {
var aliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
};
- var originalColumns = 24;
- if (dashboard.configuration.gridSettings &&
- dashboard.configuration.gridSettings.columns) {
- originalColumns = dashboard.configuration.gridSettings.columns;
- }
+ var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout);
+ var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget);
if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) {
var entityAlias;
@@ -105,37 +135,113 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
return {
widget: widget,
aliasesInfo: aliasesInfo,
+ originalSize: originalSize,
originalColumns: originalColumns
- }
+ };
}
- function copyWidget(dashboard, widget) {
- var widgetItem = prepareWidgetItem(dashboard, widget);
+ function prepareWidgetReference(dashboard, sourceState, sourceLayout, widget) {
+ var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout);
+ var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget);
+
+ return {
+ dashboardId: dashboard.id.id,
+ sourceState: sourceState,
+ sourceLayout: sourceLayout,
+ widgetId: widget.id,
+ originalSize: originalSize,
+ originalColumns: originalColumns
+ };
+ }
+
+ function copyWidget(dashboard, sourceState, sourceLayout, widget) {
+ var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
}
+ function copyWidgetReference(dashboard, sourceState, sourceLayout, widget) {
+ var widgetReference = prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
+ bufferStore.set(WIDGET_REFERENCE, angular.toJson(widgetReference));
+ }
+
function hasWidget() {
return bufferStore.get(WIDGET_ITEM);
}
- function pasteWidget(targetDashboard, position, onAliasesUpdate) {
+ function canPasteWidgetReference(dashboard, state, layout) {
+ var widgetReferenceJson = bufferStore.get(WIDGET_REFERENCE);
+ if (widgetReferenceJson) {
+ var widgetReference = angular.fromJson(widgetReferenceJson);
+ if (widgetReference.dashboardId === dashboard.id.id) {
+ if ((widgetReference.sourceState != state || widgetReference.sourceLayout != layout)
+ && dashboard.configuration.widgets[widgetReference.widgetId]) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ function pasteWidgetReference(targetDashboard, targetState, targetLayout, position) {
+ var deferred = $q.defer();
+ var widgetReferenceJson = bufferStore.get(WIDGET_REFERENCE);
+ if (widgetReferenceJson) {
+ var widgetReference = angular.fromJson(widgetReferenceJson);
+ var widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
+ if (widget) {
+ var originalColumns = widgetReference.originalColumns;
+ var originalSize = widgetReference.originalSize;
+ var targetRow = -1;
+ var targetColumn = -1;
+ if (position) {
+ targetRow = position.row;
+ targetColumn = position.column;
+ }
+ addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, null,
+ null, originalColumns, originalSize, targetRow, targetColumn).then(
+ function () {
+ deferred.resolve(widget);
+ }
+ );
+ } else {
+ deferred.reject();
+ }
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function pasteWidget(targetDashboard, targetState, targetLayout, position, onAliasesUpdateFunction) {
+ var deferred = $q.defer();
var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson);
var widget = widgetItem.widget;
var aliasesInfo = widgetItem.aliasesInfo;
var originalColumns = widgetItem.originalColumns;
+ var originalSize = widgetItem.originalSize;
var targetRow = -1;
var targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
- addWidgetToDashboard(targetDashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, targetRow, targetColumn);
+ widget.id = utils.guid();
+ addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, aliasesInfo,
+ onAliasesUpdateFunction, originalColumns, originalSize, targetRow, targetColumn).then(
+ function () {
+ deferred.resolve(widget);
+ }
+ );
+ } else {
+ deferred.reject();
}
+ return deferred.promise;
}
- function addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, row, column) {
+ function addWidgetToDashboard(dashboard, targetState, targetLayout, widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, row, column) {
+ var deferred = $q.defer();
var theDashboard;
if (dashboard) {
theDashboard = dashboard;
@@ -145,42 +251,28 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
theDashboard = dashboardUtils.validateAndUpdateDashboard(theDashboard);
- var newEntityAliases = updateAliases(theDashboard, widget, aliasesInfo);
-
- var targetColumns = 24;
- if (theDashboard.configuration.gridSettings &&
- theDashboard.configuration.gridSettings.columns) {
- targetColumns = theDashboard.configuration.gridSettings.columns;
- }
- if (targetColumns != originalColumns) {
- var ratio = targetColumns / originalColumns;
- widget.sizeX *= ratio;
- widget.sizeY *= ratio;
+ var callAliasUpdateFunction = false;
+ if (aliasesInfo) {
+ var newEntityAliases = updateAliases(theDashboard, widget, aliasesInfo);
+ var aliasesUpdated = !angular.equals(newEntityAliases, theDashboard.configuration.entityAliases);
+ if (aliasesUpdated) {
+ theDashboard.configuration.entityAliases = newEntityAliases;
+ if (onAliasesUpdateFunction) {
+ callAliasUpdateFunction = true;
+ }
+ }
}
- if (row > -1 && column > - 1) {
- widget.row = row;
- widget.col = column;
+ dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column);
+ if (callAliasUpdateFunction) {
+ onAliasesUpdateFunction().then(
+ function() {
+ deferred.resolve(theDashboard);
+ }
+ );
} else {
- row = 0;
- for (var w in theDashboard.configuration.widgets) {
- var existingWidget = theDashboard.configuration.widgets[w];
- var wRow = existingWidget.row ? existingWidget.row : 0;
- var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
- var bottom = wRow + wSizeY;
- row = Math.max(row, bottom);
- }
- widget.row = row;
- widget.col = 0;
- }
- var aliasesUpdated = !angular.equals(newEntityAliases, theDashboard.configuration.entityAliases);
- if (aliasesUpdated) {
- theDashboard.configuration.entityAliases = newEntityAliases;
- if (onAliasesUpdate) {
- onAliasesUpdate();
- }
+ deferred.resolve(theDashboard);
}
- theDashboard.configuration.widgets.push(widget);
- return theDashboard;
+ return deferred.promise;
}
function updateAliases(dashboard, widget, aliasesInfo) {
@@ -242,6 +334,4 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
}
return newAlias;
}
-
-
}
\ No newline at end of file
ui/src/app/user/user.controller.js 12(+9 -3)
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
index eeba2f4..4e702bd 100644
--- a/ui/src/app/user/user.controller.js
+++ b/ui/src/app/user/user.controller.js
@@ -22,7 +22,7 @@ import userCard from './user-card.tpl.html';
/*@ngInject*/
-export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate) {
+export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate, types) {
var tenantId = $stateParams.tenantId;
var customerId = $stateParams.customerId;
@@ -87,7 +87,10 @@ export default function UserController(userService, toast, $scope, $controller,
};
saveUserFunction = function (user) {
user.authority = "TENANT_ADMIN";
- user.tenantId = {id: tenantId};
+ user.tenantId = {
+ entityType: types.entityType.tenant,
+ id: tenantId
+ };
return userService.saveUser(user);
};
refreshUsersParamsFunction = function () {
@@ -100,7 +103,10 @@ export default function UserController(userService, toast, $scope, $controller,
};
saveUserFunction = function (user) {
user.authority = "CUSTOMER_USER";
- user.customerId = {id: customerId};
+ user.customerId = {
+ entityType: types.entityType.customer,
+ id: customerId
+ };
return userService.saveUser(user);
};
refreshUsersParamsFunction = function () {
ui/src/app/user/user.directive.js 4(+4 -0)
diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js
index 1a4b069..bd78dc2 100644
--- a/ui/src/app/user/user.directive.js
+++ b/ui/src/app/user/user.directive.js
@@ -28,6 +28,10 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi
var template = $templateCache.get(userFieldsetTemplate);
element.html(template);
+ scope.isTenantAdmin = function() {
+ return scope.user && scope.user.authority === 'TENANT_ADMIN';
+ }
+
scope.isCustomerUser = function() {
return scope.user && scope.user.authority === 'CUSTOMER_USER';
}
ui/src/app/user/user-fieldset.tpl.html 13(+11 -2)
diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html
index 7c9b0d6..a2559ff 100644
--- a/ui/src/app/user/user-fieldset.tpl.html
+++ b/ui/src/app/user/user-fieldset.tpl.html
@@ -43,10 +43,19 @@
<label translate>user.description</label>
<textarea ng-model="user.additionalInfo.description" rows="2"></textarea>
</md-input-container>
- <section class="tb-default-dashboard" flex layout="column" ng-show="isCustomerUser()">
+ <section class="tb-default-dashboard" flex layout="column">
<span class="tb-default-dashboard-label" ng-class="{'tb-disabled-label': loading || !isEdit}" translate>user.default-dashboard</span>
<section flex layout="column" layout-gt-sm="row">
- <tb-dashboard-autocomplete flex
+ <tb-dashboard-autocomplete ng-if="isTenantAdmin()"
+ flex
+ ng-disabled="loading || !isEdit"
+ the-form="theForm"
+ ng-model="user.additionalInfo.defaultDashboardId"
+ tenant-id="user.tenantId.id"
+ select-first-dashboard="false">
+ </tb-dashboard-autocomplete>
+ <tb-dashboard-autocomplete ng-if="isCustomerUser()"
+ flex
ng-disabled="loading || !isEdit"
the-form="theForm"
ng-model="user.additionalInfo.defaultDashboardId"
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
index 69fb85b..bda8835 100644
--- a/ui/src/app/widget/widget-library.controller.js
+++ b/ui/src/app/widget/widget-library.controller.js
@@ -87,7 +87,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
var sizeX = 8;
var sizeY = Math.floor(widgetTypeInfo.sizeY);
var widget = {
- id: widgetType.id,
+ typeId: widgetType.id,
isSystemType: isSystem,
bundleAlias: bundleAlias,
typeAlias: widgetTypeInfo.alias,
@@ -158,7 +158,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
}
if (widget) {
$state.go('home.widgets-bundles.widget-types.widget-type',
- {widgetTypeId: widget.id.id});
+ {widgetTypeId: widget.typeId.id});
} else {
$mdDialog.show({
controller: 'SelectWidgetTypeController',
@@ -177,7 +177,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
function exportWidgetType(event, widget) {
event.stopPropagation();
- importExport.exportWidgetType(widget.id.id);
+ importExport.exportWidgetType(widget.typeId.id);
}
function importWidgetType($event) {
ui/src/scss/main.scss 18(+17 -1)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index ab9b0d7..4ffabc9 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -280,8 +280,11 @@ $previewSize: 100px;
overflow: hidden;
label {
width: 100%;
- font-size: 24px;
+ font-size: 16px;
text-align: center;
+ @media (min-width: $layout-breakpoint-sm) {
+ font-size: 24px;
+ }
}
}
@@ -369,6 +372,19 @@ md-tabs.tb-headless {
}
}
+.md-button.tb-layout-button {
+ width: 100%;
+ height: 100%;
+ max-width: 240px;
+ span {
+ padding: 40px;
+ font-size: 18px;
+ font-weight: 400;
+ white-space: normal;
+ line-height: 18px;
+ }
+}
+
.md-button.tb-add-new-widget {
border-style: dashed;
border-width: 2px;