Browse Source

Merge with master

pull/151/head
Andrew Shvayka 9 years ago
parent
commit
f336cb6430
  1. 21
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  2. 32
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  3. 29
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  4. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  5. 4
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  6. 8
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  7. 46
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java
  8. 3
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  9. 42
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  10. 23
      dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
  11. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  12. 11
      dao/src/main/resources/schema.cql
  13. 283
      dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
  14. 62
      ui/src/app/api/attribute.service.js
  15. 35
      ui/src/app/api/dashboard.service.js
  16. 18
      ui/src/app/api/device.service.js
  17. 322
      ui/src/app/api/entity.service.js
  18. 15
      ui/src/app/api/user.service.js
  19. 2
      ui/src/app/app.config.js
  20. 372
      ui/src/app/common/dashboard-utils.service.js
  21. 11
      ui/src/app/common/types.constant.js
  22. 13
      ui/src/app/components/dashboard-autocomplete.directive.js
  23. 187
      ui/src/app/components/dashboard.directive.js
  24. 7
      ui/src/app/components/dashboard.tpl.html
  25. 128
      ui/src/app/components/related-entity-autocomplete.directive.js
  26. 30
      ui/src/app/components/related-entity-autocomplete.scss
  27. 43
      ui/src/app/components/related-entity-autocomplete.tpl.html
  28. 205
      ui/src/app/components/widget-config.directive.js
  29. 5
      ui/src/app/components/widget.controller.js
  30. 12
      ui/src/app/dashboard/add-widget.controller.js
  31. 70
      ui/src/app/dashboard/dashboard-settings.controller.js
  32. 215
      ui/src/app/dashboard/dashboard-settings.tpl.html
  33. 686
      ui/src/app/dashboard/dashboard.controller.js
  34. 6
      ui/src/app/dashboard/dashboard.routes.js
  35. 36
      ui/src/app/dashboard/dashboard.scss
  36. 241
      ui/src/app/dashboard/dashboard.tpl.html
  37. 12
      ui/src/app/dashboard/edit-widget.directive.js
  38. 10
      ui/src/app/dashboard/index.js
  39. 277
      ui/src/app/dashboard/layouts/dashboard-layout.directive.js
  40. 69
      ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
  41. 25
      ui/src/app/dashboard/layouts/index.js
  42. 83
      ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
  43. 65
      ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
  44. 33
      ui/src/app/dashboard/layouts/select-target-layout.controller.js
  45. 48
      ui/src/app/dashboard/layouts/select-target-layout.tpl.html
  46. 82
      ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
  47. 72
      ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
  48. 181
      ui/src/app/dashboard/states/default-state-controller.js
  49. 22
      ui/src/app/dashboard/states/default-state-controller.tpl.html
  50. 243
      ui/src/app/dashboard/states/entity-state-controller.js
  51. 33
      ui/src/app/dashboard/states/entity-state-controller.scss
  52. 33
      ui/src/app/dashboard/states/entity-state-controller.tpl.html
  53. 29
      ui/src/app/dashboard/states/index.js
  54. 198
      ui/src/app/dashboard/states/manage-dashboard-states.controller.js
  55. 34
      ui/src/app/dashboard/states/manage-dashboard-states.scss
  56. 127
      ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
  57. 35
      ui/src/app/dashboard/states/select-target-state.controller.js
  58. 50
      ui/src/app/dashboard/states/select-target-state.tpl.html
  59. 117
      ui/src/app/dashboard/states/states-component.directive.js
  60. 60
      ui/src/app/dashboard/states/states-controller.service.js
  61. 111
      ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
  62. 2
      ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
  63. 2
      ui/src/app/global-interceptor.service.js
  64. 55
      ui/src/app/import-export/import-export.service.js
  65. 39
      ui/src/app/locale/locale.constant.js
  66. 26
      ui/src/app/locale/translate-handler.js
  67. 186
      ui/src/app/services/item-buffer.service.js
  68. 13
      ui/src/app/user/user-fieldset.tpl.html
  69. 12
      ui/src/app/user/user.controller.js
  70. 4
      ui/src/app/user/user.directive.js
  71. 6
      ui/src/app/widget/widget-library.controller.js
  72. 18
      ui/src/scss/main.scss

21
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());
}
}

32
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

29
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);
}
}
}

18
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=");

4
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);

8
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -64,6 +64,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
return getData(dashboardEntity);
}
@Override
public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
log.trace("Executing findDashboardInfoById [{}]", dashboardId);
Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
DashboardInfoEntity dashboardInfoEntity = dashboardInfoDao.findById(dashboardId.getId());
return getData(dashboardInfoEntity);
}
@Override
public Dashboard saveDashboard(Dashboard dashboard) {
log.trace("Executing saveDashboard [{}]", dashboard);

46
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;
}
}

3
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);
}

42
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>() {

23
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;
}

1
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";

11
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,

283
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));
}
}

62
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;
}

35
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';

18
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;
}
}

322
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;
}
}
}

15
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};

2
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);

372
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;
}
}

11
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;

13
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: '=?',

187
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;
}
}

7
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,

128
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: '@'
}
};
}

30
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;
}
}

43
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>

205
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();

5
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 = {

12
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});
}
}

70
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
}
);
}
}

215
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>

686
ui/src/app/dashboard/dashboard.controller.js

File diff suppressed because it is too large

6
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: {

36
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;
}
}
}
}
/*****************************

241
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>

12
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: '='
}
};

10
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)

277
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);
}
}
}

69
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>

25
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;

83
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);
}
}

65
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>

33
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);
}
}

48
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>

82
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);
}
}

72
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>

181
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)});
}
}
}

22
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>

243
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)});
}
}
}

33
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;
}
}
}

33
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>

29
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;

198
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);
}
}

34
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;
}
}
}
}
}
}

127
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>&nbsp;</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>&nbsp</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>

35
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);
}
}

50
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>

117
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: '='
}
};
}

60
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];
}
}

111
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);
}
}
);
}
);
}

2
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>

2
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) {

55
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

39
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",

26
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');
}
};
}

186
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;
}
}

13
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"

12
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 () {

4
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';
}

6
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) {

18
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;

Loading…
Cancel
Save