Browse Source

UI: Ability to define device filters for dashboard aliases

pull/68/head
Igor Kulikov 10 years ago
parent
commit
207642e8c3
  1. 10
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  2. 33
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  3. 21
      dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
  4. 8
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  5. 22
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  6. 23
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
  7. 5
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  8. 23
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  9. 18
      dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
  10. 2
      dao/src/main/resources/system-data.cql
  11. 199
      ui/src/app/api/device.service.js
  12. 6
      ui/src/app/components/dashboard.directive.js
  13. 2
      ui/src/app/components/dashboard.tpl.html
  14. 217
      ui/src/app/components/device-filter.directive.js
  15. 45
      ui/src/app/components/device-filter.scss
  16. 67
      ui/src/app/components/device-filter.tpl.html
  17. 19
      ui/src/app/components/legend.directive.js
  18. 4
      ui/src/app/components/legend.tpl.html
  19. 26
      ui/src/app/components/timeinterval.directive.js
  20. 77
      ui/src/app/components/widget.controller.js
  21. 24
      ui/src/app/dashboard/add-widget.controller.js
  22. 2
      ui/src/app/dashboard/add-widget.tpl.html
  23. 32
      ui/src/app/dashboard/aliases-device-select-button.tpl.html
  24. 31
      ui/src/app/dashboard/aliases-device-select-panel.controller.js
  25. 33
      ui/src/app/dashboard/aliases-device-select-panel.tpl.html
  26. 153
      ui/src/app/dashboard/aliases-device-select.directive.js
  27. 43
      ui/src/app/dashboard/aliases-device-select.scss
  28. 74
      ui/src/app/dashboard/dashboard.controller.js
  29. 4
      ui/src/app/dashboard/dashboard.scss
  30. 26
      ui/src/app/dashboard/dashboard.tpl.html
  31. 107
      ui/src/app/dashboard/device-aliases.controller.js
  32. 19
      ui/src/app/dashboard/device-aliases.scss
  33. 187
      ui/src/app/dashboard/device-aliases.tpl.html
  34. 18
      ui/src/app/dashboard/edit-widget.directive.js
  35. 2
      ui/src/app/dashboard/edit-widget.tpl.html
  36. 6
      ui/src/app/dashboard/index.js
  37. 8
      ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
  38. 12
      ui/src/app/device/attribute/attribute-table.directive.js
  39. 2
      ui/src/app/device/attribute/attribute-table.tpl.html
  40. 1
      ui/src/app/device/devices.tpl.html
  41. 63
      ui/src/app/import-export/import-export.service.js
  42. 19
      ui/src/app/locale/locale.constant.js
  43. 61
      ui/src/app/services/item-buffer.service.js
  44. 9
      ui/src/app/widget/lib/flot-widget.js

10
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -157,6 +157,16 @@ public abstract class BaseController {
} }
} }
void checkArrayParameter(String name, String[] params) throws ThingsboardException {
if (params == null || params.length == 0) {
throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
for (String param : params) {
checkParameter(name, param);
}
}
}
UUID toUUID(String id) { UUID toUUID(String id) {
return UUID.fromString(id); return UUID.fromString(id);
} }

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -22,6 +23,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; 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.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -29,6 +31,11 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardException; import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg; 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;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@ -189,4 +196,30 @@ public class DeviceController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
@ResponseBody
public List<Device> getDevicesByIds(
@RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException {
checkArrayParameter("deviceIds", strDeviceIds);
try {
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
CustomerId customerId = user.getCustomerId();
List<DeviceId> deviceIds = new ArrayList<>();
for (String strDeviceId : strDeviceIds) {
deviceIds.add(new DeviceId(toUUID(strDeviceId)));
}
ListenableFuture<List<Device>> devices;
if (customerId == null || customerId.isNullUid()) {
devices = deviceService.findDevicesByTenantIdAndIdsAsync(tenantId, deviceIds);
} else {
devices = deviceService.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, deviceIds);
}
return checkNotNull(devices.get());
} catch (Exception e) {
throw handleException(e);
}
}
} }

21
dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java

@ -64,6 +64,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
return list; return list;
} }
protected ListenableFuture<List<T>> findListByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
ListenableFuture<List<T>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<T>>() {
@Nullable
@Override
public List<T> apply(@Nullable ResultSet resultSet) {
Result<T> result = getMapper().map(resultSet);
if (result != null) {
return result.all();
} else {
return Collections.emptyList();
}
}
});
return result;
}
return Futures.immediateFuture(Collections.emptyList());
}
protected T findOneByStatement(Statement statement) { protected T findOneByStatement(Statement statement) {
T object = null; T object = null;
if (statement != null) { if (statement != null) {

8
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -56,4 +56,12 @@ public abstract class DaoUtil {
return id; return id;
} }
public static List<UUID> toUUIDs(List<? extends UUIDBased> idBasedIds) {
List<UUID> ids = new ArrayList<>();
for (UUIDBased idBased : idBasedIds) {
ids.add(getId(idBased));
}
return ids;
}
} }

22
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -19,6 +19,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
@ -46,7 +47,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
* @return the list of device objects * @return the list of device objects
*/ */
List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink); List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
/**
* Find devices by tenantId and devices Ids.
*
* @param tenantId the tenantId
* @param deviceIds the device Ids
* @return the list of device objects
*/
ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds);
/** /**
* Find devices by tenantId, customerId and page link. * Find devices by tenantId, customerId and page link.
* *
@ -57,6 +67,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
*/ */
List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
/**
* Find devices by tenantId, customerId and devices Ids.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param deviceIds the device Ids
* @return the list of device objects
*/
ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
/** /**
* Find devices by tenantId and device name. * Find devices by tenantId and device name.
* *

23
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java

@ -16,12 +16,14 @@
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select; import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
import static org.thingsboard.server.dao.model.ModelConstants.*; import static org.thingsboard.server.dao.model.ModelConstants.*;
import java.util.*; import java.util.*;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -61,6 +63,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
return deviceEntities; return deviceEntities;
} }
@Override
public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds);
Select select = select().from(getColumnFamilyName());
Select.Where query = select.where();
query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
query.and(in(ID_PROPERTY, deviceIds));
return findListByStatementAsync(query);
}
@Override @Override
public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
@ -73,6 +85,17 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
return deviceEntities; return deviceEntities;
} }
@Override
public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds);
Select select = select().from(getColumnFamilyName());
Select.Where query = select.where();
query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId));
query.and(in(ID_PROPERTY, deviceIds));
return findListByStatementAsync(query);
}
@Override @Override
public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) { public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME); Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME);

5
dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface DeviceService { public interface DeviceService {
@ -43,9 +44,13 @@ public interface DeviceService {
TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink); TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
void deleteDevicesByTenantId(TenantId tenantId); void deleteDevicesByTenantId(TenantId tenantId);
TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
} }

23
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -45,8 +45,10 @@ import java.util.Optional;
import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.convertDataList;
import static org.thingsboard.server.dao.DaoUtil.getData; import static org.thingsboard.server.dao.DaoUtil.getData;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service @Service
@ -143,6 +145,16 @@ public class DeviceServiceImpl implements DeviceService {
return new TextPageData<Device>(devices, pageLink); return new TextPageData<Device>(devices, pageLink);
} }
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
validateId(tenantId, "Incorrect tenantId " + tenantId);
validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds));
return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
}
@Override @Override
public void deleteDevicesByTenantId(TenantId tenantId) { public void deleteDevicesByTenantId(TenantId tenantId) {
log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId); log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
@ -161,6 +173,17 @@ public class DeviceServiceImpl implements DeviceService {
return new TextPageData<Device>(devices, pageLink); return new TextPageData<Device>(devices, pageLink);
} }
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);
validateId(tenantId, "Incorrect tenantId " + tenantId);
validateId(customerId, "Incorrect customerId " + customerId);
validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(),
customerId.getId(), toUUIDs(deviceIds));
return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
}
@Override @Override
public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) { public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) {
log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId); log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId);

18
dao/src/main/java/org/thingsboard/server/dao/service/Validator.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class Validator { public class Validator {
@ -77,6 +78,23 @@ public class Validator {
} }
} }
/**
* This method validate list of <code>UUIDBased</code> ids. If at least one of the ids is null than throw
* <code>IncorrectParameterException</code> exception
*
* @param ids the list of ids
* @param errorMessage the error message for exception
*/
public static void validateIds(List<? extends UUIDBased> ids, String errorMessage) {
if (ids == null || ids.isEmpty()) {
throw new IncorrectParameterException(errorMessage);
} else {
for (UUIDBased id : ids) {
validateId(id, errorMessage);
}
}
}
/** /**
* This method validate <code>PageLink</code> page link. If pageLink is invalid than throw * This method validate <code>PageLink</code> page link. If pageLink is invalid than throw
* <code>IncorrectParameterException</code> exception * <code>IncorrectParameterException</code> exception

2
dao/src/main/resources/system-data.cql

File diff suppressed because one or more lines are too long

199
ui/src/app/api/device.service.js

@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function DeviceService($http, $q, $filter, telemetryWebsocketService, types) { function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
var deviceAttributesSubscriptionMap = {}; var deviceAttributesSubscriptionMap = {};
@ -30,6 +30,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
deleteDevice: deleteDevice, deleteDevice: deleteDevice,
getCustomerDevices: getCustomerDevices, getCustomerDevices: getCustomerDevices,
getDevice: getDevice, getDevice: getDevice,
getDevices: getDevices,
processDeviceAliases: processDeviceAliases,
checkDeviceAlias: checkDeviceAlias,
getDeviceCredentials: getDeviceCredentials, getDeviceCredentials: getDeviceCredentials,
getDeviceKeys: getDeviceKeys, getDeviceKeys: getDeviceKeys,
getDeviceTimeseriesValues: getDeviceTimeseriesValues, getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@ -99,6 +102,200 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
return deferred.promise; return deferred.promise;
} }
function getDevices(deviceIds) {
var deferred = $q.defer();
var ids = '';
for (var i=0;i<deviceIds.length;i++) {
if (i>0) {
ids += ',';
}
ids += deviceIds[i];
}
var url = '/api/devices?deviceIds=' + ids;
$http.get(url, null).then(function success(response) {
var devices = response.data;
devices.sort(function (device1, device2) {
var id1 = device1.id.id;
var id2 = device2.id.id;
var index1 = deviceIds.indexOf(id1);
var index2 = deviceIds.indexOf(id2);
return index1 - index2;
});
deferred.resolve(devices);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
var deferred = $q.defer();
var user = userService.getCurrentUser();
var promise;
var pageLink = {limit: limit, textSearch: deviceNameFilter};
if (user.authority === 'CUSTOMER_USER') {
var customerId = user.customerId;
promise = getCustomerDevices(customerId, pageLink);
} else {
promise = getTenantDevices(pageLink);
}
promise.then(
function success(result) {
if (result.data && result.data.length > 0) {
deferred.resolve(result.data);
} else {
deferred.resolve(null);
}
},
function fail() {
deferred.resolve(null);
}
);
return deferred.promise;
}
function deviceToDeviceInfo(device) {
return { name: device.name, id: device.id.id };
}
function devicesToDevicesInfo(devices) {
var devicesInfo = [];
for (var d in devices) {
devicesInfo.push(deviceToDeviceInfo(devices[d]));
}
return devicesInfo;
}
function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) {
if (index < aliasIds.length) {
var aliasId = aliasIds[index];
var deviceAlias = deviceAliases[aliasId];
var alias = deviceAlias.alias;
if (!deviceAlias.deviceFilter) {
getDevice(deviceAlias.deviceId).then(
function success(device) {
var resolvedAlias = {alias: alias, deviceId: device.id.id};
resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [
deviceToDeviceInfo(device)
];
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
},
function fail() {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
);
} else {
var deviceFilter = deviceAlias.deviceFilter;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
function(devices) {
if (devices && devices != null) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
} else {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
});
} else {
var deviceList = deviceFilter.deviceList;
getDevices(deviceList).then(
function success(devices) {
if (devices && devices.length > 0) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
} else {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
},
function fail() {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
);
}
}
} else {
deferred.resolve(resolution);
}
}
function processDeviceAliases(deviceAliases) {
var deferred = $q.defer();
var resolution = {
aliasesInfo: {
deviceAliases: {},
deviceAliasesInfo: {}
}
};
var aliasIds = [];
if (deviceAliases) {
for (var aliasId in deviceAliases) {
aliasIds.push(aliasId);
}
}
processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred);
return deferred.promise;
}
function checkDeviceAlias(deviceAlias) {
var deferred = $q.defer();
var deviceFilter;
if (deviceAlias.deviceId) {
deviceFilter = {
useFilter: false,
deviceNameFilter: '',
deviceList: [deviceAlias.deviceId]
}
} else {
deviceFilter = deviceAlias.deviceFilter;
}
var promise;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
} else {
var deviceList = deviceFilter.deviceList;
promise = getDevices(deviceList);
}
promise.then(
function success(devices) {
if (devices && devices.length > 0) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
},
function fail() {
deferred.resolve(false);
}
);
return deferred.promise;
}
function saveDevice(device) { function saveDevice(device) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device'; var url = '/api/device';

6
ui/src/app/components/dashboard.directive.js

@ -51,7 +51,7 @@ function Dashboard() {
scope: true, scope: true,
bindToController: { bindToController: {
widgets: '=', widgets: '=',
deviceAliasList: '=', aliasesInfo: '=',
dashboardTimewindow: '=?', dashboardTimewindow: '=?',
columns: '=', columns: '=',
margins: '=', margins: '=',
@ -274,8 +274,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
$scope.$broadcast('toggleDashboardEditMode', vm.isEdit); $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
}); });
$scope.$watch('vm.deviceAliasList', function () { $scope.$watch('vm.aliasesInfo.deviceAliases', function () {
$scope.$broadcast('deviceAliasListChanged', vm.deviceAliasList); $scope.$broadcast('deviceAliasListChanged', vm.aliasesInfo);
}, true); }, true);
$scope.$on('gridster-resized', function (event, sizes, theGridster) { $scope.$on('gridster-resized', function (event, sizes, theGridster) {

2
ui/src/app/components/dashboard.tpl.html

@ -90,7 +90,7 @@
<div flex tb-widget <div flex tb-widget
locals="{ visibleRect: vm.visibleRect, locals="{ visibleRect: vm.visibleRect,
widget: widget, widget: widget,
deviceAliasList: vm.deviceAliasList, aliasesInfo: vm.aliasesInfo,
isEdit: vm.isEdit, isEdit: vm.isEdit,
stDiff: vm.stDiff, stDiff: vm.stDiff,
dashboardTimewindow: vm.dashboardTimewindow, dashboardTimewindow: vm.dashboardTimewindow,

217
ui/src/app/components/device-filter.directive.js

@ -0,0 +1,217 @@
/*
* 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 deviceFilterTemplate from './device-filter.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './device-filter.scss';
export default angular.module('thingsboard.directives.deviceFilter', [])
.directive('tbDeviceFilter', DeviceFilter)
.name;
/*@ngInject*/
function DeviceFilter($compile, $templateCache, $q, deviceService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(deviceFilterTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.fetchDevices = function(searchText, limit) {
var pageLink = {limit: limit, textSearch: searchText};
var deferred = $q.defer();
deviceService.getTenantDevices(pageLink).then(function success(result) {
deferred.resolve(result.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
scope.updateValidity = function() {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var valid;
if (value.useFilter) {
ngModelCtrl.$setValidity('deviceList', true);
if (angular.isDefined(value.deviceNameFilter) && value.deviceNameFilter.length > 0) {
ngModelCtrl.$setValidity('deviceNameFilter', true);
valid = angular.isDefined(scope.model.matchingFilterDevice) && scope.model.matchingFilterDevice != null;
ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', valid);
} else {
ngModelCtrl.$setValidity('deviceNameFilter', false);
}
} else {
ngModelCtrl.$setValidity('deviceNameFilter', true);
ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', true);
valid = angular.isDefined(value.deviceList) && value.deviceList.length > 0;
ngModelCtrl.$setValidity('deviceList', valid);
}
}
}
ngModelCtrl.$render = function () {
destroyWatchers();
scope.model = {
useFilter: false,
deviceList: [],
deviceNameFilter: ''
}
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var model = scope.model;
model.useFilter = value.useFilter === true ? true: false;
model.deviceList = [];
model.deviceNameFilter = value.deviceNameFilter || '';
processDeviceNameFilter(model.deviceNameFilter).then(
function(device) {
scope.model.matchingFilterDevice = device;
if (value.deviceList && value.deviceList.length > 0) {
deviceService.getDevices(value.deviceList).then(function (devices) {
model.deviceList = devices;
updateMatchingDevice();
initWatchers();
});
} else {
updateMatchingDevice();
initWatchers();
}
}
)
}
}
function updateMatchingDevice() {
if (scope.model.useFilter) {
scope.model.matchingDevice = scope.model.matchingFilterDevice;
} else {
if (scope.model.deviceList && scope.model.deviceList.length > 0) {
scope.model.matchingDevice = scope.model.deviceList[0];
} else {
scope.model.matchingDevice = null;
}
}
}
function processDeviceNameFilter(deviceNameFilter) {
var deferred = $q.defer();
if (angular.isDefined(deviceNameFilter) && deviceNameFilter.length > 0) {
scope.fetchDevices(deviceNameFilter, 1).then(function (devices) {
if (devices && devices.length > 0) {
deferred.resolve(devices[0]);
} else {
deferred.resolve(null);
}
});
} else {
deferred.resolve(null);
}
return deferred.promise;
}
function destroyWatchers() {
if (scope.deviceListDeregistration) {
scope.deviceListDeregistration();
scope.deviceListDeregistration = null;
}
if (scope.useFilterDeregistration) {
scope.useFilterDeregistration();
scope.useFilterDeregistration = null;
}
if (scope.deviceNameFilterDeregistration) {
scope.deviceNameFilterDeregistration();
scope.deviceNameFilterDeregistration = null;
}
if (scope.matchingDeviceDeregistration) {
scope.matchingDeviceDeregistration();
scope.matchingDeviceDeregistration = null;
}
}
function initWatchers() {
scope.deviceListDeregistration = scope.$watch('model.deviceList', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.deviceList = [];
if (scope.model.deviceList && scope.model.deviceList.length > 0) {
for (var i in scope.model.deviceList) {
value.deviceList.push(scope.model.deviceList[i].id.id);
}
}
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
}, true);
scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.useFilter = scope.model.useFilter;
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
});
scope.deviceNameFilterDeregistration = scope.$watch('model.deviceNameFilter', function (newNameFilter, prevNameFilter) {
if (ngModelCtrl.$viewValue) {
if (!angular.equals(newNameFilter, prevNameFilter)) {
var value = ngModelCtrl.$viewValue;
value.deviceNameFilter = scope.model.deviceNameFilter;
processDeviceNameFilter(value.deviceNameFilter).then(
function(device) {
scope.model.matchingFilterDevice = device;
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
);
}
}
});
scope.matchingDeviceDeregistration = scope.$watch('model.matchingDevice', function (newMatchingDevice, prevMatchingDevice) {
if (!angular.equals(newMatchingDevice, prevMatchingDevice)) {
if (scope.onMatchingDeviceChange) {
scope.onMatchingDeviceChange({device: newMatchingDevice});
}
}
});
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
isEdit: '=',
onMatchingDeviceChange: '&'
}
};
}

45
ui/src/app/components/device-filter.scss

@ -0,0 +1,45 @@
/**
* 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-device-filter {
#device_list_chips {
.md-chips {
padding-bottom: 1px;
}
}
.device-name-filter-input {
margin-top: 10px;
margin-bottom: 0px;
.md-errors-spacer {
min-height: 0px;
}
}
.tb-filter-switch {
padding-left: 10px;
.filter-switch {
margin: 0;
}
.filter-label {
margin: 5px 0;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}

67
ui/src/app/components/device-filter.tpl.html

@ -0,0 +1,67 @@
<!--
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.
-->
<section layout='column' class="tb-device-filter">
<section layout='row'>
<section layout="column" flex ng-show="!model.useFilter">
<md-chips flex
id="device_list_chips"
ng-required="!useFilter"
ng-model="model.deviceList" md-autocomplete-snap
md-require-match="true">
<md-autocomplete
md-no-cache="true"
id="device"
md-selected-item="selectedDevice"
md-search-text="deviceSearchText"
md-items="item in fetchDevices(deviceSearchText, 10)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'device.device-list' | translate }}">
<md-item-template>
<span md-highlight-text="deviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ device: deviceSearchText }'>device.no-devices-matching</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
</section>
<section layout="row" flex ng-show="model.useFilter">
<md-input-container flex class="device-name-filter-input">
<label translate>device.name-starts-with</label>
<input ng-model="model.deviceNameFilter" aria-label="{{ 'device.name-starts-with' | translate }}">
</md-input-container>
</section>
<section class="tb-filter-switch" layout="column" layout-align="center center">
<label class="tb-small filter-label" translate>device.use-device-name-filter</label>
<md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
</md-switch>
</section>
</section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="deviceList" class="tb-error-message">device.device-list-empty</div>
<div translate ng-message="deviceNameFilter" class="tb-error-message">device.device-name-filter-required</div>
<div translate translate-values='{ device: model.deviceNameFilter }' ng-message="deviceNameFilterDeviceMatch"
class="tb-error-message">device.device-name-filter-no-device-matched</div>
</div>
</section>

19
ui/src/app/components/legend.directive.js

@ -44,8 +44,10 @@ function Legend($compile, $templateCache, types) {
scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value || scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
scope.legendConfig.position === types.position.top.value; scope.legendConfig.position === types.position.top.value;
scope.$on('legendDataUpdated', function () { scope.$on('legendDataUpdated', function (event, apply) {
scope.$digest(); if (apply) {
scope.$digest();
}
}); });
scope.toggleHideData = function(index) { scope.toggleHideData = function(index) {
@ -62,15 +64,14 @@ function Legend($compile, $templateCache, types) {
data: [] data: []
key: { key: {
label: '', dataKey: dataKey,
color: '' dataIndex: 0
dataIndex: 0
} }
data: { data: {
min: null, min: null,
max: null, max: null,
avg: null, avg: null,
total: null total: null
} }
};*/ };*/

4
ui/src/app/components/legend.tpl.html

@ -27,11 +27,11 @@
</thead> </thead>
<tbody> <tbody>
<tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys"> <tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys">
<td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.color}"></span></td> <td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td>
<td class="tb-legend-label" <td class="tb-legend-label"
ng-click="toggleHideData(legendKey.dataIndex)" ng-click="toggleHideData(legendKey.dataIndex)"
ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }"> ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }">
{{ legendKey.label }} {{ legendKey.dataKey.label }}
</td> </td>
<td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td> <td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
<td class="tb-legend-value" ng-if="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td> <td class="tb-legend-value" ng-if="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td>

26
ui/src/app/components/timeinterval.directive.js

@ -84,6 +84,13 @@ function Timeinterval($compile, $templateCache, timeService) {
scope.rendered = true; scope.rendered = true;
} }
function calculateIntervalMs() {
return (scope.days * 86400 +
scope.hours * 3600 +
scope.mins * 60 +
scope.secs) * 1000;
}
scope.updateView = function () { scope.updateView = function () {
if (!scope.rendered) { if (!scope.rendered) {
return; return;
@ -92,11 +99,11 @@ function Timeinterval($compile, $templateCache, timeService) {
var intervalMs; var intervalMs;
if (!scope.advanced) { if (!scope.advanced) {
intervalMs = scope.intervalMs; intervalMs = scope.intervalMs;
if (!intervalMs || isNaN(intervalMs)) {
intervalMs = calculateIntervalMs();
}
} else { } else {
intervalMs = (scope.days * 86400 + intervalMs = calculateIntervalMs();
scope.hours * 3600 +
scope.mins * 60 +
scope.secs) * 1000;
} }
if (!isNaN(intervalMs) && intervalMs > 0) { if (!isNaN(intervalMs) && intervalMs > 0) {
value = intervalMs; value = intervalMs;
@ -135,12 +142,13 @@ function Timeinterval($compile, $templateCache, timeService) {
scope.$watch('advanced', function (newAdvanced, prevAdvanced) { scope.$watch('advanced', function (newAdvanced, prevAdvanced) {
if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) { if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) {
if (!scope.advanced) { if (!scope.advanced) {
scope.intervalMs = (scope.days * 86400 + scope.intervalMs = calculateIntervalMs();
scope.hours * 3600 +
scope.mins * 60 +
scope.secs) * 1000;
} else { } else {
scope.setIntervalMs(scope.intervalMs); var intervalMs = scope.intervalMs;
if (!intervalMs || isNaN(intervalMs)) {
intervalMs = calculateIntervalMs();
}
scope.setIntervalMs(intervalMs);
} }
scope.updateView(); scope.updateView();
} }

77
ui/src/app/components/widget.controller.js

@ -21,7 +21,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
/*@ngInject*/ /*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService, export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
dashboardTimewindowApi, widget, deviceAliasList, widgetType) { dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
var vm = this; var vm = this;
@ -45,6 +45,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var subscriptionTimewindow = null; var subscriptionTimewindow = null;
var dataUpdateCaf = null; var dataUpdateCaf = null;
var varsRegex = /\$\{([^\}]*)\}/g;
/* /*
* data = array of datasourceData * data = array of datasourceData
* datasourceData = { * datasourceData = {
@ -68,7 +70,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
settings: widget.config.settings, settings: widget.config.settings,
units: widget.config.units || '', units: widget.config.units || '',
decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2, decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
datasources: widget.config.datasources, datasources: angular.copy(widget.config.datasources),
data: [], data: [],
hiddenData: [], hiddenData: [],
timeWindow: { timeWindow: {
@ -320,10 +322,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.legendConfig.showTotal === true); $scope.legendConfig.showTotal === true);
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
for (var i in widget.config.datasources) { for (var i in widgetContext.datasources) {
var datasource = angular.copy(widget.config.datasources[i]); var datasource = widgetContext.datasources[i];
for (var a in datasource.dataKeys) { for (var a in datasource.dataKeys) {
var dataKey = datasource.dataKeys[a]; var dataKey = datasource.dataKeys[a];
dataKey.pattern = angular.copy(dataKey.label);
var datasourceData = { var datasourceData = {
datasource: datasource, datasource: datasource,
dataKey: dataKey, dataKey: dataKey,
@ -333,8 +336,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
widgetContext.hiddenData.push({data: []}); widgetContext.hiddenData.push({data: []});
if ($scope.displayLegend) { if ($scope.displayLegend) {
var legendKey = { var legendKey = {
label: dataKey.label, dataKey: dataKey,
color: dataKey.color,
dataIndex: Number(i) + Number(a) dataIndex: Number(i) + Number(a)
}; };
$scope.legendData.keys.push(legendKey); $scope.legendData.keys.push(legendKey);
@ -367,8 +369,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} else if (widget.type === types.widgetType.rpc.value) { } else if (widget.type === types.widgetType.rpc.value) {
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
if (deviceAliasList[targetDeviceAliasId]) { if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
targetDeviceId = deviceAliasList[targetDeviceAliasId].deviceId; targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
} }
} }
if (targetDeviceId) { if (targetDeviceId) {
@ -402,13 +404,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
onMobileModeChanged(newIsMobile); onMobileModeChanged(newIsMobile);
}); });
$scope.$on('deviceAliasListChanged', function (event, newDeviceAliasList) { $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {
deviceAliasList = newDeviceAliasList; aliasesInfo = newAliasesInfo;
if (widget.type === types.widgetType.rpc.value) { if (widget.type === types.widgetType.rpc.value) {
if (targetDeviceAliasId) { if (targetDeviceAliasId) {
var deviceId = null; var deviceId = null;
if (deviceAliasList[targetDeviceAliasId]) { if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
deviceId = deviceAliasList[targetDeviceAliasId].deviceId; deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
} }
if (!angular.equals(deviceId, targetDeviceId)) { if (!angular.equals(deviceId, targetDeviceId)) {
targetDeviceId = deviceId; targetDeviceId = deviceId;
@ -609,7 +611,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
currentData.data = sourceData.data; currentData.data = sourceData.data;
onDataUpdated(); onDataUpdated();
if ($scope.caulculateLegendData) { if ($scope.caulculateLegendData) {
updateLegend(datasourceIndex + dataKeyIndex, sourceData.data); updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
} }
} }
if (apply) { if (apply) {
@ -617,7 +619,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} }
} }
function updateLegend(dataIndex, data) { function updateLegend(dataIndex, data, apply) {
var legendKeyData = $scope.legendData.data[dataIndex]; var legendKeyData = $scope.legendData.data[dataIndex];
if ($scope.legendConfig.showMin) { if ($scope.legendConfig.showMin) {
legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units); legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units);
@ -631,7 +633,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
if ($scope.legendConfig.showTotal) { if ($scope.legendConfig.showTotal) {
legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units); legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);
} }
$scope.$broadcast('legendDataUpdated'); $scope.$broadcast('legendDataUpdated', apply !== false);
} }
function isNumeric(val) { function isNumeric(val) {
@ -707,9 +709,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var deviceId = null; var deviceId = null;
var aliasName = null; var aliasName = null;
if (listener.datasource.type === types.datasourceType.device) { if (listener.datasource.type === types.datasourceType.device) {
if (deviceAliasList[listener.datasource.deviceAliasId]) { if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
deviceId = deviceAliasList[listener.datasource.deviceAliasId].deviceId; deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
aliasName = deviceAliasList[listener.datasource.deviceAliasId].alias; aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
} }
if (!angular.equals(deviceId, listener.deviceId) || if (!angular.equals(deviceId, listener.deviceId) ||
!angular.equals(aliasName, listener.datasource.name)) { !angular.equals(aliasName, listener.datasource.name)) {
@ -756,6 +758,23 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} }
} }
function updateDataKeyLabel(dataKey, deviceName, aliasName) {
var pattern = dataKey.pattern;
var label = dataKey.pattern;
var match = varsRegex.exec(pattern);
while (match !== null) {
var variable = match[0];
var variableName = match[1];
if (variableName === 'deviceName') {
label = label.split(variable).join(deviceName);
} else if (variableName === 'aliasName') {
label = label.split(variable).join(aliasName);
}
match = varsRegex.exec(pattern);
}
dataKey.label = label;
}
function subscribe() { function subscribe() {
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
notifyDataLoading(); notifyDataLoading();
@ -767,13 +786,25 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} }
} }
var index = 0; var index = 0;
for (var i in widget.config.datasources) { for (var i in widgetContext.datasources) {
var datasource = widget.config.datasources[i]; var datasource = widgetContext.datasources[i];
var deviceId = null; var deviceId = null;
if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) { if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
if (deviceAliasList[datasource.deviceAliasId]) { if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
deviceId = deviceAliasList[datasource.deviceAliasId].deviceId; deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
datasource.name = deviceAliasList[datasource.deviceAliasId].alias; datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
var deviceName = '';
var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
for (var d=0;d<devicesInfo.length;d++) {
if (devicesInfo[d].id === deviceId) {
deviceName = devicesInfo[d].name;
break;
}
}
for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);
}
} }
} else { } else {
datasource.name = types.datasourceType.function; datasource.name = types.datasourceType.function;

24
ui/src/app/dashboard/add-widget.controller.js

@ -20,11 +20,12 @@ import deviceAliasesTemplate from './device-aliases.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, widget, widgetInfo) { export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
var vm = this; var vm = this;
vm.dashboard = dashboard; vm.dashboard = dashboard;
vm.aliasesInfo = aliasesInfo;
vm.widget = widget; vm.widget = widget;
vm.widgetInfo = widgetInfo; vm.widgetInfo = widgetInfo;
@ -78,19 +79,19 @@ export default function AddWidgetController($scope, widgetService, deviceService
} }
function cancel () { function cancel () {
$mdDialog.cancel(); $mdDialog.cancel({aliasesInfo: vm.aliasesInfo});
} }
function add () { function add () {
if ($scope.theForm.$valid) { if ($scope.theForm.$valid) {
$scope.theForm.$setPristine(); $scope.theForm.$setPristine();
vm.widget.config = vm.widgetConfig; vm.widget.config = vm.widgetConfig;
$mdDialog.hide(vm.widget); $mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
} }
} }
function fetchDeviceKeys (deviceAliasId, query, type) { function fetchDeviceKeys (deviceAliasId, query, type) {
var deviceAlias = vm.dashboard.configuration.deviceAliases[deviceAliasId]; var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId];
if (deviceAlias && deviceAlias.deviceId) { if (deviceAlias && deviceAlias.deviceId) {
return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type); return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
} else { } else {
@ -101,7 +102,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
function createDeviceAlias (event, alias) { function createDeviceAlias (event, alias) {
var deferred = $q.defer(); var deferred = $q.defer();
var singleDeviceAlias = {id: null, alias: alias, deviceId: null}; var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
$mdDialog.show({ $mdDialog.show({
controller: 'DeviceAliasesController', controller: 'DeviceAliasesController',
@ -111,7 +112,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
config: { config: {
deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases), deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
widgets: null, widgets: null,
isSingleDevice: true, isSingleDeviceAlias: true,
singleDeviceAlias: singleDeviceAlias singleDeviceAlias: singleDeviceAlias
} }
}, },
@ -121,8 +122,15 @@ export default function AddWidgetController($scope, widgetService, deviceService
targetEvent: event targetEvent: event
}).then(function (singleDeviceAlias) { }).then(function (singleDeviceAlias) {
vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] = vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
{ alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId }; { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
deferred.resolve(singleDeviceAlias); deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then(
function(resolution) {
if (!resolution.error) {
vm.aliasesInfo = resolution.aliasesInfo;
}
deferred.resolve(singleDeviceAlias);
}
);
}, function () { }, function () {
deferred.reject(); deferred.reject();
}); });

2
ui/src/app/dashboard/add-widget.tpl.html

@ -37,7 +37,7 @@
ng-model="vm.widgetConfig" ng-model="vm.widgetConfig"
widget-settings-schema="vm.settingsSchema" widget-settings-schema="vm.settingsSchema"
datakey-settings-schema="vm.dataKeySettingsSchema" datakey-settings-schema="vm.dataKeySettingsSchema"
device-aliases="vm.dashboard.configuration.deviceAliases" device-aliases="vm.aliasesInfo.deviceAliases"
functions-only="vm.functionsOnly" functions-only="vm.functionsOnly"
fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)" fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
on-create-device-alias="vm.createDeviceAlias(event, alias)" on-create-device-alias="vm.createDeviceAlias(event, alias)"

32
ui/src/app/dashboard/aliases-device-select-button.tpl.html

@ -0,0 +1,32 @@
<!--
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.
-->
<section class="tb-aliases-device-select" layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
<md-button class="md-icon-button" aria-label="{{ 'dashboard.select-devices' | translate }}" ng-click="openEditMode($event)">
<md-tooltip md-direction="{{tooltipDirection}}">
{{ 'dashboard.select-devices' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'dashboard.select-devices' | translate }}" class="material-icons">devices_other</md-icon>
</md-button>
<span ng-click="openEditMode($event)">
<md-tooltip md-direction="{{tooltipDirection}}">
{{ 'dashboard.select-devices' | translate }}
</md-tooltip>
{{displayValue}}
</span>
</section>

31
ui/src/app/dashboard/aliases-device-select-panel.controller.js

@ -0,0 +1,31 @@
/*
* 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 AliasesDeviceSelectPanelController(mdPanelRef, $scope, types, deviceAliases, deviceAliasesInfo, onDeviceAliasesUpdate) {
var vm = this;
vm._mdPanelRef = mdPanelRef;
vm.deviceAliases = deviceAliases;
vm.deviceAliasesInfo = deviceAliasesInfo;
vm.onDeviceAliasesUpdate = onDeviceAliasesUpdate;
$scope.$watch('vm.deviceAliases', function () {
if (onDeviceAliasesUpdate) {
onDeviceAliasesUpdate(vm.deviceAliases);
}
}, true);
}

33
ui/src/app/dashboard/aliases-device-select-panel.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.
-->
<md-content flex layout="column">
<section flex layout="column">
<md-content flex class="md-padding" layout="column">
<div flex layout="row" ng-repeat="(aliasId, deviceAlias) in vm.deviceAliases">
<md-input-container flex>
<label>{{deviceAlias.alias}}</label>
<md-select ng-model="vm.deviceAliases[aliasId].deviceId">
<md-option ng-repeat="deviceInfo in vm.deviceAliasesInfo[aliasId]" ng-value="deviceInfo.id">
{{deviceInfo.name}}
</md-option>
</md-select>
</md-input-container>
</div>
</md-content>
</section>
</md-content>

153
ui/src/app/dashboard/aliases-device-select.directive.js

@ -0,0 +1,153 @@
/*
* 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 './aliases-device-select.scss';
import $ from 'jquery';
/* eslint-disable import/no-unresolved, import/default */
import aliasesDeviceSelectButtonTemplate from './aliases-device-select-button.tpl.html';
import aliasesDeviceSelectPanelTemplate from './aliases-device-select-panel.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/* eslint-disable angular/angularelement */
/*@ngInject*/
export default function AliasesDeviceSelectDirective($compile, $templateCache, types, $mdPanel, $document, $translate) {
var linker = function (scope, element, attrs, ngModelCtrl) {
/* tbAliasesDeviceSelect (ng-model)
* {
* "aliasId": {
* alias: alias,
* deviceId: deviceId
* }
* }
*/
var template = $templateCache.get(aliasesDeviceSelectButtonTemplate);
scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top';
element.html(template);
scope.openEditMode = function (event) {
if (scope.disabled) {
return;
}
var position;
var panelHeight = 250;
var panelWidth = 300;
var offset = element[0].getBoundingClientRect();
var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
var yPosition;
var xPosition;
if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
yPosition = $mdPanel.yPosition.ABOVE;
} else {
yPosition = $mdPanel.yPosition.BELOW;
}
if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
xPosition = $mdPanel.xPosition.ALIGN_END;
} else {
xPosition = $mdPanel.xPosition.ALIGN_START;
}
position = $mdPanel.newPanelPosition()
.relativeTo(element)
.addPanelPosition(xPosition, yPosition);
var config = {
attachTo: angular.element($document[0].body),
controller: 'AliasesDeviceSelectPanelController',
controllerAs: 'vm',
templateUrl: aliasesDeviceSelectPanelTemplate,
panelClass: 'tb-aliases-device-select-panel',
position: position,
fullscreen: false,
locals: {
'deviceAliases': angular.copy(scope.model),
'deviceAliasesInfo': scope.deviceAliasesInfo,
'onDeviceAliasesUpdate': function (deviceAliases) {
scope.model = deviceAliases;
scope.updateView();
}
},
openFrom: event,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config);
}
scope.updateView = function () {
var value = angular.copy(scope.model);
ngModelCtrl.$setViewValue(value);
updateDisplayValue();
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
scope.model = angular.copy(value);
updateDisplayValue();
}
}
function updateDisplayValue() {
var displayValue;
var singleValue = true;
var currentAliasId;
for (var aliasId in scope.model) {
if (!currentAliasId) {
currentAliasId = aliasId;
} else {
singleValue = false;
break;
}
}
if (singleValue) {
var deviceId = scope.model[currentAliasId].deviceId;
var devicesInfo = scope.deviceAliasesInfo[currentAliasId];
for (var i=0;i<devicesInfo.length;i++) {
if (devicesInfo[i].id === deviceId) {
displayValue = devicesInfo[i].name;
break;
}
}
} else {
displayValue = $translate.instant('device.devices');
}
scope.displayValue = displayValue;
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
scope: {
deviceAliasesInfo:'='
},
link: linker
};
}
/* eslint-enable angular/angularelement */

43
ui/src/app/dashboard/aliases-device-select.scss

@ -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-panel {
&.tb-aliases-device-select-panel {
position: absolute;
}
}
.tb-aliases-device-select-panel {
max-height: 250px;
min-width: 300px;
background: white;
border-radius: 4px;
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
overflow-x: hidden;
overflow-y: auto;
md-content {
background-color: #fff;
}
}
.tb-aliases-device-select {
span {
pointer-events: all;
cursor: pointer;
}
}

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

@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
/*@ngInject*/ /*@ngInject*/
export default function DashboardController(types, widgetService, userService, export default function DashboardController(types, widgetService, userService,
dashboardService, timeService, itembuffer, importExport, hotkeys, $window, $rootScope, dashboardService, timeService, deviceService, itembuffer, importExport, hotkeys, $window, $rootScope,
$scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
var user = userService.getCurrentUser(); var user = userService.getCurrentUser();
@ -82,6 +82,8 @@ export default function DashboardController(types, widgetService, userService,
vm.loadDashboard = loadDashboard; vm.loadDashboard = loadDashboard;
vm.getServerTimeDiff = getServerTimeDiff; vm.getServerTimeDiff = getServerTimeDiff;
vm.noData = noData; vm.noData = noData;
vm.dashboardConfigurationError = dashboardConfigurationError;
vm.showDashboardToolbar = showDashboardToolbar;
vm.onAddWidgetClosed = onAddWidgetClosed; vm.onAddWidgetClosed = onAddWidgetClosed;
vm.onEditWidgetClosed = onEditWidgetClosed; vm.onEditWidgetClosed = onEditWidgetClosed;
vm.openDeviceAliases = openDeviceAliases; vm.openDeviceAliases = openDeviceAliases;
@ -212,9 +214,21 @@ export default function DashboardController(types, widgetService, userService,
if (angular.isUndefined(vm.dashboard.configuration.timewindow)) { if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
vm.dashboard.configuration.timewindow = timeService.defaultTimewindow(); vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
} }
vm.dashboardConfiguration = vm.dashboard.configuration; deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
vm.widgets = vm.dashboard.configuration.widgets; .then(
deferred.resolve(); function(resolution) {
if (resolution.error && !isTenantAdmin()) {
vm.configurationError = true;
showAliasesResolutionError(resolution.error);
deferred.reject();
} else {
vm.aliasesInfo = resolution.aliasesInfo;
vm.dashboardConfiguration = vm.dashboard.configuration;
vm.widgets = vm.dashboard.configuration.widgets;
deferred.resolve();
}
}
);
}, function fail(e) { }, function fail(e) {
deferred.reject(e); deferred.reject(e);
}); });
@ -245,7 +259,15 @@ export default function DashboardController(types, widgetService, userService,
} }
function noData() { function noData() {
return vm.dashboardInitComplete && vm.widgets.length == 0; return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0;
}
function dashboardConfigurationError() {
return vm.dashboardInitComplete && vm.configurationError;
}
function showDashboardToolbar() {
return vm.dashboardInitComplete && !vm.configurationError;
} }
function openDeviceAliases($event) { function openDeviceAliases($event) {
@ -257,7 +279,7 @@ export default function DashboardController(types, widgetService, userService,
config: { config: {
deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases), deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
widgets: vm.widgets, widgets: vm.widgets,
isSingleDevice: false, isSingleDeviceAlias: false,
singleDeviceAlias: null singleDeviceAlias: null
} }
}, },
@ -267,6 +289,7 @@ export default function DashboardController(types, widgetService, userService,
targetEvent: $event targetEvent: $event
}).then(function (deviceAliases) { }).then(function (deviceAliases) {
vm.dashboard.configuration.deviceAliases = deviceAliases; vm.dashboard.configuration.deviceAliases = deviceAliases;
deviceAliasesUpdated();
}, function () { }, function () {
}); });
} }
@ -331,7 +354,7 @@ export default function DashboardController(types, widgetService, userService,
function importWidget($event) { function importWidget($event) {
$event.stopPropagation(); $event.stopPropagation();
importExport.importWidget($event, vm.dashboard); importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated);
} }
function widgetMouseDown($event, widget) { function widgetMouseDown($event, widget) {
@ -433,7 +456,7 @@ export default function DashboardController(types, widgetService, userService,
function pasteWidget($event) { function pasteWidget($event) {
var pos = vm.dashboardContainer.getEventGridPosition($event); var pos = vm.dashboardContainer.getEventGridPosition($event);
itembuffer.pasteWidget(vm.dashboard, pos); itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated);
} }
function prepareWidgetContextMenu() { function prepareWidgetContextMenu() {
@ -570,7 +593,7 @@ export default function DashboardController(types, widgetService, userService,
controller: 'AddWidgetController', controller: 'AddWidgetController',
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: addWidgetTemplate, templateUrl: addWidgetTemplate,
locals: {dashboard: vm.dashboard, widget: newWidget, widgetInfo: widgetTypeInfo}, locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
fullscreen: true, fullscreen: true,
skipHide: true, skipHide: true,
@ -579,7 +602,9 @@ export default function DashboardController(types, widgetService, userService,
var w = angular.element($window); var w = angular.element($window);
w.triggerHandler('resize'); w.triggerHandler('resize');
} }
}).then(function (widget) { }).then(function (result) {
var widget = result.widget;
vm.aliasesInfo = result.aliasesInfo;
var columns = 24; var columns = 24;
if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
columns = vm.dashboard.configuration.gridSettings.columns; columns = vm.dashboard.configuration.gridSettings.columns;
@ -590,7 +615,8 @@ export default function DashboardController(types, widgetService, userService,
widget.sizeY *= ratio; widget.sizeY *= ratio;
} }
vm.widgets.push(widget); vm.widgets.push(widget);
}, function () { }, function (rejection) {
vm.aliasesInfo = rejection.aliasesInfo;
}); });
} }
); );
@ -634,6 +660,7 @@ export default function DashboardController(types, widgetService, userService,
vm.dashboard = vm.prevDashboard; vm.dashboard = vm.prevDashboard;
vm.widgets = vm.dashboard.configuration.widgets; vm.widgets = vm.dashboard.configuration.widgets;
vm.dashboardConfiguration = vm.dashboard.configuration; vm.dashboardConfiguration = vm.dashboard.configuration;
deviceAliasesUpdated();
} }
} }
} }
@ -648,6 +675,31 @@ export default function DashboardController(types, widgetService, userService,
notifyDashboardUpdated(); notifyDashboardUpdated();
} }
function showAliasesResolutionError(error) {
var alert = $mdDialog.alert()
.parent(angular.element($document[0].body))
.clickOutsideToClose(true)
.title($translate.instant('dashboard.alias-resolution-error-title'))
.htmlContent($translate.instant(error))
.ariaLabel($translate.instant('dashboard.alias-resolution-error-title'))
.ok($translate.instant('action.close'))
alert._options.skipHide = true;
alert._options.fullscreen = true;
$mdDialog.show(alert);
}
function deviceAliasesUpdated() {
deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
.then(
function(resolution) {
if (resolution.aliasesInfo) {
vm.aliasesInfo = resolution.aliasesInfo;
}
}
);
}
function notifyDashboardUpdated() { function notifyDashboardUpdated() {
if (vm.widgetEditMode) { if (vm.widgetEditMode) {
var parentScope = $window.parent.angular.element($window.frameElement).scope(); var parentScope = $window.parent.angular.element($window.frameElement).scope();

4
ui/src/app/dashboard/dashboard.scss

@ -113,13 +113,11 @@ section.tb-dashboard-toolbar {
min-height: 36px; min-height: 36px;
height: 36px; height: 36px;
md-fab-actions { md-fab-actions {
font-size: 16px;
margin-top: 0px; margin-top: 0px;
.close-action { .close-action {
margin-right: -18px; margin-right: -18px;
} }
tb-timewindow {
font-size: 16px;
}
} }
} }
} }

26
ui/src/app/dashboard/dashboard.tpl.html

@ -23,7 +23,7 @@
'background-attachment': 'scroll', 'background-attachment': 'scroll',
'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%', 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'}"> 'background-position': '0% 0%'}">
<section class="tb-dashboard-toolbar" <section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }"> ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
<md-fab-toolbar md-open="vm.toolbarOpened" <md-fab-toolbar md-open="vm.toolbarOpened"
md-direction="left"> md-direction="left">
@ -49,6 +49,11 @@
</md-button> </md-button>
<tb-timewindow direction="left" tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow"> <tb-timewindow direction="left" tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow">
</tb-timewindow> </tb-timewindow>
<tb-aliases-device-select ng-show="!vm.isEdit"
tooltip-direction="bottom"
ng-model="vm.aliasesInfo.deviceAliases"
device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
</tb-aliases-device-select>
<md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button" <md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button"
ng-click="vm.openDeviceAliases($event)"> ng-click="vm.openDeviceAliases($event)">
<md-tooltip md-direction="bottom"> <md-tooltip md-direction="bottom">
@ -70,17 +75,27 @@
<section class="tb-dashboard-container tb-absolute-fill" <section class="tb-dashboard-container tb-absolute-fill"
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }"> ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
<section ng-show="!loading && vm.noData()" layout-align="center center" <section ng-show="!loading && vm.noData()" layout-align="center center"
ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
ng-class="{'tb-padded' : !vm.widgetEditMode}" ng-class="{'tb-padded' : !vm.widgetEditMode}"
style="text-transform: uppercase; display: flex; z-index: 1;" style="text-transform: uppercase; display: flex; z-index: 1;"
class="md-headline tb-absolute-fill"> class="md-headline tb-absolute-fill">
<span translate ng-if="!vm.isEdit"> <span translate ng-if="!vm.isEdit">
dashboard.no-widgets dashboard.no-widgets
</span> </span>
<md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)"> <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> <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
{{ 'dashboard.add-widget' | translate }} {{ 'dashboard.add-widget' | translate }}
</md-button> </md-button>
</section> </section>
<section ng-show="!loading && vm.dashboardConfigurationError()" 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>
dashboard.configuration-error
</span>
</section>
<section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center" <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.gridSettings.titleColor}">
<h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3> <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
@ -101,7 +116,7 @@
widgets="vm.widgets" widgets="vm.widgets"
columns="vm.dashboard.configuration.gridSettings.columns" columns="vm.dashboard.configuration.gridSettings.columns"
margins="vm.dashboard.configuration.gridSettings.margins" margins="vm.dashboard.configuration.gridSettings.margins"
device-alias-list="vm.dashboard.configuration.deviceAliases" aliases-info="vm.aliasesInfo"
dashboard-timewindow="vm.dashboardConfiguration.timewindow" dashboard-timewindow="vm.dashboardConfiguration.timewindow"
is-edit="vm.isEdit" is-edit="vm.isEdit"
is-mobile="vm.forceDashboardMobileMode" is-mobile="vm.forceDashboardMobileMode"
@ -139,6 +154,7 @@
<form name="vm.widgetForm" ng-if="vm.isEditingWidget"> <form name="vm.widgetForm" ng-if="vm.isEditingWidget">
<tb-edit-widget <tb-edit-widget
dashboard="vm.dashboard" dashboard="vm.dashboard"
aliases-info="vm.aliasesInfo"
widget="vm.editingWidget" widget="vm.editingWidget"
the-form="vm.widgetForm"> the-form="vm.widgetForm">
</tb-edit-widget> </tb-edit-widget>

107
ui/src/app/dashboard/device-aliases.controller.js

@ -17,25 +17,23 @@ import './device-aliases.scss';
/*@ngInject*/ /*@ngInject*/
export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate, export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
types, config) { types, config) {
var vm = this; var vm = this;
vm.isSingleDevice = config.isSingleDevice; vm.isSingleDeviceAlias = config.isSingleDeviceAlias;
vm.singleDeviceAlias = config.singleDeviceAlias; vm.singleDeviceAlias = config.singleDeviceAlias;
vm.deviceAliases = []; vm.deviceAliases = [];
vm.singleDevice = null;
vm.singleDeviceSearchText = '';
vm.title = config.customTitle ? config.customTitle : 'device.aliases'; vm.title = config.customTitle ? config.customTitle : 'device.aliases';
vm.disableAdd = config.disableAdd; vm.disableAdd = config.disableAdd;
vm.aliasToWidgetsMap = {}; vm.aliasToWidgetsMap = {};
vm.onFilterDeviceChanged = onFilterDeviceChanged;
vm.addAlias = addAlias; vm.addAlias = addAlias;
vm.cancel = cancel;
vm.deviceSearchTextChanged = deviceSearchTextChanged;
vm.deviceChanged = deviceChanged;
vm.fetchDevices = fetchDevices;
vm.removeAlias = removeAlias; vm.removeAlias = removeAlias;
vm.cancel = cancel;
vm.save = save; vm.save = save;
initController(); initController();
@ -80,40 +78,46 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
} }
} }
for (aliasId in config.deviceAliases) { if (vm.isSingleDeviceAlias) {
var alias = config.deviceAliases[aliasId].alias; if (!vm.singleDeviceAlias.deviceFilter || vm.singleDeviceAlias.deviceFilter == null) {
var deviceId = config.deviceAliases[aliasId].deviceId; vm.singleDeviceAlias.deviceFilter = {
var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''}; useFilter: false,
if (deviceId) { deviceNameFilter: '',
fetchAliasDevice(deviceAlias, deviceId); deviceList: [],
};
} }
vm.deviceAliases.push(deviceAlias);
} }
}
function fetchDevices(searchText) {
var pageLink = {limit: 10, textSearch: searchText};
var deferred = $q.defer(); for (aliasId in config.deviceAliases) {
var deviceAlias = config.deviceAliases[aliasId];
deviceService.getTenantDevices(pageLink).then(function success(result) { var alias = deviceAlias.alias;
deferred.resolve(result.data); var deviceFilter;
}, function fail() { if (!deviceAlias.deviceFilter) {
deferred.reject(); deviceFilter = {
}); useFilter: false,
deviceNameFilter: '',
return deferred.promise; deviceList: [],
} };
if (deviceAlias.deviceId) {
function deviceSearchTextChanged() { deviceFilter.deviceList = [deviceAlias.deviceId];
} else {
deviceFilter.deviceList = [];
}
} else {
deviceFilter = deviceAlias.deviceFilter;
}
var result = {id: aliasId, alias: alias, deviceFilter: deviceFilter, changed: true};
vm.deviceAliases.push(result);
}
} }
function deviceChanged(deviceAlias) { function onFilterDeviceChanged(device, deviceAlias) {
if (deviceAlias && deviceAlias.device) { if (deviceAlias) {
if (angular.isDefined(deviceAlias.changed) && !deviceAlias.changed) { if (!deviceAlias.alias || deviceAlias.alias.length == 0) {
deviceAlias.changed = true; deviceAlias.changed = false;
} else { }
deviceAlias.alias = deviceAlias.device.name; if (!deviceAlias.changed && device) {
deviceAlias.alias = device.name;
} }
} }
} }
@ -124,7 +128,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
aliasId = Math.max(vm.deviceAliases[a].id, aliasId); aliasId = Math.max(vm.deviceAliases[a].id, aliasId);
} }
aliasId++; aliasId++;
var deviceAlias = {id: aliasId, alias: '', device: null, searchText: ''}; var deviceAlias = {id: aliasId, alias: '', deviceFilter: {useFilter: false, deviceNameFilter: '', deviceList: []}, changed: false};
vm.deviceAliases.push(deviceAlias); vm.deviceAliases.push(deviceAlias);
} }
@ -150,9 +154,6 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
$mdDialog.show(alert); $mdDialog.show(alert);
} else { } else {
for (var i = index + 1; i < vm.deviceAliases.length; i++) {
vm.deviceAliases[i].changed = false;
}
vm.deviceAliases.splice(index, 1); vm.deviceAliases.splice(index, 1);
if ($scope.theForm) { if ($scope.theForm) {
$scope.theForm.$setDirty(); $scope.theForm.$setDirty();
@ -165,6 +166,15 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
$mdDialog.cancel(); $mdDialog.cancel();
} }
function cleanupDeviceFilter(deviceFilter) {
if (deviceFilter.useFilter) {
deviceFilter.deviceList = [];
} else {
deviceFilter.deviceNameFilter = '';
}
return deviceFilter;
}
function save() { function save() {
var deviceAliases = {}; var deviceAliases = {};
@ -175,9 +185,9 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
var alias; var alias;
var i; var i;
if (vm.isSingleDevice) { if (vm.isSingleDeviceAlias) {
maxAliasId = 0; maxAliasId = 0;
vm.singleDeviceAlias.deviceId = vm.singleDevice.id.id; vm.singleDeviceAlias.deviceFilter = cleanupDeviceFilter(vm.singleDeviceAlias.deviceFilter);
for (i in vm.deviceAliases) { for (i in vm.deviceAliases) {
aliasId = vm.deviceAliases[i].id; aliasId = vm.deviceAliases[i].id;
alias = vm.deviceAliases[i].alias; alias = vm.deviceAliases[i].alias;
@ -195,7 +205,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
alias = vm.deviceAliases[i].alias; alias = vm.deviceAliases[i].alias;
if (!uniqueAliasList[alias]) { if (!uniqueAliasList[alias]) {
uniqueAliasList[alias] = alias; uniqueAliasList[alias] = alias;
deviceAliases[aliasId] = {alias: alias, deviceId: vm.deviceAliases[i].device.id.id}; deviceAliases[aliasId] = {alias: alias, deviceFilter: cleanupDeviceFilter(vm.deviceAliases[i].deviceFilter)};
} else { } else {
valid = false; valid = false;
break; break;
@ -204,7 +214,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
} }
if (valid) { if (valid) {
$scope.theForm.$setPristine(); $scope.theForm.$setPristine();
if (vm.isSingleDevice) { if (vm.isSingleDeviceAlias) {
$mdDialog.hide(vm.singleDeviceAlias); $mdDialog.hide(vm.singleDeviceAlias);
} else { } else {
$mdDialog.hide(deviceAliases); $mdDialog.hide(deviceAliases);
@ -214,11 +224,4 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
} }
} }
function fetchAliasDevice(deviceAlias, deviceId) {
deviceService.getDevice(deviceId).then(function (device) {
deviceAlias.device = device;
deviceAlias.searchText = device.name;
});
}
} }

19
ui/src/app/dashboard/device-aliases.scss

@ -13,20 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.tb-alias {
padding: 10px 0 0 10px;
margin: 5px;
md-input-container { .tb-aliases-dialog {
margin: 0; .md-dialog-content {
padding-bottom: 0px;
} }
md-autocomplete { .tb-alias {
height: 30px; padding: 10px 0 0 10px;
md-autocomplete-wrap { margin: 5px;
height: 30px;
}
input, input:not(.md-input) {
height: 30px;
}
} }
} }

187
ui/src/app/dashboard/device-aliases.tpl.html

@ -15,117 +15,80 @@
limitations under the License. limitations under the License.
--> -->
<md-dialog style="width: 700px;" aria-label="{{ vm.title | translate }}"> <md-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
<form name="theForm" ng-submit="vm.save()"> <form name="theForm" ng-submit="vm.save()">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</h2> <h2>{{ vm.isSingleDeviceAlias ? ('device.configure-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</h2>
<span flex></span> <span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()"> <md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon> <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button> </md-button>
</div> </div>
</md-toolbar> </md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear> <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> <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content> <md-dialog-content>
<div class="md-dialog-content"> <div class="md-dialog-content">
<fieldset ng-disabled="loading"> <fieldset ng-disabled="loading">
<div ng-show="vm.isSingleDevice"> <div ng-show="vm.isSingleDeviceAlias">
<md-autocomplete <tb-device-filter ng-model="vm.singleDeviceAlias.deviceFilter">
ng-required="vm.isSingleDevice" </tb-device-filter>
md-input-name="device_id" </div>
ng-model="vm.singleDevice" <div ng-show="!vm.isSingleDeviceAlias" flex layout="row" layout-align="start center">
md-selected-item="vm.singleDevice" <span flex="5"></span>
md-search-text="vm.singleDeviceSearchText" <div flex layout="row" layout-align="start center"
md-search-text-change="vm.deviceSearchTextChanged(vm.singleDevice)" style="padding: 0 0 0 10px; margin: 5px;">
md-items="item in vm.fetchDevices(vm.singleDeviceSearchText)" <span translate flex="40" style="min-width: 100px;">device.alias</span>
md-item-text="item.name" <span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.devices</span>
md-min-length="0" <span style="min-width: 40px;"></span>
placeholder="{{ 'device.device' | translate }}"> </div>
<md-item-template> </div>
<span md-highlight-text="vm.singleDeviceSearchText" md-highlight-flags="^i">{{item.name}}</span> <div ng-show="!vm.isSingleDeviceAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
</md-item-template> <div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
<md-not-found> <span flex="5">{{$index + 1}}.</span>
<span translate translate-values='{ device: vm.singleDeviceSearchText }'>device.no-devices-matching</span> <div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
</md-not-found> <md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
<div ng-messages="theForm.device_id.$error"> <input required ng-change="deviceAlias.changed=true" name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
<div translate ng-message="required">device.device-required</div> <div ng-messages="aliasForm.alias.$error">
</div> <div translate ng-message="required">device.alias-required</div>
</md-autocomplete> </div>
</div> </md-input-container>
<div ng-show="!vm.isSingleDevice" flex layout="row" layout-align="start center"> <section flex="60" layout="column">
<span flex="5"></span> <tb-device-filter style="padding-left: 10px;"
<div flex layout="row" layout-align="start center" ng-model="deviceAlias.deviceFilter"
style="padding: 0 0 0 10px; margin: 5px;"> on-matching-device-change="vm.onFilterDeviceChanged(device, deviceAlias)">
<span translate flex="40" style="min-width: 100px;">device.alias</span> </tb-device-filter>
<span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.device</span> </section>
<span style="min-width: 40px;"></span> <md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
</div> ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
</div> <md-tooltip md-direction="top">
<div ng-show="!vm.isSingleDevice" style="max-height: 300px; overflow: auto; padding-bottom: 20px;"> {{ 'device.remove-alias' | translate }}
<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index"> </md-tooltip>
<span flex="5">{{$index + 1}}.</span> <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center"> close
<md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block"> </md-icon>
<input required name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias"> </md-button>
<div ng-messages="aliasForm.alias.$error"> </div>
<div translate ng-message="required">device.alias-required</div> </div>
</div> </div>
</md-input-container> <div ng-show="!vm.isSingleDeviceAlias && !vm.disableAdd" style="padding-bottom: 10px;">
<section flex="60" layout="column"> <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
<md-autocomplete flex <md-tooltip md-direction="top">
ng-required="!vm.isSingleDevice" {{ 'device.add-alias' | translate }}
md-input-name="device_id" </md-tooltip>
ng-model="deviceAlias.device" <span translate>action.add</span>
md-selected-item="deviceAlias.device" </md-button>
md-search-text="deviceAlias.searchText" </div>
md-search-text-change="vm.deviceSearchTextChanged(deviceAlias)" </fieldset>
md-selected-item-change="vm.deviceChanged(deviceAlias)" </div>
md-items="item in vm.fetchDevices(deviceAlias.searchText)" </md-dialog-content>
md-item-text="item.name" <md-dialog-actions layout="row">
md-min-length="0" <span flex></span>
placeholder="{{ 'device.device' | translate }}"> <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
<md-item-template> {{ 'action.save' | translate }}
<span md-highlight-text="deviceAlias.searchText" md-highlight-flags="^i">{{item.name}}</span> </md-button>
</md-item-template> <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
<md-not-found> </md-dialog-actions>
<span translate translate-values='{ device: deviceAlias.searchText }'>device.no-devices-matching</span> </form>
</md-not-found>
</md-autocomplete>
<div class="tb-error-messages" ng-messages="aliasForm.device_id.$error">
<div translate ng-message="required" class="tb-error-message">device.device-required</div>
</div>
</section>
<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'device.remove-alias' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
close
</md-icon>
</md-button>
</div>
</div>
</div>
<div ng-show="!vm.isSingleDevice && !vm.disableAdd" style="padding-bottom: 10px;">
<md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'device.add-alias' | translate }}
</md-tooltip>
<span translate>action.add</span>
</md-button>
</div>
</fieldset>
</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">
{{ '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> </md-dialog>

18
ui/src/app/dashboard/edit-widget.directive.js

@ -58,7 +58,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
}); });
scope.fetchDeviceKeys = function (deviceAliasId, query, type) { scope.fetchDeviceKeys = function (deviceAliasId, query, type) {
var deviceAlias = scope.dashboard.configuration.deviceAliases[deviceAliasId]; var deviceAlias = scope.aliasesInfo.deviceAliases[deviceAliasId];
if (deviceAlias && deviceAlias.deviceId) { if (deviceAlias && deviceAlias.deviceId) {
return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type); return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
} else { } else {
@ -69,7 +69,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
scope.createDeviceAlias = function (event, alias) { scope.createDeviceAlias = function (event, alias) {
var deferred = $q.defer(); var deferred = $q.defer();
var singleDeviceAlias = {id: null, alias: alias, deviceId: null}; var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
$mdDialog.show({ $mdDialog.show({
controller: 'DeviceAliasesController', controller: 'DeviceAliasesController',
@ -79,7 +79,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
config: { config: {
deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases), deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
widgets: null, widgets: null,
isSingleDevice: true, isSingleDeviceAlias: true,
singleDeviceAlias: singleDeviceAlias singleDeviceAlias: singleDeviceAlias
} }
}, },
@ -89,8 +89,15 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
targetEvent: event targetEvent: event
}).then(function (singleDeviceAlias) { }).then(function (singleDeviceAlias) {
scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] = scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
{ alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId }; { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
deferred.resolve(singleDeviceAlias); deviceService.processDeviceAliases(scope.dashboard.configuration.deviceAliases).then(
function(resolution) {
if (!resolution.error) {
scope.aliasesInfo = resolution.aliasesInfo;
}
deferred.resolve(singleDeviceAlias);
}
);
}, function () { }, function () {
deferred.reject(); deferred.reject();
}); });
@ -106,6 +113,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
link: linker, link: linker,
scope: { scope: {
dashboard: '=', dashboard: '=',
aliasesInfo: '=',
widget: '=', widget: '=',
theForm: '=' theForm: '='
} }

2
ui/src/app/dashboard/edit-widget.tpl.html

@ -20,7 +20,7 @@
ng-model="widgetConfig" ng-model="widgetConfig"
widget-settings-schema="settingsSchema" widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema" datakey-settings-schema="dataKeySettingsSchema"
device-aliases="dashboard.configuration.deviceAliases" device-aliases="aliasesInfo.deviceAliases"
functions-only="functionsOnly" functions-only="functionsOnly"
fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)" fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
on-create-device-alias="createDeviceAlias(event, alias)" on-create-device-alias="createDeviceAlias(event, alias)"

6
ui/src/app/dashboard/index.js

@ -24,6 +24,7 @@ import thingsboardApiUser from '../api/user.service';
import thingsboardApiDashboard from '../api/dashboard.service'; import thingsboardApiDashboard from '../api/dashboard.service';
import thingsboardApiCustomer from '../api/customer.service'; import thingsboardApiCustomer from '../api/customer.service';
import thingsboardDetailsSidenav from '../components/details-sidenav.directive'; import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
import thingsboardDeviceFilter from '../components/device-filter.directive';
import thingsboardWidgetConfig from '../components/widget-config.directive'; import thingsboardWidgetConfig from '../components/widget-config.directive';
import thingsboardDashboard from '../components/dashboard.directive'; import thingsboardDashboard from '../components/dashboard.directive';
import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive'; import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
@ -36,12 +37,14 @@ import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller'; import DashboardsController from './dashboards.controller';
import DashboardController from './dashboard.controller'; import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller'; import DeviceAliasesController from './device-aliases.controller';
import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
import DashboardSettingsController from './dashboard-settings.controller'; import DashboardSettingsController from './dashboard-settings.controller';
import AssignDashboardToCustomerController from './assign-to-customer.controller'; import AssignDashboardToCustomerController from './assign-to-customer.controller';
import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller'; import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
import AddWidgetController from './add-widget.controller'; import AddWidgetController from './add-widget.controller';
import DashboardDirective from './dashboard.directive'; import DashboardDirective from './dashboard.directive';
import EditWidgetDirective from './edit-widget.directive'; import EditWidgetDirective from './edit-widget.directive';
import AliasesDeviceSelectDirective from './aliases-device-select.directive';
export default angular.module('thingsboard.dashboard', [ export default angular.module('thingsboard.dashboard', [
uiRouter, uiRouter,
@ -55,6 +58,7 @@ export default angular.module('thingsboard.dashboard', [
thingsboardApiDashboard, thingsboardApiDashboard,
thingsboardApiCustomer, thingsboardApiCustomer,
thingsboardDetailsSidenav, thingsboardDetailsSidenav,
thingsboardDeviceFilter,
thingsboardWidgetConfig, thingsboardWidgetConfig,
thingsboardDashboard, thingsboardDashboard,
thingsboardExpandFullscreen, thingsboardExpandFullscreen,
@ -64,10 +68,12 @@ export default angular.module('thingsboard.dashboard', [
.controller('DashboardsController', DashboardsController) .controller('DashboardsController', DashboardsController)
.controller('DashboardController', DashboardController) .controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController) .controller('DeviceAliasesController', DeviceAliasesController)
.controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
.controller('DashboardSettingsController', DashboardSettingsController) .controller('DashboardSettingsController', DashboardSettingsController)
.controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController) .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
.controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController) .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
.controller('AddWidgetController', AddWidgetController) .controller('AddWidgetController', AddWidgetController)
.directive('tbDashboardDetails', DashboardDirective) .directive('tbDashboardDetails', DashboardDirective)
.directive('tbEditWidget', EditWidgetDirective) .directive('tbEditWidget', EditWidgetDirective)
.directive('tbAliasesDeviceSelect', AliasesDeviceSelectDirective)
.name; .name;

8
ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js

@ -45,9 +45,13 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
}; };
aliasesInfo.datasourceAliases[0] = { aliasesInfo.datasourceAliases[0] = {
aliasName: deviceName, aliasName: deviceName,
deviceId: deviceId deviceFilter: {
useFilter: false,
deviceNameFilter: '',
deviceList: [deviceId]
}
}; };
theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1); theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, null, 48, -1, -1);
dashboardService.saveDashboard(theDashboard).then( dashboardService.saveDashboard(theDashboard).then(
function success(dashboard) { function success(dashboard) {
$mdDialog.hide(); $mdDialog.hide();

12
ui/src/app/device/attribute/attribute-table.directive.js

@ -255,8 +255,16 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.firstBundle = true; scope.firstBundle = true;
scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards; scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
scope.deviceAliases = {}; scope.aliasesInfo = {
scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId}; deviceAliases: {
'1': {alias: scope.deviceName, deviceId: scope.deviceId}
},
deviceAliasesInfo: {
'1': [
{name: scope.deviceName, id: scope.deviceId}
]
}
};
var dataKeyType = scope.attributeScope === types.latestTelemetry ? var dataKeyType = scope.attributeScope === types.latestTelemetry ?
types.dataKeyType.timeseries : types.dataKeyType.attribute; types.dataKeyType.timeseries : types.dataKeyType.attribute;

2
ui/src/app/device/attribute/attribute-table.tpl.html

@ -156,7 +156,7 @@
rn-swipe-disabled="true"> rn-swipe-disabled="true">
<li ng-repeat="widgets in widgetsList"> <li ng-repeat="widgets in widgetsList">
<tb-dashboard <tb-dashboard
device-alias-list="deviceAliases" aliases-info="aliasesInfo"
widgets="widgets" widgets="widgets"
get-st-diff="getServerTimeDiff()" get-st-diff="getServerTimeDiff()"
columns="20" columns="20"

1
ui/src/app/device/devices.tpl.html

@ -41,6 +41,7 @@
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}"> <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex <tb-attribute-table flex
device-id="vm.grid.operatingItem().id.id" device-id="vm.grid.operatingItem().id.id"
device-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.latestTelemetry.value}}" default-attribute-scope="{{vm.types.latestTelemetry.value}}"
disable-attribute-scope-selection="true"> disable-attribute-scope-selection="true">
</tb-attribute-table> </tb-attribute-table>

63
ui/src/app/import-export/import-export.service.js

@ -45,7 +45,25 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
exportToPc(prepareExport(widgetItem), name + '.json'); exportToPc(prepareExport(widgetItem), name + '.json');
} }
function importWidget($event, dashboard) { function prepareDeviceAlias(aliasInfo) {
var deviceFilter;
if (aliasInfo.deviceId) {
deviceFilter = {
useFilter: false,
deviceNameFilter: '',
deviceList: [aliasInfo.deviceId]
}
delete aliasInfo.deviceId;
} else {
deviceFilter = aliasInfo.deviceFilter;
}
return {
alias: aliasInfo.aliasName,
deviceFilter: deviceFilter
};
}
function importWidget($event, dashboard, onAliasesUpdate) {
openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then( openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
function success(widgetItem) { function success(widgetItem) {
if (!validateImportedWidget(widgetItem)) { if (!validateImportedWidget(widgetItem)) {
@ -66,20 +84,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
if (datasourceAliases) { if (datasourceAliases) {
for (datasourceIndex in datasourceAliases) { for (datasourceIndex in datasourceAliases) {
datasourceAliasesMap[aliasId] = datasourceIndex; datasourceAliasesMap[aliasId] = datasourceIndex;
deviceAliases[aliasId] = { deviceAliases[aliasId] = prepareDeviceAlias(datasourceAliases[datasourceIndex]);
alias: datasourceAliases[datasourceIndex].aliasName,
deviceId: datasourceAliases[datasourceIndex].deviceId
};
aliasId++; aliasId++;
} }
} }
if (targetDeviceAliases) { if (targetDeviceAliases) {
for (datasourceIndex in targetDeviceAliases) { for (datasourceIndex in targetDeviceAliases) {
targetDeviceAliasesMap[aliasId] = datasourceIndex; targetDeviceAliasesMap[aliasId] = datasourceIndex;
deviceAliases[aliasId] = { deviceAliases[aliasId] = prepareDeviceAlias(targetDeviceAliases[datasourceIndex]);
alias: targetDeviceAliases[datasourceIndex].aliasName,
deviceId: targetDeviceAliases[datasourceIndex].deviceId
};
aliasId++; aliasId++;
} }
} }
@ -97,26 +109,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
var datasourceIndex; var datasourceIndex;
if (datasourceAliasesMap[aliasId]) { if (datasourceAliasesMap[aliasId]) {
datasourceIndex = datasourceAliasesMap[aliasId]; datasourceIndex = datasourceAliasesMap[aliasId];
datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId; datasourceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
} else if (targetDeviceAliasesMap[aliasId]) { } else if (targetDeviceAliasesMap[aliasId]) {
datasourceIndex = targetDeviceAliasesMap[aliasId]; datasourceIndex = targetDeviceAliasesMap[aliasId];
targetDeviceAliases[datasourceIndex].deviceId = deviceAlias.deviceId; targetDeviceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
} }
} }
addImportedWidget(dashboard, widget, aliasesInfo, originalColumns); addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
}, },
function fail() {} function fail() {}
); );
} else { } else {
addImportedWidget(dashboard, widget, aliasesInfo, originalColumns); addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
} }
} }
); );
} else { } else {
addImportedWidget(dashboard, widget, aliasesInfo, originalColumns); addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
} }
} else { } else {
addImportedWidget(dashboard, widget, aliasesInfo, originalColumns); addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
} }
} }
}, },
@ -140,8 +152,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true; return true;
} }
function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) { function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1); itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
} }
// Dashboard functions // Dashboard functions
@ -248,19 +260,18 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) { function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
var aliasId = aliasIds[index]; var aliasId = aliasIds[index];
var deviceAlias = deviceAliases[aliasId]; var deviceAlias = deviceAliases[aliasId];
if (deviceAlias.deviceId) { deviceService.checkDeviceAlias(deviceAlias).then(
deviceService.getDevice(deviceAlias.deviceId, true).then( function(result) {
function success() { if (result) {
checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred); checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
}, } else {
function fail() {
var missingDeviceAlias = angular.copy(deviceAlias); var missingDeviceAlias = angular.copy(deviceAlias);
missingDeviceAlias.deviceId = null; missingDeviceAlias.deviceFilter = null;
missingDeviceAliases[aliasId] = missingDeviceAlias; missingDeviceAliases[aliasId] = missingDeviceAlias;
checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred); checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
} }
); }
} );
} }
function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) { function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
@ -274,7 +285,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
deviceAliases: missingDeviceAliases, deviceAliases: missingDeviceAliases,
widgets: widgets, widgets: widgets,
isSingleWidget: isSingleWidget, isSingleWidget: isSingleWidget,
isSingleDevice: false, isSingleDeviceAlias: false,
singleDeviceAlias: null, singleDeviceAlias: null,
customTitle: customTitle, customTitle: customTitle,
disableAdd: true disableAdd: true

19
ui/src/app/locale/locale.constant.js

@ -254,14 +254,19 @@ export default angular.module('thingsboard.locale', [])
"create-new-dashboard": "Create new dashboard", "create-new-dashboard": "Create new dashboard",
"dashboard-file": "Dashboard file", "dashboard-file": "Dashboard file",
"invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.", "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
"dashboard-import-missing-aliases-title": "Select missing devices for dashboard aliases", "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard",
"create-new-widget": "Create new widget", "create-new-widget": "Create new widget",
"import-widget": "Import widget", "import-widget": "Import widget",
"widget-file": "Widget file", "widget-file": "Widget file",
"invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.", "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
"widget-import-missing-aliases-title": "Select missing devices used by widget", "widget-import-missing-aliases-title": "Configure aliases used by imported widget",
"open-toolbar": "Open dashboard toolbar", "open-toolbar": "Open dashboard toolbar",
"close-toolbar": "Close toolbar" "close-toolbar": "Close toolbar",
"configuration-error": "Configuration error",
"alias-resolution-error-title": "Dashboard aliases configuration error",
"invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" +
"Please contact your administrator in order to resolve this issue.",
"select-devices": "Select devices"
}, },
"datakey": { "datakey": {
"settings": "Settings", "settings": "Settings",
@ -301,12 +306,18 @@ export default angular.module('thingsboard.locale', [])
"create-new-alias": "Create a new one!", "create-new-alias": "Create a new one!",
"create-new-key": "Create a new one!", "create-new-key": "Create a new one!",
"duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.", "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
"select-device-for-alias": "Select device for '{{alias}}' alias", "configure-alias": "Configure '{{alias}}' alias",
"no-devices-matching": "No devices matching '{{device}}' were found.", "no-devices-matching": "No devices matching '{{device}}' were found.",
"alias": "Alias", "alias": "Alias",
"alias-required": "Device alias is required.", "alias-required": "Device alias is required.",
"remove-alias": "Remove device alias", "remove-alias": "Remove device alias",
"add-alias": "Add device alias", "add-alias": "Add device alias",
"name-starts-with": "Name starts with",
"device-list": "Device list",
"use-device-name-filter": "Use filter",
"device-list-empty": "No devices selected.",
"device-name-filter-required": "Device name filter is required.",
"device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.",
"add": "Add Device", "add": "Add Device",
"assign-to-customer": "Assign to customer", "assign-to-customer": "Assign to customer",
"assign-device-to-customer": "Assign Device(s) To Customer", "assign-device-to-customer": "Assign Device(s) To Customer",

61
ui/src/app/services/item-buffer.service.js

@ -43,19 +43,38 @@ function ItemBuffer(bufferStore, types) {
datasourceAliases: { datasourceAliases: {
datasourceIndex: { datasourceIndex: {
aliasName: "...", aliasName: "...",
deviceId: "..." deviceFilter: "..."
} }
} }
targetDeviceAliases: { targetDeviceAliases: {
targetDeviceAliasIndex: { targetDeviceAliasIndex: {
aliasName: "...", aliasName: "...",
deviceId: "..." deviceFilter: "..."
} }
} }
.... ....
} }
**/ **/
function getDeviceFilter(alias) {
if (alias.deviceId) {
return {
useFilter: false,
deviceNameFilter: '',
deviceList: [alias.deviceId]
};
} else {
return alias.deviceFilter;
}
}
function prepareAliasInfo(deviceAlias) {
return {
aliasName: deviceAlias.alias,
deviceFilter: getDeviceFilter(deviceAlias)
};
}
function prepareWidgetItem(dashboard, widget) { function prepareWidgetItem(dashboard, widget) {
var aliasesInfo = { var aliasesInfo = {
datasourceAliases: {}, datasourceAliases: {},
@ -75,10 +94,7 @@ function ItemBuffer(bufferStore, types) {
if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) { if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId]; deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
if (deviceAlias) { if (deviceAlias) {
aliasesInfo.datasourceAliases[i] = { aliasesInfo.datasourceAliases[i] = prepareAliasInfo(deviceAlias);
aliasName: deviceAlias.alias,
deviceId: deviceAlias.deviceId
}
} }
} }
} }
@ -89,10 +105,7 @@ function ItemBuffer(bufferStore, types) {
if (targetDeviceAliasId) { if (targetDeviceAliasId) {
deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId]; deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
if (deviceAlias) { if (deviceAlias) {
aliasesInfo.targetDeviceAliases[i] = { aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(deviceAlias);
aliasName: deviceAlias.alias,
deviceId: deviceAlias.deviceId
}
} }
} }
} }
@ -114,7 +127,7 @@ function ItemBuffer(bufferStore, types) {
return bufferStore.get(WIDGET_ITEM); return bufferStore.get(WIDGET_ITEM);
} }
function pasteWidget(targetDashboard, position) { function pasteWidget(targetDashboard, position, onAliasesUpdate) {
var widgetItemJson = bufferStore.get(WIDGET_ITEM); var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) { if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson); var widgetItem = angular.fromJson(widgetItemJson);
@ -127,11 +140,11 @@ function ItemBuffer(bufferStore, types) {
targetRow = position.row; targetRow = position.row;
targetColumn = position.column; targetColumn = position.column;
} }
addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn); addWidgetToDashboard(targetDashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, targetRow, targetColumn);
} }
} }
function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) { function addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, row, column) {
var theDashboard; var theDashboard;
if (dashboard) { if (dashboard) {
theDashboard = dashboard; theDashboard = dashboard;
@ -144,7 +157,7 @@ function ItemBuffer(bufferStore, types) {
if (!theDashboard.configuration.deviceAliases) { if (!theDashboard.configuration.deviceAliases) {
theDashboard.configuration.deviceAliases = {}; theDashboard.configuration.deviceAliases = {};
} }
updateAliases(theDashboard, widget, aliasesInfo); var newDeviceAliases = updateAliases(theDashboard, widget, aliasesInfo);
if (!theDashboard.configuration.widgets) { if (!theDashboard.configuration.widgets) {
theDashboard.configuration.widgets = []; theDashboard.configuration.widgets = [];
@ -174,12 +187,19 @@ function ItemBuffer(bufferStore, types) {
widget.row = row; widget.row = row;
widget.col = 0; widget.col = 0;
} }
var aliasesUpdated = !angular.equals(newDeviceAliases, theDashboard.configuration.deviceAliases);
if (aliasesUpdated) {
theDashboard.configuration.deviceAliases = newDeviceAliases;
if (onAliasesUpdate) {
onAliasesUpdate();
}
}
theDashboard.configuration.widgets.push(widget); theDashboard.configuration.widgets.push(widget);
return theDashboard; return theDashboard;
} }
function updateAliases(dashboard, widget, aliasesInfo) { function updateAliases(dashboard, widget, aliasesInfo) {
var deviceAliases = dashboard.configuration.deviceAliases; var deviceAliases = angular.copy(dashboard.configuration.deviceAliases);
var aliasInfo; var aliasInfo;
var newAliasId; var newAliasId;
for (var datasourceIndex in aliasesInfo.datasourceAliases) { for (var datasourceIndex in aliasesInfo.datasourceAliases) {
@ -192,12 +212,19 @@ function ItemBuffer(bufferStore, types) {
newAliasId = getDeviceAliasId(deviceAliases, aliasInfo); newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId; widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
} }
return deviceAliases;
}
function isDeviceFiltersEqual(alias1, alias2) {
var filter1 = getDeviceFilter(alias1);
var filter2 = getDeviceFilter(alias2);
return angular.equals(filter1, filter2);
} }
function getDeviceAliasId(deviceAliases, aliasInfo) { function getDeviceAliasId(deviceAliases, aliasInfo) {
var newAliasId; var newAliasId;
for (var aliasId in deviceAliases) { for (var aliasId in deviceAliases) {
if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) { if (isDeviceFiltersEqual(deviceAliases[aliasId], aliasInfo)) {
newAliasId = aliasId; newAliasId = aliasId;
break; break;
} }
@ -209,7 +236,7 @@ function ItemBuffer(bufferStore, types) {
newAliasId = Math.max(newAliasId, aliasId); newAliasId = Math.max(newAliasId, aliasId);
} }
newAliasId++; newAliasId++;
deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId}; deviceAliases[newAliasId] = {alias: newAliasName, deviceFilter: aliasInfo.deviceFilter};
} }
return newAliasId; return newAliasId;
} }

9
ui/src/app/widget/lib/flot-widget.js

@ -35,7 +35,6 @@ export default class TbFlot {
var colors = []; var colors = [];
for (var i in ctx.data) { for (var i in ctx.data) {
var series = ctx.data[i]; var series = ctx.data[i];
series.label = series.dataKey.label;
colors.push(series.dataKey.color); colors.push(series.dataKey.color);
var keySettings = series.dataKey.settings; var keySettings = series.dataKey.settings;
@ -130,7 +129,7 @@ export default class TbFlot {
if (this.chartType === 'pie') { if (this.chartType === 'pie') {
ctx.tooltipFormatter = function(item) { ctx.tooltipFormatter = function(item) {
var divElement = seriesInfoDiv(item.series.label, item.series.color, var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
item.datapoint[1][0][1], tbFlot.ctx.trackUnits, tbFlot.ctx.trackDecimals, true, item.series.percent); item.datapoint[1][0][1], tbFlot.ctx.trackUnits, tbFlot.ctx.trackDecimals, true, item.series.percent);
return divElement.prop('outerHTML'); return divElement.prop('outerHTML');
}; };
@ -313,7 +312,7 @@ export default class TbFlot {
if (options.series.pie.label.show) { if (options.series.pie.label.show) {
options.series.pie.label.formatter = function (label, series) { options.series.pie.label.formatter = function (label, series) {
return "<div class='pie-label'>" + label + "<br/>" + Math.round(series.percent) + "%</div>"; return "<div class='pie-label'>" + series.dataKey.label + "<br/>" + Math.round(series.percent) + "%</div>";
} }
options.series.pie.label.radius = 3/4; options.series.pie.label.radius = 3/4;
options.series.pie.label.background = { options.series.pie.label.background = {
@ -346,7 +345,7 @@ export default class TbFlot {
} }
update() { update() {
if (!this.isMouseInteraction) { if (!this.isMouseInteraction && this.ctx.plot) {
if (this.chartType === 'line' || this.chartType === 'bar') { if (this.chartType === 'line' || this.chartType === 'bar') {
this.options.xaxis.min = this.ctx.timeWindow.minTime; this.options.xaxis.min = this.ctx.timeWindow.minTime;
this.options.xaxis.max = this.ctx.timeWindow.maxTime; this.options.xaxis.max = this.ctx.timeWindow.maxTime;
@ -918,7 +917,7 @@ export default class TbFlot {
value: value, value: value,
hoverIndex: hoverIndex, hoverIndex: hoverIndex,
color: series.dataKey.color, color: series.dataKey.color,
label: series.label, label: series.dataKey.label,
time: pointTime, time: pointTime,
distance: hoverDistance, distance: hoverDistance,
index: i index: i

Loading…
Cancel
Save