Browse Source

Merge github.com:thingsboard/thingsboard

pull/30/head
volodymyr-babak 10 years ago
parent
commit
13a8232d7c
  1. 135
      application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
  2. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  3. 14
      common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
  4. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
  5. 7
      dao/src/main/resources/system-data.cql
  6. 17
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
  7. 2
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
  8. 4
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
  9. 9
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
  10. 5
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
  11. 21
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
  12. 83
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
  13. 10
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
  14. 15
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
  15. 16
      extensions-core/src/main/proto/telemetry.proto
  16. 18
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
  17. 2
      transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
  18. 25
      transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  19. 11
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  20. 3
      ui/src/app/api/device.service.js
  21. 2
      ui/src/app/app.js
  22. 108
      ui/src/app/components/dashboard.directive.js
  23. 2
      ui/src/app/components/dashboard.scss
  24. 144
      ui/src/app/components/dashboard.tpl.html
  25. 16
      ui/src/app/components/datasource-device.scss
  26. 58
      ui/src/app/components/datasource-device.tpl.html
  27. 9
      ui/src/app/components/datasource-func.scss
  28. 33
      ui/src/app/components/datasource-func.tpl.html
  29. 12
      ui/src/app/components/datasource.scss
  30. 8
      ui/src/app/components/datasource.tpl.html
  31. 13
      ui/src/app/components/details-sidenav.scss
  32. 5
      ui/src/app/components/details-sidenav.tpl.html
  33. 40
      ui/src/app/components/keyboard-shortcut.filter.js
  34. 51
      ui/src/app/components/mousepoint-menu.directive.js
  35. 2
      ui/src/app/components/tb-event-directives.js
  36. 4
      ui/src/app/components/widget-config.tpl.html
  37. 23
      ui/src/app/components/widgets-bundle-select.directive.js
  38. 4
      ui/src/app/components/widgets-bundle-select.scss
  39. 4
      ui/src/app/dashboard/dashboard-settings.controller.js
  40. 5
      ui/src/app/dashboard/dashboard-settings.tpl.html
  41. 217
      ui/src/app/dashboard/dashboard.controller.js
  42. 36
      ui/src/app/dashboard/dashboard.tpl.html
  43. 2
      ui/src/app/dashboard/index.js
  44. 81
      ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
  45. 10
      ui/src/app/device/attribute/attribute-table.directive.js
  46. 3
      ui/src/app/device/attribute/attribute-table.tpl.html
  47. 2
      ui/src/app/layout/breadcrumb.tpl.html
  48. 5
      ui/src/app/layout/home.scss
  49. 4
      ui/src/app/layout/home.tpl.html
  50. 191
      ui/src/app/services/item-buffer.service.js
  51. 7
      ui/src/locale/en_US.json
  52. 3
      ui/src/scss/main.scss

135
application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java

@ -19,12 +19,18 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.thingsboard.client.tools.RestClient;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.mqtt.AbstractFeatureIntegrationTest;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -40,28 +46,88 @@ public class MqttServerSideRpcIntegrationTest extends AbstractFeatureIntegration
private static final String USERNAME = "tenant@thingsboard.org";
private static final String PASSWORD = "tenant";
private Device savedDevice;
private String accessToken;
private RestClient restClient;
@Before
public void beforeTest() throws Exception {
restClient = new RestClient(BASE_URL);
restClient.login(USERNAME, PASSWORD);
}
@Test
public void testServerMqttOneWayRpc() throws Exception {
Device device = new Device();
device.setName("Test Server-Side RPC Device");
savedDevice = restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
DeviceCredentials deviceCredentials =
restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
device.setName("Test One-Way Server-Side RPC");
Device savedDevice = getSavedDevice(device);
DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
accessToken = deviceCredentials.getCredentialsId();
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
client.connect(options);
Thread.sleep(3000);
client.subscribe("v1/devices/me/rpc/request/+", 1);
client.setCallback(new TestMqttCallback(client));
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
ResponseEntity result = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class);
Assert.assertEquals(HttpStatus.OK, result.getStatusCode());
Assert.assertNull(result.getBody());
}
@Test
public void testServerMqttOneWayRpcDeviceOffline() throws Exception {
Device device = new Device();
device.setName("Test One-Way Server-Side RPC Device Offline");
Device savedDevice = getSavedDevice(device);
DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
try {
restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class);
Assert.fail("HttpClientErrorException expected, but not encountered");
} catch (HttpClientErrorException e) {
log.error(e.getMessage(), e);
Assert.assertEquals(HttpStatus.REQUEST_TIMEOUT, e.getStatusCode());
Assert.assertEquals("408 null", e.getMessage());
}
}
@Test
public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception {
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String nonExistentDeviceId = UUID.randomUUID().toString();
try {
restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class);
Assert.fail("HttpClientErrorException expected, but not encountered");
} catch (HttpClientErrorException e) {
log.error(e.getMessage(), e);
Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
Assert.assertEquals("400 null", e.getMessage());
}
}
@Test
public void testServerMqttTwoWayRpc() throws Exception {
Device device = new Device();
device.setName("Test Two-Way Server-Side RPC");
Device savedDevice = getSavedDevice(device);
DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
@ -69,16 +135,63 @@ public class MqttServerSideRpcIntegrationTest extends AbstractFeatureIntegration
options.setUserName(accessToken);
client.connect(options);
Thread.sleep(3000);
client.subscribe("v1/devices/me/rpc/request/+",1);
client.subscribe("v1/devices/me/rpc/request/+", 1);
client.setCallback(new TestMqttCallback(client));
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class).getBody();
log.info("Result: " + result);
String result = getStringResult(setGpioRequest, "twoway", deviceId);
Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
}
@Test
public void testServerMqttTwoWayRpcDeviceOffline() throws Exception {
Device device = new Device();
device.setName("Test Two-Way Server-Side RPC Device Offline");
Device savedDevice = getSavedDevice(device);
DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
try {
restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class);
Assert.fail("HttpClientErrorException expected, but not encountered");
} catch (HttpClientErrorException e) {
log.error(e.getMessage(), e);
Assert.assertEquals(HttpStatus.REQUEST_TIMEOUT, e.getStatusCode());
Assert.assertEquals("408 null", e.getMessage());
}
}
@Test
public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception {
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String nonExistentDeviceId = UUID.randomUUID().toString();
try {
restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class);
Assert.fail("HttpClientErrorException expected, but not encountered");
} catch (HttpClientErrorException e) {
log.error(e.getMessage(), e);
Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
Assert.assertEquals("400 null", e.getMessage());
}
}
private Device getSavedDevice(Device device) {
return restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
}
private DeviceCredentials getDeviceCredentials(Device savedDevice) {
return restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
}
private String getStringResult(String requestData, String callType, String deviceId) {
return restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/" + callType + "/" + deviceId, requestData, String.class).getBody();
}
private static class TestMqttCallback implements MqttCallback {
private final MqttAsyncClient client;

2
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -29,6 +29,8 @@ public class DataConstants {
public static final String SERVER_SCOPE = "SERVER_SCOPE";
public static final String SHARED_SCOPE = "SHARED_SCOPE";
public static final String[] ALL_SCOPES = {CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE};
public static final String ALARM = "ALARM";
public static final String ERROR = "ERROR";
public static final String LC_EVENT = "LC_EVENT";

14
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java

@ -18,6 +18,8 @@ package org.thingsboard.server.common.msg.core;
import lombok.ToString;
import org.thingsboard.server.common.msg.session.MsgType;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
@ToString
@ -28,6 +30,10 @@ public class BasicGetAttributesRequest extends BasicRequest implements GetAttrib
private final Set<String> clientKeys;
private final Set<String> sharedKeys;
public BasicGetAttributesRequest(Integer requestId) {
this(requestId, Collections.emptySet(), Collections.emptySet());
}
public BasicGetAttributesRequest(Integer requestId, Set<String> clientKeys, Set<String> sharedKeys) {
super(requestId);
this.clientKeys = clientKeys;
@ -40,13 +46,13 @@ public class BasicGetAttributesRequest extends BasicRequest implements GetAttrib
}
@Override
public Set<String> getClientAttributeNames() {
return clientKeys;
public Optional<Set<String>> getClientAttributeNames() {
return Optional.of(clientKeys);
}
@Override
public Set<String> getSharedAttributeNames() {
return sharedKeys;
public Optional<Set<String>> getSharedAttributeNames() {
return Optional.ofNullable(sharedKeys);
}
}

5
common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.msg.core;
import java.util.Optional;
import java.util.Set;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
@ -22,7 +23,7 @@ import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
public interface GetAttributesRequest extends FromDeviceRequestMsg {
Set<String> getClientAttributeNames();
Set<String> getSharedAttributeNames();
Optional<Set<String>> getClientAttributeNames();
Optional<Set<String>> getSharedAttributeNames();
}

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

File diff suppressed because one or more lines are too long

17
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java

@ -175,6 +175,23 @@ public class SubscriptionManager {
}
}
public void onAttributesUpdateFromServer(PluginContext ctx, DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
Optional<ServerAddress> serverAddress = ctx.resolve(deviceId);
if (!serverAddress.isPresent()) {
onLocalSubscriptionUpdate(ctx, deviceId, SubscriptionType.ATTRIBUTES, s -> {
List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
for (AttributeKvEntry kv : attributes) {
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
}
}
return subscriptionUpdate;
});
} else {
rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), deviceId, scope, attributes);
}
}
private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));

2
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java

@ -43,7 +43,7 @@ public class TelemetryStoragePlugin extends AbstractPlugin<EmptyComponentConfigu
public TelemetryStoragePlugin() {
this.subscriptionManager = new SubscriptionManager();
this.restMsgHandler = new TelemetryRestMsgHandler();
this.restMsgHandler = new TelemetryRestMsgHandler(subscriptionManager);
this.ruleMsgHandler = new TelemetryRuleMsgHandler(subscriptionManager);
this.websocketMsgHandler = new TelemetryWebsocketMsgHandler(subscriptionManager);
this.rpcMsgHandler = new TelemetryRpcMsgHandler(subscriptionManager);

4
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java

@ -24,10 +24,6 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
@NoArgsConstructor
public class AttributesSubscriptionCmd extends SubscriptionCmd {
public AttributesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe) {
super(cmdId, deviceId, keys, unsubscribe);
}
@Override
public SubscriptionType getType() {
return SubscriptionType.ATTRIBUTES;

9
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java

@ -26,6 +26,7 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
private int cmdId;
private String deviceId;
private String keys;
private String scope;
private boolean unsubscribe;
public abstract SubscriptionType getType();
@ -62,6 +63,14 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
this.unsubscribe = unsubscribe;
}
public String getScope() {
return scope;
}
public void setKeys(String keys) {
this.keys = keys;
}
@Override
public String toString() {
return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";

5
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java

@ -26,11 +26,6 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long timeWindow;
public TimeseriesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe, long timeWindow) {
super(cmdId, deviceId, keys, unsubscribe);
this.timeWindow = timeWindow;
}
public long getTimeWindow() {
return timeWindow;
}

21
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java

@ -29,6 +29,7 @@ import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHand
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData;
import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
import org.thingsboard.server.extensions.core.plugin.telemetry.TsData;
import javax.servlet.ServletException;
@ -39,6 +40,12 @@ import java.util.stream.Collectors;
@Slf4j
public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
private final SubscriptionManager subscriptionManager;
public TelemetryRestMsgHandler(SubscriptionManager subscriptionManager) {
this.subscriptionManager = subscriptionManager;
}
@Override
public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
RestRequest request = msg.getRequest();
@ -74,9 +81,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
if (!StringUtils.isEmpty(scope)) {
attributes = ctx.loadAttributes(deviceId, scope);
} else {
attributes = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SERVER_SCOPE));
attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SHARED_SCOPE));
attributes = new ArrayList<>();
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(ctx.loadAttributes(deviceId, s)));
}
List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
@ -99,9 +105,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
if (!StringUtils.isEmpty(scope)) {
attributes = getAttributeKvEntries(ctx, scope, deviceId, keys);
} else {
attributes = getAttributeKvEntries(ctx, DataConstants.CLIENT_SCOPE, deviceId, keys);
attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SHARED_SCOPE, deviceId, keys));
attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SERVER_SCOPE, deviceId, keys));
attributes = new ArrayList<>();
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(getAttributeKvEntries(ctx, s, deviceId, keys)));
}
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
@ -145,6 +150,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
@Override
public void onSuccess(PluginContext ctx, Void value) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
subscriptionManager.onAttributesUpdateFromServer(ctx, deviceId, scope, attributes);
}
@Override
@ -172,7 +178,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
DeviceId deviceId = DeviceId.fromString(pathParams[0]);
String scope = pathParams[1];
if (DataConstants.SERVER_SCOPE.equals(scope) ||
DataConstants.SHARED_SCOPE.equals(scope)) {
DataConstants.SHARED_SCOPE.equals(scope) ||
DataConstants.CLIENT_SCOPE.equals(scope)) {
String keysParam = request.getParameter("keys");
if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(",");

83
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java

@ -19,6 +19,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.RpcMsgHandler;
@ -42,9 +43,10 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
private final SubscriptionManager subscriptionManager;
private static final int SUBSCRIPTION_CLAZZ = 1;
private static final int SUBSCRIPTION_UPDATE_CLAZZ = 2;
private static final int SESSION_CLOSE_CLAZZ = 3;
private static final int SUBSCRIPTION_CLOSE_CLAZZ = 4;
private static final int ATTRIBUTES_UPDATE_CLAZZ = 2;
private static final int SUBSCRIPTION_UPDATE_CLAZZ = 3;
private static final int SESSION_CLOSE_CLAZZ = 4;
private static final int SUBSCRIPTION_CLOSE_CLAZZ = 5;
@Override
public void process(PluginContext ctx, RpcMsg msg) {
@ -55,6 +57,9 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
case SUBSCRIPTION_UPDATE_CLAZZ:
processRemoteSubscriptionUpdate(ctx, msg);
break;
case ATTRIBUTES_UPDATE_CLAZZ:
processAttributeUpdate(ctx, msg);
break;
case SESSION_CLOSE_CLAZZ:
processSessionClose(ctx, msg);
break;
@ -76,6 +81,17 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
subscriptionManager.onRemoteSubscriptionUpdate(ctx, proto.getSessionId(), convert(proto));
}
private void processAttributeUpdate(PluginContext ctx, RpcMsg msg) {
AttributeUpdateProto proto;
try {
proto = AttributeUpdateProto.parseFrom(msg.getMsgData());
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
subscriptionManager.onAttributesUpdateFromServer(ctx, DeviceId.fromString(proto.getDeviceId()), proto.getScope(),
proto.getDataList().stream().map(this::toAttribute).collect(Collectors.toList()));
}
private void processSubscriptionCmd(PluginContext ctx, RpcMsg msg) {
SubscriptionProto proto;
try {
@ -167,11 +183,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
} else {
Map<String, List<Object>> data = new TreeMap<>();
proto.getDataList().forEach(v -> {
List<Object> values = data.get(v.getKey());
if (values == null) {
values = new ArrayList<>();
data.put(v.getKey(), values);
}
List<Object> values = data.computeIfAbsent(v.getKey(), k -> new ArrayList<>());
for (int i = 0; i < v.getTsCount(); i++) {
Object[] value = new Object[2];
value[0] = v.getTs(i);
@ -182,4 +194,59 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
return new SubscriptionUpdate(proto.getSubscriptionId(), data);
}
}
public void onAttributesUpdate(PluginContext ctx, ServerAddress address, DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(deviceId, scope, attributes).toByteArray()));
}
private AttributeUpdateProto getAttributesUpdateProto(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
AttributeUpdateProto.Builder builder = AttributeUpdateProto.newBuilder();
builder.setDeviceId(deviceId.toString());
builder.setScope(scope);
attributes.forEach(
attr -> {
AttributeUpdateValueListProto.Builder dataBuilder = AttributeUpdateValueListProto.newBuilder();
dataBuilder.setKey(attr.getKey());
dataBuilder.setTs(attr.getLastUpdateTs());
dataBuilder.setValueType(attr.getDataType().ordinal());
switch (attr.getDataType()) {
case BOOLEAN:
dataBuilder.setBoolValue(attr.getBooleanValue().get());
break;
case LONG:
dataBuilder.setLongValue(attr.getLongValue().get());
break;
case DOUBLE:
dataBuilder.setDoubleValue(attr.getDoubleValue().get());
break;
case STRING:
dataBuilder.setStrValue(attr.getStrValue().get());
break;
}
builder.addData(dataBuilder.build());
}
);
return builder.build();
}
private AttributeKvEntry toAttribute(AttributeUpdateValueListProto proto) {
KvEntry entry = null;
DataType type = DataType.values()[proto.getValueType()];
switch (type) {
case BOOLEAN:
entry = new BooleanDataEntry(proto.getKey(), proto.getBoolValue());
break;
case LONG:
entry = new LongDataEntry(proto.getKey(), proto.getLongValue());
break;
case DOUBLE:
entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleValue());
break;
case STRING:
entry = new StringDataEntry(proto.getKey(), proto.getStrValue());
break;
}
return new BaseAttributeKvEntry(entry, proto.getTs());
}
}

10
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java

@ -58,10 +58,14 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
}
private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Set<String> names) {
private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Optional<Set<String>> names) {
List<AttributeKvEntry> attributes;
if (!names.isEmpty()) {
attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names));
if (names.isPresent()) {
if (!names.get().isEmpty()) {
attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names.get()));
} else {
attributes = ctx.loadAttributes(deviceId, scope);
}
} else {
attributes = Collections.emptyList();
}

15
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java

@ -104,7 +104,13 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
SubscriptionState sub;
if (keysOptional.isPresent()) {
List<String> keys = new ArrayList<>(keysOptional.get());
List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keys);
List<AttributeKvEntry> data = new ArrayList<>();
if (StringUtils.isEmpty(cmd.getScope())) {
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s, keys)));
} else {
data.addAll(ctx.loadAttributes(deviceId, cmd.getScope(), keys));
}
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
@ -114,7 +120,12 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
} else {
List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
List<AttributeKvEntry> data = new ArrayList<>();
if (StringUtils.isEmpty(cmd.getScope())) {
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s)));
} else {
data.addAll(ctx.loadAttributes(deviceId, cmd.getScope()));
}
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));

16
extensions-core/src/main/proto/telemetry.proto

@ -36,6 +36,12 @@ message SubscriptionUpdateProto {
repeated SubscriptionUpdateValueListProto data = 5;
}
message AttributeUpdateProto {
string deviceId = 1;
string scope = 2;
repeated AttributeUpdateValueListProto data = 3;
}
message SessionCloseProto {
string sessionId = 1;
}
@ -54,4 +60,14 @@ message SubscriptionUpdateValueListProto {
string key = 1;
repeated int64 ts = 2;
repeated string value = 3;
}
message AttributeUpdateValueListProto {
string key = 1;
int64 ts = 2;
int32 valueType = 3;
string strValue = 4;
int64 longValue = 5;
double doubleValue = 6;
bool boolValue = 7;
}

18
transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java

@ -167,17 +167,13 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
private FromDeviceMsg convertToGetAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
List<String> queryElements = inbound.getOptions().getUriQuery();
if (queryElements == null || queryElements.size() == 0) {
log.warn("[{}] Query is empty!", ctx.getSessionId());
throw new AdaptorException(new IllegalArgumentException("Query is empty!"));
}
Set<String> clientKeys = toKeys(ctx, queryElements, "clientKeys");
Set<String> sharedKeys = toKeys(ctx, queryElements, "sharedKeys");
if (clientKeys.isEmpty() && sharedKeys.isEmpty()) {
throw new AdaptorException("No clientKeys and serverKeys parameters!");
if (queryElements != null || queryElements.size() > 0) {
Set<String> clientKeys = toKeys(ctx, queryElements, "clientKeys");
Set<String> sharedKeys = toKeys(ctx, queryElements, "sharedKeys");
return new BasicGetAttributesRequest(0, clientKeys, sharedKeys);
} else {
return new BasicGetAttributesRequest(0);
}
return new BasicGetAttributesRequest(0, clientKeys, sharedKeys);
}
private Set<String> toKeys(SessionContext ctx, List<String> queryElements, String attributeName) throws AdaptorException {
@ -191,7 +187,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
if (!StringUtils.isEmpty(keys)) {
return new HashSet<>(Arrays.asList(keys.split(",")));
} else {
return Collections.emptySet();
return null;
}
}

2
transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java

@ -182,7 +182,7 @@ public class CoapServerTest {
public void testNoKeysAttributesGetRequest() {
CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.ATTRIBUTES.name().toLowerCase() + "?data=key1,key2");
CoapResponse response = client.setTimeout(6000).get();
Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
Assert.assertEquals(ResponseCode.CONTENT, response.getCode());
}
@Test

25
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -38,6 +38,7 @@ import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.transport.http.session.HttpSessionCtx;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -60,20 +61,22 @@ public class DeviceApiController {
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "clientKeys", required = false) String clientKeys,
@RequestParam(value = "sharedKeys", required = false) String sharedKeys) {
@RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
@RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
if (StringUtils.isEmpty(clientKeys) && StringUtils.isEmpty(sharedKeys)) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} else {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
Set<String> clientKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
Set<String> sharedKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
process(ctx, new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet));
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
GetAttributesRequest request;
if (StringUtils.isEmpty(clientKeys) && StringUtils.isEmpty(sharedKeys)) {
request = new BasicGetAttributesRequest(0);
} else {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
Set<String> clientKeySet = !StringUtils.isEmpty(clientKeys) ? new HashSet<>(Arrays.asList(clientKeys.split(","))) : null;
Set<String> sharedKeySet = !StringUtils.isEmpty(sharedKeys) ? new HashSet<>(Arrays.asList(sharedKeys.split(","))) : null;
request = new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet);
}
process(ctx, request);
} else {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
}
return responseWriter;

11
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -162,8 +162,13 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.ATTRIBUTES_REQUEST_TOPIC_PREFIX.length()));
String payload = inbound.payload().toString(UTF8);
JsonElement requestBody = new JsonParser().parse(payload);
return new BasicGetAttributesRequest(requestId,
toStringSet(requestBody, "clientKeys"), toStringSet(requestBody, "sharedKeys"));
Set<String> clientKeys = toStringSet(requestBody, "clientKeys");
Set<String> sharedKeys = toStringSet(requestBody, "sharedKeys");
if (clientKeys == null && sharedKeys == null) {
return new BasicGetAttributesRequest(requestId);
} else {
return new BasicGetAttributesRequest(requestId, clientKeys, sharedKeys);
}
} catch (RuntimeException e) {
log.warn("Failed to decode get attributes request", e);
throw new AdaptorException(e);
@ -189,7 +194,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
if (element != null) {
return new HashSet<>(Arrays.asList(element.getAsString().split(",")));
} else {
return Collections.emptySet();
return null;
}
}

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

@ -293,7 +293,8 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
if (!deviceAttributesSubscription) {
var subscriptionCommand = {
deviceId: deviceId
deviceId: deviceId,
scope: attributeScope
};
var type = attributeScope === types.latestTelemetry.value ?

2
ui/src/app/app.js

@ -49,6 +49,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller';
import thingsboardMenu from './services/menu.service';
import thingsboardUtils from './common/utils.service';
import thingsboardTypes from './common/types.constant';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast';
import thingsboardHome from './layout';
@ -95,6 +96,7 @@ angular.module('thingsboard', [
thingsboardMenu,
thingsboardUtils,
thingsboardTypes,
thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,
thingsboardHome,

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

@ -23,6 +23,7 @@ import thingsboardWidget from './widget.directive';
import thingsboardToast from '../services/toast';
import thingsboardTimewindow from './timewindow.directive';
import thingsboardEvents from './tb-event-directives';
import thingsboardMousepointMenu from './mousepoint-menu.directive';
/* eslint-disable import/no-unresolved, import/default */
@ -38,6 +39,7 @@ export default angular.module('thingsboard.directives.dashboard', [thingsboardTy
thingsboardWidget,
thingsboardTimewindow,
thingsboardEvents,
thingsboardMousepointMenu,
gridster.name])
.directive('tbDashboard', Dashboard)
.name;
@ -59,7 +61,10 @@ function Dashboard() {
isRemoveActionEnabled: '=',
onEditWidget: '&?',
onRemoveWidget: '&?',
onWidgetMouseDown: '&?',
onWidgetClicked: '&?',
prepareDashboardContextMenu: '&?',
prepareWidgetContextMenu: '&?',
loadWidgets: '&?',
onInit: '&?',
onInitFailed: '&?',
@ -75,8 +80,9 @@ function Dashboard() {
function DashboardController($scope, $rootScope, $element, $timeout, $log, toast, types) {
var highlightedMode = false;
var highlightedIndex = -1;
var mouseDownIndex = -1;
var highlightedWidget = null;
var selectedWidget = null;
var mouseDownWidget = -1;
var widgetMouseMoved = false;
var gridsterParent = null;
@ -117,6 +123,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.isWidgetExpanded = false;
vm.isHighlighted = isHighlighted;
vm.isNotHighlighted = isNotHighlighted;
vm.selectWidget = selectWidget;
vm.getSelectedWidget = getSelectedWidget;
vm.highlightWidget = highlightWidget;
vm.resetHighlight = resetHighlight;
@ -134,6 +142,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.removeWidget = removeWidget;
vm.loading = loading;
vm.openDashboardContextMenu = openDashboardContextMenu;
vm.openWidgetContextMenu = openWidgetContextMenu;
vm.getEventGridPosition = getEventGridPosition;
vm.contextMenuItems = [];
vm.contextMenuEvent = null;
vm.widgetContextMenuItems = [];
vm.widgetContextMenuEvent = null;
//$element[0].onmousemove=function(){
// widgetMouseMove();
// }
@ -305,7 +324,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
function resetWidgetClick () {
mouseDownIndex = -1;
mouseDownWidget = -1;
widgetMouseMoved = false;
}
@ -315,25 +334,27 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
function widgetMouseDown ($event, widget) {
mouseDownIndex = vm.widgets.indexOf(widget);
mouseDownWidget = widget;
widgetMouseMoved = false;
if (vm.onWidgetMouseDown) {
vm.onWidgetMouseDown({event: $event, widget: widget});
}
}
function widgetMouseMove () {
if (mouseDownIndex > -1) {
if (mouseDownWidget) {
widgetMouseMoved = true;
}
}
function widgetMouseUp ($event, widget) {
$timeout(function () {
if (!widgetMouseMoved && mouseDownIndex > -1) {
var index = vm.widgets.indexOf(widget);
if (index === mouseDownIndex) {
if (!widgetMouseMoved && mouseDownWidget) {
if (widget === mouseDownWidget) {
widgetClicked($event, widget);
}
}
mouseDownIndex = -1;
mouseDownWidget = null;
widgetMouseMoved = false;
}, 0);
}
@ -347,6 +368,41 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
function openDashboardContextMenu($event, $mdOpenMousepointMenu) {
if (vm.prepareDashboardContextMenu) {
vm.contextMenuItems = vm.prepareDashboardContextMenu();
if (vm.contextMenuItems && vm.contextMenuItems.length > 0) {
vm.contextMenuEvent = $event;
$mdOpenMousepointMenu($event);
}
}
}
function openWidgetContextMenu($event, widget, $mdOpenMousepointMenu) {
if (vm.prepareWidgetContextMenu) {
vm.widgetContextMenuItems = vm.prepareWidgetContextMenu({widget: widget});
if (vm.widgetContextMenuItems && vm.widgetContextMenuItems.length > 0) {
vm.widgetContextMenuEvent = $event;
$mdOpenMousepointMenu($event);
}
}
}
function getEventGridPosition(event) {
var pos = {
row: 0,
column: 0
}
var offset = gridsterParent.offset();
var x = event.pageX - offset.left + gridsterParent.scrollLeft();
var y = event.pageY - offset.top + gridsterParent.scrollTop();
if (gridster) {
pos.row = gridster.pixelsToRows(y);
pos.column = gridster.pixelsToColumns(x);
}
return pos;
}
function editWidget ($event, widget) {
resetWidgetClick();
if ($event) {
@ -367,10 +423,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
function highlightWidget(widgetIndex, delay) {
function highlightWidget(widget, delay) {
highlightedMode = true;
highlightedIndex = widgetIndex;
var item = $('.gridster-item', gridster.$element)[widgetIndex];
highlightedWidget = widget;
var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)];
if (item) {
var height = $(item).outerHeight(true);
var rectHeight = gridsterParent.height();
@ -385,17 +441,39 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
function selectWidget(widget, delay) {
selectedWidget = widget;
var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)];
if (item) {
var height = $(item).outerHeight(true);
var rectHeight = gridsterParent.height();
var offset = (rectHeight - height) / 2;
var scrollTop = item.offsetTop;
if (offset > 0) {
scrollTop -= offset;
}
gridsterParent.animate({
scrollTop: scrollTop
}, delay);
}
}
function getSelectedWidget() {
return selectedWidget;
}
function resetHighlight() {
highlightedMode = false;
highlightedIndex = -1;
highlightedWidget = null;
selectedWidget = null;
}
function isHighlighted(widget) {
return highlightedMode && vm.widgets.indexOf(widget) === highlightedIndex;
return (highlightedMode && highlightedWidget === widget) || (selectedWidget === widget);
}
function isNotHighlighted(widget) {
return highlightedMode && vm.widgets.indexOf(widget) != highlightedIndex;
return highlightedMode && highlightedWidget != widget;
}
function widgetColor(widget) {

2
ui/src/app/components/dashboard.scss

@ -20,6 +20,7 @@ div.tb-widget {
height: 100%;
margin: 0;
overflow: hidden;
outline: none;
@include transition(all .2s ease-in-out);
.tb-widget-title {
@ -91,6 +92,7 @@ md-content.tb-dashboard-content {
left: 0;
right: 0;
bottom: 0;
outline: none;
}
.tb-widget-error-container {

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

@ -19,64 +19,90 @@
ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
</md-content>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
<div id="gridster-child" gridster="vm.gridsterOpts">
<ul>
<!-- ng-click="widgetClicked($event, widget)" -->
<li gridster-item="widget" ng-repeat="widget in vm.widgets">
<div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)"
style="
cursor: pointer;
color: {{vm.widgetColor(widget)}};
background-color: {{vm.widgetBackgroundColor(widget)}};
padding: {{vm.widgetPadding(widget)}}
">
<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
<div class="tb-widget-actions" layout="row" layout-align="start center">
<md-button id="expand-button"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button md-primary"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<md-icon class="material-icons">
edit
</md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<md-icon class="material-icons">
close
</md-icon>
</md-button>
</div>
<div flex layout="column" class="tb-widget-content">
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
<md-menu md-position-mode="target target" tb-mousepoint-menu>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
<div id="gridster-child" gridster="vm.gridsterOpts">
<ul>
<!-- ng-click="widgetClicked($event, widget)" -->
<li gridster-item="widget" ng-repeat="widget in vm.widgets">
<md-menu md-position-mode="target target" tb-mousepoint-menu>
<div tb-expand-fullscreen
expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)"
ng-click=""
tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
style="
cursor: pointer;
color: {{vm.widgetColor(widget)}};
background-color: {{vm.widgetBackgroundColor(widget)}};
padding: {{vm.widgetPadding(widget)}}
">
<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
<div class="tb-widget-actions" layout="row" layout-align="start center">
<md-button id="expand-button"
ng-show="!vm.isEdit"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button md-primary"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<md-icon class="material-icons">
edit
</md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<md-icon class="material-icons">
close
</md-icon>
</md-button>
</div>
<div flex layout="column" class="tb-widget-content">
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span>
<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</li>
</ul>
</div>
</div>
</div>
</md-content>
</md-content>
<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<md-menu-item ng-repeat ="item in vm.contextMenuItems">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span>
<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>

16
ui/src/app/components/datasource-device.scss

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../scss/constants';
.tb-device-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
.tb-not-found {
display: block;
@ -27,3 +30,16 @@
white-space: normal !important;
}
}
tb-datasource-device {
@media (min-width: $layout-breakpoint-gt-sm) {
padding-left: 4px;
padding-right: 4px;
}
tb-device-alias-select {
@media (min-width: $layout-breakpoint-gt-sm) {
width: 200px;
max-width: 200px;
}
}
}

58
ui/src/app/components/datasource-device.tpl.html

@ -15,16 +15,16 @@
limitations under the License.
-->
<section flex layout='row' layout-align="start center">
<tb-device-alias-select flex="40"
<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
<tb-device-alias-select
tb-required="true"
device-aliases="deviceAliases"
ng-model="deviceAlias"
on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
</tb-device-alias-select>
<section flex="120" layout='column'>
<section flex layout='row' layout-align="start center">
<md-chips flex style="padding-left: 4px;"
<section flex layout='column'>
<section flex layout='column' layout-align="center" style="padding-left: 4px;">
<md-chips flex
id="timeseries_datakey_chips"
ng-required="true"
ng-model="timeseriesDataKeys" md-autocomplete-snap
@ -56,14 +56,19 @@
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center">
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div>
{{$chip.label}}:
<strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
<strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
<strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
</div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
@ -71,7 +76,7 @@
</div>
</md-chip-template>
</md-chips>
<md-chips flex ng-if="widgetType === types.widgetType.latest.value" style="padding-left: 4px;"
<md-chips flex ng-if="widgetType === types.widgetType.latest.value"
id="attribute_datakey_chips"
ng-required="true"
ng-model="attributeDataKeys" md-autocomplete-snap
@ -103,19 +108,24 @@
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div>
{{$chip.label}}:
<strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
<strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
<strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
</div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
</md-chip-template>
</md-chips>
</section>

9
ui/src/app/components/datasource-func.scss

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../scss/constants';
.tb-func-datakey-autocomplete {
.tb-not-found {
display: block;
@ -27,3 +30,9 @@
white-space: normal !important;
}
}
tb-datasource-func {
@media (min-width: $layout-breakpoint-gt-sm) {
padding-left: 8px;
}
}

33
ui/src/app/components/datasource-func.tpl.html

@ -15,8 +15,8 @@
limitations under the License.
-->
<section flex layout='column'>
<md-chips flex style="padding-left: 4px;"
<section flex layout='column' style="padding-left: 4px;">
<md-chips flex
id="function_datakey_chips"
ng-required="true"
ng-model="funcDataKeys" md-autocomplete-snap
@ -48,18 +48,23 @@
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div>
{{$chip.label}}:
<strong>{{$chip.name}}</strong>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong>{{$chip.name}}</strong>
</div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">

12
ui/src/app/components/datasource.scss

@ -38,6 +38,7 @@
.tb-color-preview {
content: '';
min-width: 24px;
width: 24px;
height: 24px;
border: 2px solid #fff;
@ -52,3 +53,14 @@
height: 100%;
}
}
.tb-attribute-chip {
.tb-chip-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tb-chip-separator {
white-space: pre;
}
}

8
ui/src/app/components/datasource.tpl.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<section flex layout='row' layout-align="start center" class="tb-datasource">
<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center" class="tb-datasource">
<md-input-container style="min-width: 110px;">
<md-select placeholder="{{ 'datasource.type' | translate }}" required id="datasourceType" ng-model="model.type">
<md-option ng-repeat="datasourceType in datasourceTypes" value="{{datasourceType}}">
@ -23,15 +23,15 @@
</md-option>
</md-select>
</md-input-container>
<section flex layout='row' layout-align="start center" class="datasource" ng-switch on="model.type">
<tb-datasource-func flex style="padding-left: 8px;"
<section flex class="datasource" ng-switch on="model.type">
<tb-datasource-func flex
ng-switch-default
ng-model="model"
datakey-settings-schema="datakeySettingsSchema"
ng-required="model.type === types.datasourceType.function"
generate-data-key="generateDataKey({chip: chip, type: type})">
</tb-datasource-func>
<tb-datasource-device flex style="padding-left: 4px; padding-right: 4px;"
<tb-datasource-device flex
ng-model="model"
datakey-settings-schema="datakeySettingsSchema"
ng-switch-when="device"

13
ui/src/app/components/details-sidenav.scss

@ -20,15 +20,28 @@
font-weight: 400;
text-transform: uppercase;
margin: 20px 8px 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: inherit;
}
.tb-details-subtitle {
font-size: 1.000rem;
margin: 10px 0;
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: inherit;
}
md-sidenav.tb-sidenav-details {
.md-toolbar-tools {
min-height: 100px;
max-height: 120px;
height: 100%;
}
width: 100% !important;
max-width: 100% !important;
z-index: 59 !important;

5
ui/src/app/components/details-sidenav.tpl.html

@ -22,13 +22,12 @@
layout="column">
<header>
<md-toolbar class="md-theme-light" ng-style="{'height':headerHeightPx+'px'}">
<div class="md-toolbar-tools">
<div class="md-toolbar-tools" layout="column" layout-align="start start">
<div class="md-toolbar-tools" layout="row">
<div flex class="md-toolbar-tools" layout="column" layout-align="start start">
<span class="tb-details-title">{{headerTitle}}</span>
<span class="tb-details-subtitle">{{headerSubtitle}}</span>
<span style="width: 100%;" ng-transclude="headerPane"></span>
</div>
<span flex></span>
<div ng-transclude="detailsButtons"></div>
<md-button class="md-icon-button" ng-click="closeDetails()">
<md-icon aria-label="close" class="material-icons">close</md-icon>

40
ui/src/app/components/keyboard-shortcut.filter.js

@ -0,0 +1,40 @@
/*
* Copyright © 2016 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.
*/
export default angular.module('thingsboard.filters.keyboardShortcut', [])
.filter('keyboardShortcut', KeyboardShortcut)
.name;
/*@ngInject*/
function KeyboardShortcut($window) {
return function(str) {
if (!str) return;
var keys = str.split('-');
var isOSX = /Mac OS X/.test($window.navigator.userAgent);
var seperator = (!isOSX || keys.length > 2) ? '+' : '';
var abbreviations = {
M: isOSX ? '⌘' : 'Ctrl',
A: isOSX ? 'Option' : 'Alt',
S: 'Shift'
};
return keys.map(function(key, index) {
var last = index == keys.length - 1;
return last ? key : abbreviations[key];
}).join(seperator);
};
}

51
ui/src/app/components/mousepoint-menu.directive.js

@ -0,0 +1,51 @@
/*
* Copyright © 2016 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.
*/
export default angular.module('thingsboard.directives.mousepointMenu', [])
.directive('tbMousepointMenu', MousepointMenu)
.name;
/*@ngInject*/
function MousepointMenu() {
var linker = function ($scope, $element, $attrs, RightClickContextMenu) {
$scope.$mdOpenMousepointMenu = function($event){
RightClickContextMenu.offsets = function(){
var offset = $element.offset();
var x = $event.pageX - offset.left;
var y = $event.pageY - offset.top;
var offsets = {
left: x,
top: y
}
return offsets;
}
RightClickContextMenu.open($event);
};
$scope.$mdCloseMousepointMenu = function() {
RightClickContextMenu.close();
}
}
return {
restrict: "A",
link: linker,
require: 'mdMenu'
};
}

2
ui/src/app/components/tb-event-directives.js

@ -20,7 +20,7 @@ const PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
var tbEventDirectives = {};
angular.forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave contextmenu keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(eventName) {
var directiveName = directiveNormalize('tb-' + eventName);
tbEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse) {

4
ui/src/app/components/widget-config.tpl.html

@ -25,7 +25,7 @@
<input name="title" ng-model="title">
</md-input-container>
<span translate>widget-config.general-settings</span>
<div layout="row" layout-align="start center">
<div layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
<div layout="row" layout-padding>
<md-checkbox flex aria-label="{{ 'widget-config.display-title' | translate }}"
ng-model="showTitle">{{ 'widget-config.display-title' | translate }}
@ -80,7 +80,7 @@
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 110px;">widget-config.datasource-type</span>
<span translate flex
<span hide show-gt-sm translate flex
style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span>
</div>

23
ui/src/app/components/widgets-bundle-select.directive.js

@ -55,12 +55,26 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
if (widgetsBundles.length > 0) {
scope.widgetsBundle = widgetsBundles[0];
}
} else if (angular.isDefined(scope.selectBundleAlias)) {
selectWidgetsBundleByAlias(scope.selectBundleAlias);
}
},
function fail() {
}
);
function selectWidgetsBundleByAlias(alias) {
if (scope.widgetsBundles && alias) {
for (var w in scope.widgetsBundles) {
var widgetsBundle = scope.widgetsBundles[w];
if (widgetsBundle.alias === alias) {
scope.widgetsBundle = widgetsBundle;
break;
}
}
}
}
scope.isSystem = function(item) {
return item && item.tenantId.id === types.id.nullUid;
}
@ -79,6 +93,12 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
scope.updateView();
});
scope.$watch('selectBundleAlias', function (newVal, prevVal) {
if (newVal !== prevVal) {
selectWidgetsBundleByAlias(scope.selectBundleAlias);
}
});
$compile(element.contents())(scope);
}
@ -90,7 +110,8 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
bundlesScope: '@',
theForm: '=?',
tbRequired: '=?',
selectFirstBundle: '='
selectFirstBundle: '=',
selectBundleAlias: '=?'
}
};
}

4
ui/src/app/components/widgets-bundle-select.scss

@ -35,10 +35,12 @@ tb-widgets-bundle-select {
tb-widgets-bundle-select, .tb-widgets-bundle-select {
.md-text {
display: block;
width: 100%;
}
.tb-bundle-item {
display: block;
display: inline-block;
width: 100%;
span {
display: inline-block;
vertical-align: middle;

4
ui/src/app/dashboard/dashboard-settings.controller.js

@ -28,6 +28,10 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
vm.gridSettings = gridSettings || {};
if (angular.isUndefined(vm.gridSettings.showTitle)) {
vm.gridSettings.showTitle = true;
}
vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
vm.gridSettings.columns = vm.gridSettings.columns || 24;
vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];

5
ui/src/app/dashboard/dashboard-settings.tpl.html

@ -31,6 +31,11 @@
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
<div layout="row" layout-padding>
<md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
</md-checkbox>
</div>
<md-input-container class="md-block">
<label translate>dashboard.columns-count</label>
<input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"

217
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, $window, $rootScope,
dashboardService, itembuffer, hotkeys, $window, $rootScope,
$scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
var user = userService.getCurrentUser();
@ -48,7 +48,10 @@ export default function DashboardController(types, widgetService, userService,
vm.addWidgetFromType = addWidgetFromType;
vm.dashboardInited = dashboardInited;
vm.dashboardInitFailed = dashboardInitFailed;
vm.widgetMouseDown = widgetMouseDown;
vm.widgetClicked = widgetClicked;
vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
vm.editWidget = editWidget;
vm.isTenantAdmin = isTenantAdmin;
vm.loadDashboard = loadDashboard;
@ -63,6 +66,7 @@ export default function DashboardController(types, widgetService, userService,
vm.toggleDashboardEditMode = toggleDashboardEditMode;
vm.onRevertWidgetEdit = onRevertWidgetEdit;
vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
vm.displayTitle = displayTitle;
vm.widgetsBundle;
@ -194,6 +198,7 @@ export default function DashboardController(types, widgetService, userService,
function dashboardInited(dashboard) {
vm.dashboardContainer = dashboard;
initHotKeys();
}
function isTenantAdmin() {
@ -289,18 +294,194 @@ export default function DashboardController(types, widgetService, userService,
var delayOffset = transition ? 350 : 0;
var delay = transition ? 400 : 300;
$timeout(function () {
vm.dashboardContainer.highlightWidget(vm.editingWidgetIndex, delay);
vm.dashboardContainer.highlightWidget(widget, delay);
}, delayOffset, false);
}
}
}
function widgetMouseDown($event, widget) {
if (vm.isEdit && !vm.isEditingWidget) {
vm.dashboardContainer.selectWidget(widget, 0);
}
}
function widgetClicked($event, widget) {
if (vm.isEditingWidget) {
editWidget($event, widget);
}
}
function isHotKeyAllowed(event) {
var target = event.target || event.srcElement;
var scope = angular.element(target).scope();
return scope && scope.$parent !== $rootScope;
}
function initHotKeys() {
$translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
hotkeys.bindTo($scope)
.add({
combo: 'ctrl+c',
description: translations['action.copy'],
callback: function (event) {
if (isHotKeyAllowed(event) &&
vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
var widget = vm.dashboardContainer.getSelectedWidget();
if (widget) {
event.preventDefault();
copyWidget(event, widget);
}
}
}
})
.add({
combo: 'ctrl+v',
description: translations['action.paste'],
callback: function (event) {
if (isHotKeyAllowed(event) &&
vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
if (itembuffer.hasWidget()) {
event.preventDefault();
pasteWidget(event);
}
}
}
})
.add({
combo: 'ctrl+x',
description: translations['action.delete'],
callback: function (event) {
if (isHotKeyAllowed(event) &&
vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
var widget = vm.dashboardContainer.getSelectedWidget();
if (widget) {
event.preventDefault();
removeWidget(event, widget);
}
}
}
});
});
}
function prepareDashboardContextMenu() {
var dashboardContextActions = [];
if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
dashboardContextActions.push(
{
action: openDashboardSettings,
enabled: true,
value: "dashboard.settings",
icon: "settings"
}
);
dashboardContextActions.push(
{
action: openDeviceAliases,
enabled: true,
value: "device.aliases",
icon: "devices_other"
}
);
dashboardContextActions.push(
{
action: pasteWidget,
enabled: itembuffer.hasWidget(),
value: "action.paste",
icon: "content_paste",
shortcut: "M-V"
}
);
}
return dashboardContextActions;
}
function pasteWidget($event) {
var pos = vm.dashboardContainer.getEventGridPosition($event);
itembuffer.pasteWidget(vm.dashboard, pos);
}
function prepareWidgetContextMenu() {
var widgetContextActions = [];
if (vm.isEdit && !vm.isEditingWidget) {
widgetContextActions.push(
{
action: editWidget,
enabled: true,
value: "action.edit",
icon: "edit"
}
);
if (!vm.widgetEditMode) {
widgetContextActions.push(
{
action: copyWidget,
enabled: true,
value: "action.copy",
icon: "content_copy",
shortcut: "M-C"
}
);
widgetContextActions.push(
{
action: removeWidget,
enabled: true,
value: "action.delete",
icon: "clear",
shortcut: "M-X"
}
);
}
}
return widgetContextActions;
}
function copyWidget($event, widget) {
var aliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
};
var originalColumns = 24;
if (vm.dashboard.configuration.gridSettings &&
vm.dashboard.configuration.gridSettings.columns) {
originalColumns = vm.dashboard.configuration.gridSettings.columns;
}
if (widget.config && vm.dashboard.configuration
&& vm.dashboard.configuration.deviceAliases) {
var deviceAlias;
if (widget.config.datasources) {
for (var i=0;i<widget.config.datasources.length;i++) {
var datasource = widget.config.datasources[i];
if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
deviceAlias = vm.dashboard.configuration.deviceAliases[datasource.deviceAliasId];
if (deviceAlias) {
aliasesInfo.datasourceAliases[i] = {
aliasName: deviceAlias.alias,
deviceId: deviceAlias.deviceId
}
}
}
}
}
if (widget.config.targetDeviceAliasIds) {
for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
if (targetDeviceAliasId) {
deviceAlias = vm.dashboard.configuration.deviceAliases[targetDeviceAliasId];
if (deviceAlias) {
aliasesInfo.targetDeviceAliases[i] = {
aliasName: deviceAlias.alias,
deviceId: deviceAlias.deviceId
}
}
}
}
}
}
itembuffer.copyWidget(widget, aliasesInfo, originalColumns);
}
function helpLinkIdForWidgetType() {
var link = 'widgetsConfig';
if (vm.editingWidget && vm.editingWidget.type) {
@ -322,6 +503,15 @@ export default function DashboardController(types, widgetService, userService,
return link;
}
function displayTitle() {
if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) {
return vm.dashboard.configuration.gridSettings.showTitle;
} else {
return true;
}
}
function onRevertWidgetEdit(widgetForm) {
if (widgetForm.$dirty) {
widgetForm.$setPristine();
@ -331,7 +521,9 @@ export default function DashboardController(types, widgetService, userService,
function saveWidget(widgetForm) {
widgetForm.$setPristine();
vm.widgets[vm.editingWidgetIndex] = angular.copy(vm.editingWidget);
var widget = angular.copy(vm.editingWidget);
vm.widgets[vm.editingWidgetIndex] = widget;
vm.dashboardContainer.highlightWidget(widget, 0);
}
function onEditWidgetClosed() {
@ -421,8 +613,8 @@ export default function DashboardController(types, widgetService, userService,
});
}
function toggleDashboardEditMode() {
vm.isEdit = !vm.isEdit;
function setEditMode(isEdit, revert) {
vm.isEdit = isEdit;
if (vm.isEdit) {
if (vm.widgetEditMode) {
vm.prevWidgets = angular.copy(vm.widgets);
@ -433,14 +625,23 @@ export default function DashboardController(types, widgetService, userService,
if (vm.widgetEditMode) {
vm.widgets = vm.prevWidgets;
} else {
vm.dashboard = vm.prevDashboard;
vm.widgets = vm.dashboard.configuration.widgets;
if (vm.dashboardContainer) {
vm.dashboardContainer.resetHighlight();
}
if (revert) {
vm.dashboard = vm.prevDashboard;
vm.widgets = vm.dashboard.configuration.widgets;
}
}
}
}
function toggleDashboardEditMode() {
setEditMode(!vm.isEdit, true);
}
function saveDashboard() {
vm.isEdit = false;
setEditMode(false, false);
notifyDashboardUpdated();
}

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

@ -16,7 +16,7 @@
-->
<md-content flex tb-expand-fullscreen="vm.widgetEditMode" hide-expand-button="vm.widgetEditMode">
<section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
<!--section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
class="tb-header-buttons tb-top-header-buttons md-fab" ng-style="{'right': '50px'}">
<md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit" ng-disabled="loading"
class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
@ -37,7 +37,7 @@
<ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
</md-button>
</section>
</section-->
<section ng-show="!loading && vm.noData()" layout-align="center center"
ng-class="{'tb-padded' : !vm.widgetEditMode}"
style="text-transform: uppercase; display: flex; z-index: 1;"
@ -51,7 +51,7 @@
</md-button>
</section>
<section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center">
<h3 ng-show="!vm.isEdit">{{ vm.dashboard.title }}</h3>
<h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
<md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
<label translate>dashboard.title</label>
<input class="tb-dashboard-title" required name="title" ng-model="vm.dashboard.title">
@ -64,7 +64,7 @@
</md-button>
</section>
<div class="tb-absolute-fill"
ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
<tb-dashboard
dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
@ -82,7 +82,11 @@
is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
on-edit-widget="vm.editWidget(event, widget)"
on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
on-widget-clicked="vm.widgetClicked(event, widget)"
on-widget-context-menu="vm.widgetContextMenu(event, widget)"
prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
on-remove-widget="vm.removeWidget(event, widget)"
load-widgets="vm.loadDashboard()"
on-init="vm.dashboardInited(dashboard)"
@ -176,8 +180,8 @@
</div>
</tb-details-sidenav>
<!-- </section> -->
<section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
<md-button ng-disabled="loading" ng-if="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
<section layout="row" layout-wrap class="tb-footer-buttons md-fab">
<md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
aria-label="{{ 'dashboard.add-widget' | translate }}">
<md-tooltip md-direction="top">
@ -185,5 +189,25 @@
</md-tooltip>
<ng-md-icon icon="add"></ng-md-icon>
</md-button>
<md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.saveDashboard()">
<md-tooltip md-direction="top">
{{ 'action.apply-changes' | translate }}
</md-tooltip>
<ng-md-icon icon="done"></ng-md-icon>
</md-button>
<md-button ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode"
ng-if="vm.isTenantAdmin()" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.edit-mode' | translate }}"
ng-click="vm.toggleDashboardEditMode()">
<md-tooltip md-direction="top">
{{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}
</md-tooltip>
<ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
</md-button>
</section>
</md-content>

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

@ -29,6 +29,7 @@ import thingsboardDashboard from '../components/dashboard.directive';
import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
import thingsboardTypes from '../common/types.constant';
import thingsboardItemBuffer from '../services/item-buffer.service';
import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
@ -45,6 +46,7 @@ export default angular.module('thingsboard.dashboard', [
uiRouter,
gridster.name,
thingsboardTypes,
thingsboardItemBuffer,
thingsboardGrid,
thingsboardApiWidget,
thingsboardApiUser,

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

@ -14,7 +14,7 @@
* limitations under the License.
*/
/*@ngInject*/
export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, dashboardService, deviceId, deviceName, widget) {
export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, deviceId, deviceName, widget) {
var vm = this;
@ -34,62 +34,20 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
function add() {
$scope.theForm.$setPristine();
var theDashboard;
var deviceAliases;
widget.col = 0;
widget.sizeX /= 2;
widget.sizeY /= 2;
if (vm.addToDashboardType === 0) {
theDashboard = vm.dashboard;
if (!theDashboard.configuration) {
theDashboard.configuration = {};
}
deviceAliases = theDashboard.configuration.deviceAliases;
if (!deviceAliases) {
deviceAliases = {};
theDashboard.configuration.deviceAliases = deviceAliases;
}
var newAliasId;
for (var aliasId in deviceAliases) {
if (deviceAliases[aliasId].deviceId === deviceId) {
newAliasId = aliasId;
break;
}
}
if (!newAliasId) {
var newAliasName = createDeviceAliasName(deviceAliases, deviceName);
newAliasId = 0;
for (aliasId in deviceAliases) {
newAliasId = Math.max(newAliasId, aliasId);
}
newAliasId++;
deviceAliases[newAliasId] = {alias: newAliasName, deviceId: deviceId};
}
widget.config.datasources[0].deviceAliasId = newAliasId;
if (!theDashboard.configuration.widgets) {
theDashboard.configuration.widgets = [];
}
var row = 0;
for (var w in theDashboard.configuration.widgets) {
var existingWidget = theDashboard.configuration.widgets[w];
var wRow = existingWidget.row ? existingWidget.row : 0;
var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
var bottom = wRow + wSizeY;
row = Math.max(row, bottom);
}
widget.row = row;
theDashboard.configuration.widgets.push(widget);
} else {
theDashboard = vm.newDashboard;
deviceAliases = {};
deviceAliases['1'] = {alias: deviceName, deviceId: deviceId};
theDashboard.configuration = {};
theDashboard.configuration.widgets = [];
widget.row = 0;
theDashboard.configuration.widgets.push(widget);
theDashboard.configuration.deviceAliases = deviceAliases;
}
var aliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
};
aliasesInfo.datasourceAliases[0] = {
aliasName: deviceName,
deviceId: deviceId
};
theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1);
dashboardService.saveDashboard(theDashboard).then(
function success(dashboard) {
$mdDialog.hide();
@ -98,25 +56,6 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
}
}
);
}
function createDeviceAliasName(deviceAliases, alias) {
var c = 0;
var newAlias = angular.copy(alias);
var unique = false;
while (!unique) {
unique = true;
for (var devAliasId in deviceAliases) {
var devAlias = deviceAliases[devAliasId];
if (newAlias === devAlias.alias) {
c++;
newAlias = alias + c;
unique = false;
}
}
}
return newAlias;
}
}

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

@ -239,6 +239,8 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
index: 0
}
scope.widgetsBundle = null;
scope.firstBundle = true;
scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
scope.deviceAliases = {};
scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
@ -326,13 +328,6 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}
}
});
widgetService.getWidgetsBundleByAlias(types.systemBundleAlias.cards).then(
function success(widgetsBundle) {
scope.firstBundle = true;
scope.widgetsBundle = widgetsBundle;
}
);
}
scope.exitWidgetMode = function() {
@ -344,6 +339,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.widgetsIndexWatch();
scope.widgetsIndexWatch = null;
}
scope.selectedWidgetsBundleAlias = null;
scope.mode = 'default';
scope.getDeviceAttributes(true);
}

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

@ -105,7 +105,8 @@
<tb-widgets-bundle-select flex-offset="5"
flex
ng-model="widgetsBundle"
select-first-bundle="false">
select-first-bundle="false"
select-bundle-alias="selectedWidgetsBundleAlias">
</tb-widgets-bundle-select>
</div>
<md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">

2
ui/src/app/layout/breadcrumb.tpl.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-breadcrumb">
<div flex class="tb-breadcrumb" layout="row">
<h1 flex hide-gt-sm>{{ steps[steps.length-1].ncyBreadcrumbLabel | breadcrumbLabel }}</h1>
<span hide-xs hide-sm ng-repeat="step in steps" ng-switch="$last || !!step.abstract">
<a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}">

5
ui/src/app/layout/home.scss

@ -29,6 +29,11 @@
.tb-breadcrumb {
font-size: 18px !important;
font-weight: 400 !important;
h1, a, span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
a {
border: none;
opacity: 0.75;

4
ui/src/app/layout/home.tpl.html

@ -39,7 +39,7 @@
<div flex layout="column" tabIndex="-1" role="main">
<md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
<div flex class="md-toolbar-tools">
<div layout="row" flex class="md-toolbar-tools">
<md-button id="main" hide-gt-sm
class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
<md-icon aria-label="{{ 'home.menu' | translate }}" class="material-icons">menu</md-icon>
@ -47,7 +47,7 @@
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="searchConfig.showSearch = !searchConfig.showSearch" ng-class="{'tb-invisible': !vm.displaySearchMode()}" >
<md-icon aria-label="{{ 'action.back' | translate }}" class="material-icons">arrow_back</md-icon>
</md-button>
<div flex ng-show="!vm.displaySearchMode()" tb-no-animate flex class="md-toolbar-tools">
<div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
<span ng-cloak ncy-breadcrumb></span>
</div>
<md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>

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

@ -0,0 +1,191 @@
/*
* Copyright © 2016 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 angularStorage from 'angular-storage';
export default angular.module('thingsboard.itembuffer', [angularStorage])
.factory('itembuffer', ItemBuffer)
.factory('bufferStore', function(store) {
var newStore = store.getNamespacedStore('tbBufferStore', null, null, false);
return newStore;
})
.name;
/*@ngInject*/
function ItemBuffer(bufferStore) {
const WIDGET_ITEM = "widget_item";
var service = {
copyWidget: copyWidget,
hasWidget: hasWidget,
pasteWidget: pasteWidget,
addWidgetToDashboard: addWidgetToDashboard
}
return service;
/**
aliasesInfo {
datasourceAliases: {
datasourceIndex: {
aliasName: "...",
deviceId: "..."
}
}
targetDeviceAliases: {
targetDeviceAliasIndex: {
aliasName: "...",
deviceId: "..."
}
}
....
}
**/
function copyWidget(widget, aliasesInfo, originalColumns) {
var widgetItem = {
widget: widget,
aliasesInfo: aliasesInfo,
originalColumns: originalColumns
}
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
}
function hasWidget() {
return bufferStore.get(WIDGET_ITEM);
}
function pasteWidget(targetDasgboard, position) {
var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson);
var widget = widgetItem.widget;
var aliasesInfo = widgetItem.aliasesInfo;
var originalColumns = widgetItem.originalColumns;
var targetRow = -1;
var targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
}
}
function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) {
var theDashboard;
if (dashboard) {
theDashboard = dashboard;
} else {
theDashboard = {};
}
if (!theDashboard.configuration) {
theDashboard.configuration = {};
}
if (!theDashboard.configuration.deviceAliases) {
theDashboard.configuration.deviceAliases = {};
}
updateAliases(theDashboard, widget, aliasesInfo);
if (!theDashboard.configuration.widgets) {
theDashboard.configuration.widgets = [];
}
var targetColumns = 24;
if (theDashboard.configuration.gridSettings &&
theDashboard.configuration.gridSettings.columns) {
targetColumns = theDashboard.configuration.gridSettings.columns;
}
if (targetColumns != originalColumns) {
var ratio = targetColumns / originalColumns;
widget.sizeX *= ratio;
widget.sizeY *= ratio;
}
if (row > -1 && column > - 1) {
widget.row = row;
widget.col = column;
} else {
row = 0;
for (var w in theDashboard.configuration.widgets) {
var existingWidget = theDashboard.configuration.widgets[w];
var wRow = existingWidget.row ? existingWidget.row : 0;
var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
var bottom = wRow + wSizeY;
row = Math.max(row, bottom);
}
widget.row = row;
widget.col = 0;
}
theDashboard.configuration.widgets.push(widget);
return theDashboard;
}
function updateAliases(dashboard, widget, aliasesInfo) {
var deviceAliases = dashboard.configuration.deviceAliases;
var aliasInfo;
var newAliasId;
for (var datasourceIndex in aliasesInfo.datasourceAliases) {
aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
widget.config.datasources[datasourceIndex].deviceAliasId = newAliasId;
}
for (var targetDeviceAliasIndex in aliasesInfo.targetDeviceAliases) {
aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex];
newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
}
}
function getDeviceAliasId(deviceAliases, aliasInfo) {
var newAliasId;
for (var aliasId in deviceAliases) {
if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) {
newAliasId = aliasId;
break;
}
}
if (!newAliasId) {
var newAliasName = createDeviceAliasName(deviceAliases, aliasInfo.aliasName);
newAliasId = 0;
for (aliasId in deviceAliases) {
newAliasId = Math.max(newAliasId, aliasId);
}
newAliasId++;
deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId};
}
return newAliasId;
}
function createDeviceAliasName(deviceAliases, alias) {
var c = 0;
var newAlias = angular.copy(alias);
var unique = false;
while (!unique) {
unique = true;
for (var devAliasId in deviceAliases) {
var devAlias = deviceAliases[devAliasId];
if (newAlias === devAlias.alias) {
c++;
newAlias = alias + c;
unique = false;
}
}
}
return newAlias;
}
}

7
ui/src/locale/en_US.json

@ -38,7 +38,9 @@
"create": "Create",
"drag": "Drag",
"refresh": "Refresh",
"undo": "Undo"
"undo": "Undo",
"copy": "Copy",
"paste": "Paste"
},
"admin": {
"general": "General",
@ -211,7 +213,8 @@
"vertical-margin": "Vertical margin",
"vertical-margin-required": "Vertical margin value is required.",
"min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value."
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
"display-title": "Display dashboard title"
},
"datakey": {
"settings": "Settings",

3
ui/src/scss/main.scss

@ -169,6 +169,9 @@ md-menu-item {
md-menu-item {
.md-button {
display: block;
.tb-alt-text {
float: right;
}
}
}

Loading…
Cancel
Save