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) {
return UUID.fromString(id);
}

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
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.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -29,6 +31,11 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api")
@ -189,4 +196,30 @@ public class DeviceController extends BaseController {
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;
}
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) {
T object = 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;
}
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.UUID;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao;
@ -46,7 +47,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
* @return the list of device objects
*/
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.
*
@ -57,6 +67,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
*/
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.
*

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

@ -16,12 +16,14 @@
package org.thingsboard.server.dao.device;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
import static org.thingsboard.server.dao.model.ModelConstants.*;
import java.util.*;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device;
@ -61,6 +63,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
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
public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink 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;
}
@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
public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
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.TextPageLink;
import java.util.List;
import java.util.Optional;
public interface DeviceService {
@ -43,9 +44,13 @@ public interface DeviceService {
TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
void deleteDevicesByTenantId(TenantId tenantId);
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);
}

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.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.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@ -143,6 +145,16 @@ public class DeviceServiceImpl implements DeviceService {
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
public void deleteDevicesByTenantId(TenantId tenantId) {
log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
@ -161,6 +173,17 @@ public class DeviceServiceImpl implements DeviceService {
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
public void unassignCustomerDevices(TenantId tenantId, CustomerId 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.dao.exception.IncorrectParameterException;
import java.util.List;
import java.util.UUID;
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
* <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;
/*@ngInject*/
function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
var deviceAttributesSubscriptionMap = {};
@ -30,6 +30,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
deleteDevice: deleteDevice,
getCustomerDevices: getCustomerDevices,
getDevice: getDevice,
getDevices: getDevices,
processDeviceAliases: processDeviceAliases,
checkDeviceAlias: checkDeviceAlias,
getDeviceCredentials: getDeviceCredentials,
getDeviceKeys: getDeviceKeys,
getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@ -99,6 +102,200 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
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) {
var deferred = $q.defer();
var url = '/api/device';

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

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

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

@ -90,7 +90,7 @@
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect,
widget: widget,
deviceAliasList: vm.deviceAliasList,
aliasesInfo: vm.aliasesInfo,
isEdit: vm.isEdit,
stDiff: vm.stDiff,
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.legendConfig.position === types.position.top.value;
scope.$on('legendDataUpdated', function () {
scope.$digest();
scope.$on('legendDataUpdated', function (event, apply) {
if (apply) {
scope.$digest();
}
});
scope.toggleHideData = function(index) {
@ -62,15 +64,14 @@ function Legend($compile, $templateCache, types) {
data: []
key: {
label: '',
color: ''
dataIndex: 0
dataKey: dataKey,
dataIndex: 0
}
data: {
min: null,
max: null,
avg: null,
total: null
min: null,
max: null,
avg: null,
total: null
}
};*/

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

@ -27,11 +27,11 @@
</thead>
<tbody>
<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"
ng-click="toggleHideData(legendKey.dataIndex)"
ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }">
{{ legendKey.label }}
{{ legendKey.dataKey.label }}
</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>

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

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

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

@ -21,7 +21,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
/*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
dashboardTimewindowApi, widget, deviceAliasList, widgetType) {
dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
var vm = this;
@ -45,6 +45,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var subscriptionTimewindow = null;
var dataUpdateCaf = null;
var varsRegex = /\$\{([^\}]*)\}/g;
/*
* data = array of datasourceData
* datasourceData = {
@ -68,7 +70,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
settings: widget.config.settings,
units: widget.config.units || '',
decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
datasources: widget.config.datasources,
datasources: angular.copy(widget.config.datasources),
data: [],
hiddenData: [],
timeWindow: {
@ -320,10 +322,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.legendConfig.showTotal === true);
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
for (var i in widget.config.datasources) {
var datasource = angular.copy(widget.config.datasources[i]);
for (var i in widgetContext.datasources) {
var datasource = widgetContext.datasources[i];
for (var a in datasource.dataKeys) {
var dataKey = datasource.dataKeys[a];
dataKey.pattern = angular.copy(dataKey.label);
var datasourceData = {
datasource: datasource,
dataKey: dataKey,
@ -333,8 +336,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
widgetContext.hiddenData.push({data: []});
if ($scope.displayLegend) {
var legendKey = {
label: dataKey.label,
color: dataKey.color,
dataKey: dataKey,
dataIndex: Number(i) + Number(a)
};
$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) {
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
if (deviceAliasList[targetDeviceAliasId]) {
targetDeviceId = deviceAliasList[targetDeviceAliasId].deviceId;
if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
}
}
if (targetDeviceId) {
@ -402,13 +404,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
onMobileModeChanged(newIsMobile);
});
$scope.$on('deviceAliasListChanged', function (event, newDeviceAliasList) {
deviceAliasList = newDeviceAliasList;
$scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {
aliasesInfo = newAliasesInfo;
if (widget.type === types.widgetType.rpc.value) {
if (targetDeviceAliasId) {
var deviceId = null;
if (deviceAliasList[targetDeviceAliasId]) {
deviceId = deviceAliasList[targetDeviceAliasId].deviceId;
if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
}
if (!angular.equals(deviceId, targetDeviceId)) {
targetDeviceId = deviceId;
@ -609,7 +611,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
currentData.data = sourceData.data;
onDataUpdated();
if ($scope.caulculateLegendData) {
updateLegend(datasourceIndex + dataKeyIndex, sourceData.data);
updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, 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];
if ($scope.legendConfig.showMin) {
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) {
legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);
}
$scope.$broadcast('legendDataUpdated');
$scope.$broadcast('legendDataUpdated', apply !== false);
}
function isNumeric(val) {
@ -707,9 +709,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var deviceId = null;
var aliasName = null;
if (listener.datasource.type === types.datasourceType.device) {
if (deviceAliasList[listener.datasource.deviceAliasId]) {
deviceId = deviceAliasList[listener.datasource.deviceAliasId].deviceId;
aliasName = deviceAliasList[listener.datasource.deviceAliasId].alias;
if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
}
if (!angular.equals(deviceId, listener.deviceId) ||
!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() {
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
notifyDataLoading();
@ -767,13 +786,25 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
var index = 0;
for (var i in widget.config.datasources) {
var datasource = widget.config.datasources[i];
for (var i in widgetContext.datasources) {
var datasource = widgetContext.datasources[i];
var deviceId = null;
if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
if (deviceAliasList[datasource.deviceAliasId]) {
deviceId = deviceAliasList[datasource.deviceAliasId].deviceId;
datasource.name = deviceAliasList[datasource.deviceAliasId].alias;
if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
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 {
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 */
/*@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;
vm.dashboard = dashboard;
vm.aliasesInfo = aliasesInfo;
vm.widget = widget;
vm.widgetInfo = widgetInfo;
@ -78,19 +79,19 @@ export default function AddWidgetController($scope, widgetService, deviceService
}
function cancel () {
$mdDialog.cancel();
$mdDialog.cancel({aliasesInfo: vm.aliasesInfo});
}
function add () {
if ($scope.theForm.$valid) {
$scope.theForm.$setPristine();
vm.widget.config = vm.widgetConfig;
$mdDialog.hide(vm.widget);
$mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
}
}
function fetchDeviceKeys (deviceAliasId, query, type) {
var deviceAlias = vm.dashboard.configuration.deviceAliases[deviceAliasId];
var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId];
if (deviceAlias && deviceAlias.deviceId) {
return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
} else {
@ -101,7 +102,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
function createDeviceAlias (event, alias) {
var deferred = $q.defer();
var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
$mdDialog.show({
controller: 'DeviceAliasesController',
@ -111,7 +112,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
config: {
deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
widgets: null,
isSingleDevice: true,
isSingleDeviceAlias: true,
singleDeviceAlias: singleDeviceAlias
}
},
@ -121,8 +122,15 @@ export default function AddWidgetController($scope, widgetService, deviceService
targetEvent: event
}).then(function (singleDeviceAlias) {
vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
{ alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
deferred.resolve(singleDeviceAlias);
{ alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then(
function(resolution) {
if (!resolution.error) {
vm.aliasesInfo = resolution.aliasesInfo;
}
deferred.resolve(singleDeviceAlias);
}
);
}, function () {
deferred.reject();
});

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

@ -37,7 +37,7 @@
ng-model="vm.widgetConfig"
widget-settings-schema="vm.settingsSchema"
datakey-settings-schema="vm.dataKeySettingsSchema"
device-aliases="vm.dashboard.configuration.deviceAliases"
device-aliases="vm.aliasesInfo.deviceAliases"
functions-only="vm.functionsOnly"
fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
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*/
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) {
var user = userService.getCurrentUser();
@ -82,6 +82,8 @@ export default function DashboardController(types, widgetService, userService,
vm.loadDashboard = loadDashboard;
vm.getServerTimeDiff = getServerTimeDiff;
vm.noData = noData;
vm.dashboardConfigurationError = dashboardConfigurationError;
vm.showDashboardToolbar = showDashboardToolbar;
vm.onAddWidgetClosed = onAddWidgetClosed;
vm.onEditWidgetClosed = onEditWidgetClosed;
vm.openDeviceAliases = openDeviceAliases;
@ -212,9 +214,21 @@ export default function DashboardController(types, widgetService, userService,
if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
}
vm.dashboardConfiguration = vm.dashboard.configuration;
vm.widgets = vm.dashboard.configuration.widgets;
deferred.resolve();
deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
.then(
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) {
deferred.reject(e);
});
@ -245,7 +259,15 @@ export default function DashboardController(types, widgetService, userService,
}
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) {
@ -257,7 +279,7 @@ export default function DashboardController(types, widgetService, userService,
config: {
deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
widgets: vm.widgets,
isSingleDevice: false,
isSingleDeviceAlias: false,
singleDeviceAlias: null
}
},
@ -267,6 +289,7 @@ export default function DashboardController(types, widgetService, userService,
targetEvent: $event
}).then(function (deviceAliases) {
vm.dashboard.configuration.deviceAliases = deviceAliases;
deviceAliasesUpdated();
}, function () {
});
}
@ -331,7 +354,7 @@ export default function DashboardController(types, widgetService, userService,
function importWidget($event) {
$event.stopPropagation();
importExport.importWidget($event, vm.dashboard);
importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated);
}
function widgetMouseDown($event, widget) {
@ -433,7 +456,7 @@ export default function DashboardController(types, widgetService, userService,
function pasteWidget($event) {
var pos = vm.dashboardContainer.getEventGridPosition($event);
itembuffer.pasteWidget(vm.dashboard, pos);
itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated);
}
function prepareWidgetContextMenu() {
@ -570,7 +593,7 @@ export default function DashboardController(types, widgetService, userService,
controller: 'AddWidgetController',
controllerAs: 'vm',
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),
fullscreen: true,
skipHide: true,
@ -579,7 +602,9 @@ export default function DashboardController(types, widgetService, userService,
var w = angular.element($window);
w.triggerHandler('resize');
}
}).then(function (widget) {
}).then(function (result) {
var widget = result.widget;
vm.aliasesInfo = result.aliasesInfo;
var columns = 24;
if (vm.dashboard.configuration.gridSettings && 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;
}
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.widgets = vm.dashboard.configuration.widgets;
vm.dashboardConfiguration = vm.dashboard.configuration;
deviceAliasesUpdated();
}
}
}
@ -648,6 +675,31 @@ export default function DashboardController(types, widgetService, userService,
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() {
if (vm.widgetEditMode) {
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;
height: 36px;
md-fab-actions {
font-size: 16px;
margin-top: 0px;
.close-action {
margin-right: -18px;
}
tb-timewindow {
font-size: 16px;
}
}
}
}

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

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

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

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

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

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

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

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

@ -20,7 +20,7 @@
ng-model="widgetConfig"
widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema"
device-aliases="dashboard.configuration.deviceAliases"
device-aliases="aliasesInfo.deviceAliases"
functions-only="functionsOnly"
fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
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 thingsboardApiCustomer from '../api/customer.service';
import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
import thingsboardDeviceFilter from '../components/device-filter.directive';
import thingsboardWidgetConfig from '../components/widget-config.directive';
import thingsboardDashboard from '../components/dashboard.directive';
import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
@ -36,12 +37,14 @@ import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller';
import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
import DashboardSettingsController from './dashboard-settings.controller';
import AssignDashboardToCustomerController from './assign-to-customer.controller';
import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
import AddWidgetController from './add-widget.controller';
import DashboardDirective from './dashboard.directive';
import EditWidgetDirective from './edit-widget.directive';
import AliasesDeviceSelectDirective from './aliases-device-select.directive';
export default angular.module('thingsboard.dashboard', [
uiRouter,
@ -55,6 +58,7 @@ export default angular.module('thingsboard.dashboard', [
thingsboardApiDashboard,
thingsboardApiCustomer,
thingsboardDetailsSidenav,
thingsboardDeviceFilter,
thingsboardWidgetConfig,
thingsboardDashboard,
thingsboardExpandFullscreen,
@ -64,10 +68,12 @@ export default angular.module('thingsboard.dashboard', [
.controller('DashboardsController', DashboardsController)
.controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController)
.controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
.controller('DashboardSettingsController', DashboardSettingsController)
.controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
.controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
.controller('AddWidgetController', AddWidgetController)
.directive('tbDashboardDetails', DashboardDirective)
.directive('tbEditWidget', EditWidgetDirective)
.directive('tbAliasesDeviceSelect', AliasesDeviceSelectDirective)
.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] = {
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(
function success(dashboard) {
$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.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
scope.deviceAliases = {};
scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
scope.aliasesInfo = {
deviceAliases: {
'1': {alias: scope.deviceName, deviceId: scope.deviceId}
},
deviceAliasesInfo: {
'1': [
{name: scope.deviceName, id: scope.deviceId}
]
}
};
var dataKeyType = scope.attributeScope === types.latestTelemetry ?
types.dataKeyType.timeseries : types.dataKeyType.attribute;

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

@ -156,7 +156,7 @@
rn-swipe-disabled="true">
<li ng-repeat="widgets in widgetsList">
<tb-dashboard
device-alias-list="deviceAliases"
aliases-info="aliasesInfo"
widgets="widgets"
get-st-diff="getServerTimeDiff()"
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 }}">
<tb-attribute-table flex
device-id="vm.grid.operatingItem().id.id"
device-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.latestTelemetry.value}}"
disable-attribute-scope-selection="true">
</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');
}
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(
function success(widgetItem) {
if (!validateImportedWidget(widgetItem)) {
@ -66,20 +84,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
if (datasourceAliases) {
for (datasourceIndex in datasourceAliases) {
datasourceAliasesMap[aliasId] = datasourceIndex;
deviceAliases[aliasId] = {
alias: datasourceAliases[datasourceIndex].aliasName,
deviceId: datasourceAliases[datasourceIndex].deviceId
};
deviceAliases[aliasId] = prepareDeviceAlias(datasourceAliases[datasourceIndex]);
aliasId++;
}
}
if (targetDeviceAliases) {
for (datasourceIndex in targetDeviceAliases) {
targetDeviceAliasesMap[aliasId] = datasourceIndex;
deviceAliases[aliasId] = {
alias: targetDeviceAliases[datasourceIndex].aliasName,
deviceId: targetDeviceAliases[datasourceIndex].deviceId
};
deviceAliases[aliasId] = prepareDeviceAlias(targetDeviceAliases[datasourceIndex]);
aliasId++;
}
}
@ -97,26 +109,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
var datasourceIndex;
if (datasourceAliasesMap[aliasId]) {
datasourceIndex = datasourceAliasesMap[aliasId];
datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
datasourceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
} else if (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() {}
);
} else {
addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
}
}
);
} else {
addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
}
} 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;
}
function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) {
itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1);
function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
}
// Dashboard functions
@ -248,19 +260,18 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
var aliasId = aliasIds[index];
var deviceAlias = deviceAliases[aliasId];
if (deviceAlias.deviceId) {
deviceService.getDevice(deviceAlias.deviceId, true).then(
function success() {
deviceService.checkDeviceAlias(deviceAlias).then(
function(result) {
if (result) {
checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
},
function fail() {
} else {
var missingDeviceAlias = angular.copy(deviceAlias);
missingDeviceAlias.deviceId = null;
missingDeviceAlias.deviceFilter = null;
missingDeviceAliases[aliasId] = missingDeviceAlias;
checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
}
);
}
}
);
}
function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
@ -274,7 +285,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
deviceAliases: missingDeviceAliases,
widgets: widgets,
isSingleWidget: isSingleWidget,
isSingleDevice: false,
isSingleDeviceAlias: false,
singleDeviceAlias: null,
customTitle: customTitle,
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",
"dashboard-file": "Dashboard file",
"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",
"import-widget": "Import widget",
"widget-file": "Widget file",
"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",
"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": {
"settings": "Settings",
@ -301,12 +306,18 @@ export default angular.module('thingsboard.locale', [])
"create-new-alias": "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.",
"select-device-for-alias": "Select device for '{{alias}}' alias",
"configure-alias": "Configure '{{alias}}' alias",
"no-devices-matching": "No devices matching '{{device}}' were found.",
"alias": "Alias",
"alias-required": "Device alias is required.",
"remove-alias": "Remove 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",
"assign-to-customer": "Assign 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: {
datasourceIndex: {
aliasName: "...",
deviceId: "..."
deviceFilter: "..."
}
}
targetDeviceAliases: {
targetDeviceAliasIndex: {
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) {
var aliasesInfo = {
datasourceAliases: {},
@ -75,10 +94,7 @@ function ItemBuffer(bufferStore, types) {
if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
if (deviceAlias) {
aliasesInfo.datasourceAliases[i] = {
aliasName: deviceAlias.alias,
deviceId: deviceAlias.deviceId
}
aliasesInfo.datasourceAliases[i] = prepareAliasInfo(deviceAlias);
}
}
}
@ -89,10 +105,7 @@ function ItemBuffer(bufferStore, types) {
if (targetDeviceAliasId) {
deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
if (deviceAlias) {
aliasesInfo.targetDeviceAliases[i] = {
aliasName: deviceAlias.alias,
deviceId: deviceAlias.deviceId
}
aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(deviceAlias);
}
}
}
@ -114,7 +127,7 @@ function ItemBuffer(bufferStore, types) {
return bufferStore.get(WIDGET_ITEM);
}
function pasteWidget(targetDashboard, position) {
function pasteWidget(targetDashboard, position, onAliasesUpdate) {
var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson);
@ -127,11 +140,11 @@ function ItemBuffer(bufferStore, types) {
targetRow = position.row;
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;
if (dashboard) {
theDashboard = dashboard;
@ -144,7 +157,7 @@ function ItemBuffer(bufferStore, types) {
if (!theDashboard.configuration.deviceAliases) {
theDashboard.configuration.deviceAliases = {};
}
updateAliases(theDashboard, widget, aliasesInfo);
var newDeviceAliases = updateAliases(theDashboard, widget, aliasesInfo);
if (!theDashboard.configuration.widgets) {
theDashboard.configuration.widgets = [];
@ -174,12 +187,19 @@ function ItemBuffer(bufferStore, types) {
widget.row = row;
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);
return theDashboard;
}
function updateAliases(dashboard, widget, aliasesInfo) {
var deviceAliases = dashboard.configuration.deviceAliases;
var deviceAliases = angular.copy(dashboard.configuration.deviceAliases);
var aliasInfo;
var newAliasId;
for (var datasourceIndex in aliasesInfo.datasourceAliases) {
@ -192,12 +212,19 @@ function ItemBuffer(bufferStore, types) {
newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
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) {
var newAliasId;
for (var aliasId in deviceAliases) {
if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) {
if (isDeviceFiltersEqual(deviceAliases[aliasId], aliasInfo)) {
newAliasId = aliasId;
break;
}
@ -209,7 +236,7 @@ function ItemBuffer(bufferStore, types) {
newAliasId = Math.max(newAliasId, aliasId);
}
newAliasId++;
deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId};
deviceAliases[newAliasId] = {alias: newAliasName, deviceFilter: aliasInfo.deviceFilter};
}
return newAliasId;
}

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

@ -35,7 +35,6 @@ export default class TbFlot {
var colors = [];
for (var i in ctx.data) {
var series = ctx.data[i];
series.label = series.dataKey.label;
colors.push(series.dataKey.color);
var keySettings = series.dataKey.settings;
@ -130,7 +129,7 @@ export default class TbFlot {
if (this.chartType === 'pie') {
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);
return divElement.prop('outerHTML');
};
@ -313,7 +312,7 @@ export default class TbFlot {
if (options.series.pie.label.show) {
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.background = {
@ -346,7 +345,7 @@ export default class TbFlot {
}
update() {
if (!this.isMouseInteraction) {
if (!this.isMouseInteraction && this.ctx.plot) {
if (this.chartType === 'line' || this.chartType === 'bar') {
this.options.xaxis.min = this.ctx.timeWindow.minTime;
this.options.xaxis.max = this.ctx.timeWindow.maxTime;
@ -918,7 +917,7 @@ export default class TbFlot {
value: value,
hoverIndex: hoverIndex,
color: series.dataKey.color,
label: series.label,
label: series.dataKey.label,
time: pointTime,
distance: hoverDistance,
index: i

Loading…
Cancel
Save