Browse Source

Merge remote-tracking branch 'origin/master' into feature/entity-view

pull/1072/head
Volodymyr Babak 8 years ago
parent
commit
a1aff91c09
  1. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  3. 2
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  4. 32
      application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
  5. 218
      application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
  6. 5
      application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java
  7. 13
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  8. 2
      application/src/main/resources/logback.xml
  9. 95
      application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
  10. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
  11. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
  12. 23
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
  13. 6
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  14. 8
      ui/src/app/api/subscription.js
  15. 8
      ui/src/app/components/datetime-period.tpl.html
  16. 1
      ui/src/app/components/json-form.directive.js
  17. 68
      ui/src/app/components/react/json-form-rc-select.jsx
  18. 11
      ui/src/app/components/react/json-form-schema-form.jsx
  19. 10
      ui/src/app/locale/locale.constant-en_US.json
  20. 2
      ui/src/app/widget/lib/CanvasDigitalGauge.js
  21. 31
      ui/src/app/widget/lib/alarm-status-filter-panel.scss
  22. 28
      ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html
  23. 188
      ui/src/app/widget/lib/alarms-table-widget.js
  24. 30
      ui/src/app/widget/lib/alarms-table-widget.scss
  25. 40
      ui/src/app/widget/lib/alarms-table-widget.tpl.html
  26. 31
      ui/src/app/widget/lib/display-columns-panel.scss
  27. 24
      ui/src/app/widget/lib/display-columns-panel.tpl.html
  28. 77
      ui/src/app/widget/lib/entities-table-widget.js
  29. 30
      ui/src/app/widget/lib/entities-table-widget.scss
  30. 27
      ui/src/app/widget/lib/entities-table-widget.tpl.html

2
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -155,7 +155,7 @@ class DefaultTbContext implements TbContext {
@Override @Override
public ScriptEngine createJsScriptEngine(String script, String... argNames) { public ScriptEngine createJsScriptEngine(String script, String... argNames) {
return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames); return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
} }
@Override @Override

2
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -83,7 +83,7 @@ public class AlarmController extends BaseController {
Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm)); Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm));
logEntityAction(savedAlarm.getId(), savedAlarm, logEntityAction(savedAlarm.getId(), savedAlarm,
getCurrentUser().getCustomerId(), getCurrentUser().getCustomerId(),
savedAlarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedAlarm; return savedAlarm;
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.ALARM), alarm, logEntityAction(emptyId(EntityType.ALARM), alarm,

2
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -276,7 +276,7 @@ public class RuleChainController extends BaseController {
String errorText = ""; String errorText = "";
ScriptEngine engine = null; ScriptEngine engine = null;
try { try {
engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames); engine = new RuleNodeJsScriptEngine(jsSandboxService, getCurrentUser().getId(), script, argNames);
TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L); TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L);
switch (scriptType) { switch (scriptType) {
case "update": case "update":

32
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java

@ -24,6 +24,8 @@ import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache; import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.RetryForever; import org.apache.curator.retry.RetryForever;
import org.apache.curator.utils.CloseableUtils; import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.CreateMode;
@ -127,12 +129,38 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
.creatingParentsIfNeeded() .creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress())); .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress()));
log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath); log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
client.getConnectionStateListenable().addListener(checkReconnect(self));
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create ZK node", e); log.error("Failed to create ZK node", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private ConnectionStateListener checkReconnect(ServerInstance self) {
return (client, newState) -> {
log.info("[{}:{}] ZK state changed: {}", self.getHost(), self.getPort(), newState);
if (newState == ConnectionState.LOST) {
reconnect();
}
};
}
private boolean reconnectInProgress = false;
private synchronized void reconnect() {
if (!reconnectInProgress) {
reconnectInProgress = true;
try {
client.blockUntilConnected();
publishCurrentServer();
} catch (InterruptedException e) {
log.error("Failed to reconnect to ZK: {}", e.getMessage(), e);
} finally {
reconnectInProgress = false;
}
}
}
@Override @Override
public void unpublishCurrentServer() { public void unpublishCurrentServer() {
try { try {
@ -156,7 +184,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
.filter(cd -> !cd.getPath().equals(nodePath)) .filter(cd -> !cd.getPath().equals(nodePath))
.map(cd -> { .map(cd -> {
try { try {
return new ServerInstance( (ServerAddress) SerializationUtils.deserialize(cd.getData())); return new ServerInstance((ServerAddress) SerializationUtils.deserialize(cd.getData()));
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
log.error("Failed to decode ZK node", e); log.error("Failed to decode ZK node", e);
throw new RuntimeException(e); throw new RuntimeException(e);
@ -198,7 +226,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
} }
ServerInstance instance; ServerInstance instance;
try { try {
ServerAddress serverAddress = SerializationUtils.deserialize(data.getData()); ServerAddress serverAddress = SerializationUtils.deserialize(data.getData());
instance = new ServerInstance(serverAddress); instance = new ServerInstance(serverAddress);
} catch (SerializationException e) { } catch (SerializationException e) {
log.error("Failed to decode server instance for node {}", data.getPath(), e); log.error("Failed to decode server instance for node {}", data.getPath(), e);

218
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -21,8 +20,12 @@ import com.google.common.util.concurrent.ListenableFuture;
import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes; import delight.nashornsandbox.NashornSandboxes;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.id.EntityId;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
@ -44,9 +47,10 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
private ExecutorService monitorExecutorService; private ExecutorService monitorExecutorService;
private final Map<UUID, String> functionsMap = new ConcurrentHashMap<>(); private final Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
private final Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>(); private final Map<BlackListKey, BlackListInfo> blackListedFunctions = new ConcurrentHashMap<>();
private final Map<String, Pair<UUID, AtomicInteger>> scriptToId = new ConcurrentHashMap<>();
private final Map<UUID, AtomicInteger> scriptIdToCount = new ConcurrentHashMap<>(); private final Map<String, ScriptInfo> scriptKeyToInfo = new ConcurrentHashMap<>();
private final Map<UUID, ScriptInfo> scriptIdToInfo = new ConcurrentHashMap<>();
@PostConstruct @PostConstruct
public void init() { public void init() {
@ -65,7 +69,7 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
@PreDestroy @PreDestroy
public void stop() { public void stop() {
if (monitorExecutorService != null) { if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow(); monitorExecutorService.shutdownNow();
} }
} }
@ -80,90 +84,107 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
@Override @Override
public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) { public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
Pair<UUID, AtomicInteger> deduplicated = deduplicate(scriptType, scriptBody); ScriptInfo scriptInfo = deduplicate(scriptType, scriptBody);
UUID scriptId = deduplicated.getLeft(); UUID scriptId = scriptInfo.getId();
AtomicInteger duplicateCount = deduplicated.getRight(); AtomicInteger duplicateCount = scriptInfo.getCount();
if(duplicateCount.compareAndSet(0, 1)) { synchronized (scriptInfo.getLock()) {
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); if (duplicateCount.compareAndSet(0, 1)) {
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); try {
try { evaluate(scriptId, scriptType, scriptBody, argNames);
if (useJsSandbox()) { } catch (Exception e) {
sandbox.eval(jsScript); duplicateCount.decrementAndGet();
} else { log.warn("Failed to compile JS script: {}", e.getMessage(), e);
engine.eval(jsScript); return Futures.immediateFailedFuture(e);
} }
functionsMap.put(scriptId, functionName); } else {
} catch (Exception e) { duplicateCount.incrementAndGet();
duplicateCount.decrementAndGet();
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
return Futures.immediateFailedFuture(e);
} }
} else {
duplicateCount.incrementAndGet();
} }
return Futures.immediateFuture(scriptId); return Futures.immediateFuture(scriptId);
} }
private void evaluate(UUID scriptId, JsScriptType scriptType, String scriptBody, String... argNames) throws ScriptException {
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
if (useJsSandbox()) {
sandbox.eval(jsScript);
} else {
engine.eval(jsScript);
}
functionsMap.put(scriptId, functionName);
}
@Override @Override
public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) { public ListenableFuture<Object> invokeFunction(UUID scriptId, EntityId entityId, Object... args) {
String functionName = functionsMap.get(scriptId); String functionName = functionsMap.get(scriptId);
if (functionName == null) { if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); String message = "No compiled script found for scriptId: [" + scriptId + "]!";
} log.warn(message);
if (!isBlackListed(scriptId)) { return Futures.immediateFailedFuture(new RuntimeException(message));
try { }
Object result;
if (useJsSandbox()) { BlackListInfo blackListInfo = blackListedFunctions.get(new BlackListKey(scriptId, entityId));
result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); if (blackListInfo != null && blackListInfo.getCount() >= getMaxErrors()) {
} else { RuntimeException throwable = new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!", blackListInfo.getCause());
result = ((Invocable)engine).invokeFunction(functionName, args); throwable.printStackTrace();
} return Futures.immediateFailedFuture(throwable);
return Futures.immediateFuture(result); }
} catch (Exception e) {
blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet(); try {
return Futures.immediateFailedFuture(e); return invoke(functionName, args);
} } catch (Exception e) {
BlackListKey blackListKey = new BlackListKey(scriptId, entityId);
blackListedFunctions.computeIfAbsent(blackListKey, key -> new BlackListInfo()).incrementWithReason(e);
return Futures.immediateFailedFuture(e);
}
}
private ListenableFuture<Object> invoke(String functionName, Object... args) throws ScriptException, NoSuchMethodException {
Object result;
if (useJsSandbox()) {
result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
} else { } else {
return Futures.immediateFailedFuture( result = ((Invocable) engine).invokeFunction(functionName, args);
new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!"));
} }
return Futures.immediateFuture(result);
} }
@Override @Override
public ListenableFuture<Void> release(UUID scriptId) { public ListenableFuture<Void> release(UUID scriptId, EntityId entityId) {
AtomicInteger count = scriptIdToCount.get(scriptId); ScriptInfo scriptInfo = scriptIdToInfo.get(scriptId);
if(count != null) { if (scriptInfo == null) {
if(count.decrementAndGet() > 0) { log.warn("Script release called for not existing script id [{}]", scriptId);
return Futures.immediateFuture(null);
}
synchronized (scriptInfo.getLock()) {
int remainingDuplicates = scriptInfo.getCount().decrementAndGet();
if (remainingDuplicates > 0) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
}
String functionName = functionsMap.get(scriptId); String functionName = functionsMap.get(scriptId);
if (functionName != null) { if (functionName != null) {
try { try {
if (useJsSandbox()) { if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;"); sandbox.eval(functionName + " = undefined;");
} else { } else {
engine.eval(functionName + " = undefined;"); engine.eval(functionName + " = undefined;");
}
functionsMap.remove(scriptId);
blackListedFunctions.remove(new BlackListKey(scriptId, entityId));
} catch (ScriptException e) {
log.error("Could not release script [{}] [{}]", scriptId, remainingDuplicates);
return Futures.immediateFailedFuture(e);
} }
functionsMap.remove(scriptId); } else {
blackListedFunctions.remove(scriptId); log.warn("Function name do not exist for script [{}] [{}]", scriptId, remainingDuplicates);
} catch (ScriptException e) {
return Futures.immediateFailedFuture(e);
} }
} }
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
private boolean isBlackListed(UUID scriptId) {
if (blackListedFunctions.containsKey(scriptId)) {
AtomicInteger errorCount = blackListedFunctions.get(scriptId);
return errorCount.get() >= getMaxErrors();
} else {
return false;
}
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) { private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
switch (scriptType) { switch (scriptType) {
@ -174,15 +195,66 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
} }
} }
private Pair<UUID, AtomicInteger> deduplicate(JsScriptType scriptType, String scriptBody) { private ScriptInfo deduplicate(JsScriptType scriptType, String scriptBody) {
Pair<UUID, AtomicInteger> precomputed = Pair.of(UUID.randomUUID(), new AtomicInteger()); ScriptInfo meta = ScriptInfo.preInit();
String key = deduplicateKey(scriptType, scriptBody);
Pair<UUID, AtomicInteger> pair = scriptToId.computeIfAbsent(deduplicateKey(scriptType, scriptBody), i -> precomputed); ScriptInfo latestMeta = scriptKeyToInfo.computeIfAbsent(key, i -> meta);
AtomicInteger duplicateCount = scriptIdToCount.computeIfAbsent(pair.getLeft(), i -> pair.getRight()); return scriptIdToInfo.computeIfAbsent(latestMeta.getId(), i -> latestMeta);
return Pair.of(pair.getLeft(), duplicateCount);
} }
private String deduplicateKey(JsScriptType scriptType, String scriptBody) { private String deduplicateKey(JsScriptType scriptType, String scriptBody) {
return scriptType + "_" + scriptBody; return scriptType + "_" + scriptBody;
} }
@Getter
private static class ScriptInfo {
private final UUID id;
private final Object lock;
private final AtomicInteger count;
ScriptInfo(UUID id, Object lock, AtomicInteger count) {
this.id = id;
this.lock = lock;
this.count = count;
}
static ScriptInfo preInit() {
UUID preId = UUID.randomUUID();
AtomicInteger preCount = new AtomicInteger();
Object preLock = new Object();
return new ScriptInfo(preId, preLock, preCount);
}
}
@EqualsAndHashCode
@Getter
@RequiredArgsConstructor
private static class BlackListKey {
private final UUID scriptId;
private final EntityId entityId;
}
@Data
private static class BlackListInfo {
private final AtomicInteger count;
private Exception ex;
BlackListInfo() {
this.count = new AtomicInteger(0);
}
void incrementWithReason(Exception e) {
count.incrementAndGet();
ex = e;
}
int getCount() {
return count.get();
}
Exception getCause() {
return ex;
}
}
} }

5
application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java

@ -17,6 +17,7 @@
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.UUID; import java.util.UUID;
@ -24,8 +25,8 @@ public interface JsSandboxService {
ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames); ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args); ListenableFuture<Object> invokeFunction(UUID scriptId, EntityId entityId, Object... args);
ListenableFuture<Void> release(UUID scriptId); ListenableFuture<Void> release(UUID scriptId, EntityId entityId);
} }

13
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -39,9 +40,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private final JsSandboxService sandboxService; private final JsSandboxService sandboxService;
private final UUID scriptId; private final UUID scriptId;
private final EntityId entityId;
public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) { public RuleNodeJsScriptEngine(JsSandboxService sandboxService, EntityId entityId, String script, String... argNames) {
this.sandboxService = sandboxService; this.sandboxService = sandboxService;
this.entityId = entityId;
try { try {
this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
} catch (Exception e) { } catch (Exception e) {
@ -162,20 +165,20 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private JsonNode executeScript(TbMsg msg) throws ScriptException { private JsonNode executeScript(TbMsg msg) throws ScriptException {
try { try {
String[] inArgs = prepareArgs(msg); String[] inArgs = prepareArgs(msg);
String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); String eval = sandboxService.invokeFunction(this.scriptId, this.entityId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
return mapper.readTree(eval); return mapper.readTree(eval);
} catch (ExecutionException e) { } catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) { if (e.getCause() instanceof ScriptException) {
throw (ScriptException)e.getCause(); throw (ScriptException)e.getCause();
} else { } else {
throw new ScriptException("Failed to execute js script: " + e.getMessage()); throw new ScriptException(e);
} }
} catch (Exception e) { } catch (Exception e) {
throw new ScriptException("Failed to execute js script: " + e.getMessage()); throw new ScriptException(e);
} }
} }
public void destroy() { public void destroy() {
sandboxService.release(this.scriptId); sandboxService.release(this.scriptId, this.entityId);
} }
} }

2
application/src/main/resources/logback.xml

@ -17,7 +17,7 @@
--> -->
<!DOCTYPE configuration> <!DOCTYPE configuration>
<configuration> <configuration scan="true" scanPeriod="10 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>

95
application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java

@ -21,12 +21,18 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -35,6 +41,8 @@ public class RuleNodeJsScriptEngineTest {
private ScriptEngine scriptEngine; private ScriptEngine scriptEngine;
private TestNashornJsSandboxService jsSandboxService; private TestNashornJsSandboxService jsSandboxService;
private EntityId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3); jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
@ -48,7 +56,7 @@ public class RuleNodeJsScriptEngineTest {
@Test @Test
public void msgCanBeUpdated() throws ScriptException { public void msgCanBeUpdated() throws ScriptException {
String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
@ -65,7 +73,7 @@ public class RuleNodeJsScriptEngineTest {
@Test @Test
public void newAttributesCanBeAddedInMsg() throws ScriptException { public void newAttributesCanBeAddedInMsg() throws ScriptException {
String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -81,7 +89,7 @@ public class RuleNodeJsScriptEngineTest {
@Test @Test
public void payloadCanBeUpdated() throws ScriptException { public void payloadCanBeUpdated() throws ScriptException {
String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -99,7 +107,7 @@ public class RuleNodeJsScriptEngineTest {
@Test @Test
public void metadataAccessibleForFilter() throws ScriptException { public void metadataAccessibleForFilter() throws ScriptException {
String function = "return metadata.humidity < 15;"; String function = "return metadata.humidity < 15;";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -113,7 +121,7 @@ public class RuleNodeJsScriptEngineTest {
@Test @Test
public void dataAccessibleForFilter() throws ScriptException { public void dataAccessibleForFilter() throws ScriptException {
String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -134,7 +142,7 @@ public class RuleNodeJsScriptEngineTest {
"};\n" + "};\n" +
"\n" + "\n" +
"return nextRelation(metadata, msg);"; "return nextRelation(metadata, msg);";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -156,7 +164,7 @@ public class RuleNodeJsScriptEngineTest {
"};\n" + "};\n" +
"\n" + "\n" +
"return nextRelation(metadata, msg);"; "return nextRelation(metadata, msg);";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -168,4 +176,75 @@ public class RuleNodeJsScriptEngineTest {
scriptEngine.destroy(); scriptEngine.destroy();
} }
@Test
public void concurrentReleasedCorrectly() throws InterruptedException, ExecutionException {
String code = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
int repeat = 1000;
ExecutorService service = Executors.newFixedThreadPool(repeat);
Map<UUID, Object> scriptIds = new ConcurrentHashMap<>();
CountDownLatch startLatch = new CountDownLatch(repeat);
CountDownLatch finishLatch = new CountDownLatch(repeat);
AtomicInteger failedCount = new AtomicInteger(0);
for (int i = 0; i < repeat; i++) {
service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code));
}
finishLatch.await();
assertTrue(scriptIds.size() == 1);
assertTrue(failedCount.get() == 0);
CountDownLatch nextStart = new CountDownLatch(repeat);
CountDownLatch nextFinish = new CountDownLatch(repeat);
for (int i = 0; i < repeat; i++) {
service.submit(() -> runScript(nextStart, nextFinish, failedCount, scriptIds, code));
}
nextFinish.await();
assertTrue(scriptIds.size() == 1);
assertTrue(failedCount.get() == 0);
service.shutdownNow();
}
@Test
public void concurrentFailedEvaluationShouldThrowException() throws InterruptedException {
String code = "metadata.temp = metadata.temp * 10; urn {metadata: metadata};";
int repeat = 10000;
ExecutorService service = Executors.newFixedThreadPool(repeat);
Map<UUID, Object> scriptIds = new ConcurrentHashMap<>();
CountDownLatch startLatch = new CountDownLatch(repeat);
CountDownLatch finishLatch = new CountDownLatch(repeat);
AtomicInteger failedCount = new AtomicInteger(0);
for (int i = 0; i < repeat; i++) {
service.submit(() -> {
service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code));
});
}
finishLatch.await();
assertTrue(scriptIds.isEmpty());
assertEquals(repeat, failedCount.get());
service.shutdownNow();
}
private void runScript(CountDownLatch startLatch, CountDownLatch finishLatch, AtomicInteger failedCount,
Map<UUID, Object> scriptIds, String code) {
try {
for (int k = 0; k < 10; k++) {
startLatch.countDown();
startLatch.await();
UUID scriptId = jsSandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, code).get();
scriptIds.put(scriptId, new Object());
jsSandboxService.invokeFunction(scriptId, ruleNodeId, "{}", "{}", "TEXT").get();
jsSandboxService.release(scriptId, ruleNodeId).get();
}
} catch (Throwable th) {
failedCount.incrementAndGet();
} finally {
finishLatch.countDown();
}
}
} }

5
dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java

@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -238,7 +239,9 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
query.getKey(), query.getKey(),
query.getStartTs(), query.getStartTs(),
query.getEndTs(), query.getEndTs(),
new PageRequest(0, query.getLimit())))); new PageRequest(0, query.getLimit(),
new Sort(Sort.Direction.fromString(
query.getOrderBy()), "ts")))));
} }
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java

@ -35,7 +35,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
@Query("SELECT tskv FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " + @Query("SELECT tskv FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " +
"AND tskv.entityType = :entityType AND tskv.key = :entityKey " + "AND tskv.entityType = :entityType AND tskv.key = :entityKey " +
"AND tskv.ts > :startTs AND tskv.ts < :endTs ORDER BY tskv.ts DESC") "AND tskv.ts > :startTs AND tskv.ts < :endTs")
List<TsKvEntity> findAllWithLimit(@Param("entityId") String entityId, List<TsKvEntity> findAllWithLimit(@Param("entityId") String entityId,
@Param("entityType") EntityType entityType, @Param("entityType") EntityType entityType,
@Param("entityKey") String key, @Param("entityKey") String key,

23
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java

@ -26,10 +26,8 @@ import org.thingsboard.rule.engine.api.*;
import org.thingsboard.rule.engine.api.util.DonAsynchron; import org.thingsboard.rule.engine.api.util.DonAsynchron;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -39,8 +37,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.*;
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE;
import static org.thingsboard.server.common.data.kv.Aggregation.NONE; import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
/** /**
@ -64,6 +61,7 @@ public class TbGetTelemetryNode implements TbNode {
private long endTsOffset; private long endTsOffset;
private int limit; private int limit;
private ObjectMapper mapper; private ObjectMapper mapper;
private String fetchMode;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
@ -72,6 +70,7 @@ public class TbGetTelemetryNode implements TbNode {
startTsOffset = TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval()); startTsOffset = TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval());
endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval()); endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval());
limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1; limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1;
fetchMode = config.getFetchMode();
mapper = new ObjectMapper(); mapper = new ObjectMapper();
mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
@ -96,14 +95,18 @@ public class TbGetTelemetryNode implements TbNode {
} }
} }
//TODO: handle direction;
private List<ReadTsKvQuery> buildQueries() { private List<ReadTsKvQuery> buildQueries() {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
long startTs = ts - startTsOffset; long startTs = ts - startTsOffset;
long endTs = ts - endTsOffset; long endTs = ts - endTsOffset;
String orderBy;
if (fetchMode.equals(FETCH_MODE_FIRST) || fetchMode.equals(FETCH_MODE_ALL)) {
orderBy = "ASC";
} else {
orderBy = "DESC";
}
return tsKeyNames.stream() return tsKeyNames.stream()
.map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE)) .map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE, orderBy))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -116,7 +119,7 @@ public class TbGetTelemetryNode implements TbNode {
} }
for (String key : tsKeyNames) { for (String key : tsKeyNames) {
if(resultNode.has(key)){ if (resultNode.has(key)) {
msg.getMetaData().putValue(key, resultNode.get(key).toString()); msg.getMetaData().putValue(key, resultNode.get(key).toString());
} }
} }
@ -127,11 +130,11 @@ public class TbGetTelemetryNode implements TbNode {
} }
private void processArray(ObjectNode node, TsKvEntry entry) { private void processArray(ObjectNode node, TsKvEntry entry) {
if(node.has(entry.getKey())){ if (node.has(entry.getKey())) {
ArrayNode arrayNode = (ArrayNode) node.get(entry.getKey()); ArrayNode arrayNode = (ArrayNode) node.get(entry.getKey());
ObjectNode obj = buildNode(entry); ObjectNode obj = buildNode(entry);
arrayNode.add(obj); arrayNode.add(obj);
}else { } else {
ArrayNode arrayNode = mapper.createArrayNode(); ArrayNode arrayNode = mapper.createArrayNode();
ObjectNode obj = buildNode(entry); ObjectNode obj = buildNode(entry);
arrayNode.add(obj); arrayNode.add(obj);

6
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -113,6 +113,12 @@ public class CoapTransportResource extends CoapResource {
@Override @Override
public void handlePOST(CoapExchange exchange) { public void handlePOST(CoapExchange exchange) {
if(quotaService.isQuotaExceeded(exchange.getSourceAddress().getHostAddress())) {
log.warn("COAP Quota exceeded for [{}:{}] . Disconnect", exchange.getSourceAddress().getHostAddress(), exchange.getSourcePort());
exchange.respond(ResponseCode.BAD_REQUEST);
return;
}
Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest()); Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
if (!featureType.isPresent()) { if (!featureType.isPresent()) {
log.trace("Missing feature type parameter"); log.trace("Missing feature type parameter");

8
ui/src/app/api/subscription.js

@ -267,6 +267,14 @@ export default class Subscription {
} else { } else {
this.startWatchingTimewindow(); this.startWatchingTimewindow();
} }
registration = this.ctx.$scope.$watch(function () {
return subscription.alarmSearchStatus;
}, function (newAlarmSearchStatus, prevAlarmSearchStatus) {
if (!angular.equals(newAlarmSearchStatus, prevAlarmSearchStatus)) {
subscription.update();
}
}, true);
this.registrations.push(registration);
} }
initDataSubscription() { initDataSubscription() {

8
ui/src/app/components/datetime-period.tpl.html

@ -18,14 +18,14 @@
<section layout="column" layout-align="start start"> <section layout="column" layout-align="start start">
<section layout="row" layout-align="start start"> <section layout="row" layout-align="start start">
<mdp-date-picker ng-model="startDate" mdp-placeholder="{{ 'datetime.date-from' | translate }}" <mdp-date-picker ng-model="startDate" mdp-placeholder="{{ 'datetime.date-from' | translate }}"
mdp-max-date="maxStartDate"></mdp-date-picker> ></mdp-date-picker>
<mdp-time-picker ng-model="startDate" mdp-placeholder="{{ 'datetime.time-from' | translate }}" <mdp-time-picker ng-model="startDate" mdp-placeholder="{{ 'datetime.time-from' | translate }}"
mdp-max-date="maxStartDate" mdp-auto-switch="true"></mdp-time-picker> mdp-auto-switch="true"></mdp-time-picker>
</section> </section>
<section layout="row" layout-align="start start"> <section layout="row" layout-align="start start">
<mdp-date-picker ng-model="endDate" mdp-placeholder="{{ 'datetime.date-to' | translate }}" <mdp-date-picker ng-model="endDate" mdp-placeholder="{{ 'datetime.date-to' | translate }}"
mdp-min-date="minEndDate" mdp-max-date="maxEndDate"></mdp-date-picker> ></mdp-date-picker>
<mdp-time-picker ng-model="endDate" mdp-placeholder="{{ 'datetime.time-to' | translate }}" <mdp-time-picker ng-model="endDate" mdp-placeholder="{{ 'datetime.time-to' | translate }}"
mdp-min-date="minEndDate" mdp-max-date="maxEndDate" mdp-auto-switch="true"></mdp-time-picker> mdp-auto-switch="true"></mdp-time-picker>
</section> </section>
</section> </section>

1
ui/src/app/components/json-form.directive.js

@ -82,6 +82,7 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
val = undefined; val = undefined;
} }
selectOrSet(key, scope.model, val); selectOrSet(key, scope.model, val);
scope.formProps.model = scope.model;
}, },
onColorClick: function(event, key, val) { onColorClick: function(event, key, val) {
scope.showColorPicker(event, val); scope.showColorPicker(event, val);

68
ui/src/app/components/react/json-form-rc-select.jsx

@ -27,39 +27,90 @@ class ThingsboardRcSelect extends React.Component {
this.onDeselect = this.onDeselect.bind(this); this.onDeselect = this.onDeselect.bind(this);
this.onBlur = this.onBlur.bind(this); this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this); this.onFocus = this.onFocus.bind(this);
let emptyValue = this.props.form.schema.type === 'array'? [] : null;
this.state = { this.state = {
currentValue: this.props.value || emptyValue, currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'),
items: this.props.form.items, items: this.props.form.items,
focused: false focused: false
}; };
} }
keyToCurrentValue(key, isArray) {
var currentValue = isArray ? [] : null;
if (isArray) {
var keys = key;
if (keys) {
for (var i = 0; i < keys.length; i++) {
currentValue.push({key: keys[i], label: this.labelFromKey(keys[i])});
}
}
} else {
currentValue = {key: key, label: this.labelFromKey(key)};
}
return currentValue;
}
labelFromKey(key) {
let label = key || '';
if (key) {
for (var i=0;i<this.props.form.items.length;i++) {
var item = this.props.form.items[i];
if (item.value === key) {
label = item.label;
break;
}
}
}
return label;
}
arrayValues(items) {
var v = [];
if (items) {
for (var i = 0; i < items.length; i++) {
v.push(items[i].key);
}
}
return v;
}
keyIndex(values, key) {
var index = -1;
if (values) {
for (var i = 0; i < values.length; i++) {
if (values[i].key === key) {
index = i;
break;
}
}
}
return index;
}
onSelect(value, option) { onSelect(value, option) {
if(this.props.form.schema.type === 'array') { if(this.props.form.schema.type === 'array') {
let v = this.state.currentValue; let v = this.state.currentValue;
v.push(value); v.push(this.keyToCurrentValue(value.key, false));
this.setState({ this.setState({
currentValue: v currentValue: v
}); });
this.props.onChangeValidate(v); this.props.onChangeValidate(this.arrayValues(v));
} else { } else {
this.setState({currentValue: value}); this.setState({currentValue: this.keyToCurrentValue(value.key, false)});
this.props.onChangeValidate({target: {value: value}}); this.props.onChangeValidate({target: {value: value.key}});
} }
} }
onDeselect(value, option) { onDeselect(value, option) {
if (this.props.form.schema.type === 'array') { if (this.props.form.schema.type === 'array') {
let v = this.state.currentValue; let v = this.state.currentValue;
let index = v.indexOf(value); let index = this.keyIndex(v, value.key);
if (index > -1) { if (index > -1) {
v.splice(index, 1); v.splice(index, 1);
} }
this.setState({ this.setState({
currentValue: v currentValue: v
}); });
this.props.onChangeValidate(v); this.props.onChangeValidate(this.arrayValues(v));
} }
} }
@ -105,6 +156,7 @@ class ThingsboardRcSelect extends React.Component {
combobox={this.props.form.combobox} combobox={this.props.form.combobox}
disabled={this.props.form.readonly} disabled={this.props.form.readonly}
value={this.state.currentValue} value={this.state.currentValue}
labelInValue={true}
onSelect={this.onSelect} onSelect={this.onSelect}
onDeselect={this.onDeselect} onDeselect={this.onDeselect}
onFocus={this.onFocus} onFocus={this.onFocus}

11
ui/src/app/components/react/json-form-schema-form.jsx

@ -63,11 +63,15 @@ class ThingsboardSchemaForm extends React.Component {
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onColorClick = this.onColorClick.bind(this); this.onColorClick = this.onColorClick.bind(this);
this.hasConditions = false;
} }
onChange(key, val) { onChange(key, val) {
//console.log('SchemaForm.onChange', key, val); //console.log('SchemaForm.onChange', key, val);
this.props.onModelChange(key, val); this.props.onModelChange(key, val);
if (this.hasConditions) {
this.forceUpdate();
}
} }
onColorClick(event, key, val) { onColorClick(event, key, val) {
@ -81,8 +85,11 @@ class ThingsboardSchemaForm extends React.Component {
console.log('Invalid field: \"' + form.key[0] + '\"!'); console.log('Invalid field: \"' + form.key[0] + '\"!');
return null; return null;
} }
if(form.condition && eval(form.condition) === false) { if(form.condition) {
return null; this.hasConditions = true;
if (eval(form.condition) === false) {
return null;
}
} }
return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} mapper={mapper} builder={this.builder}/> return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} mapper={mapper} builder={this.builder}/>
} }

10
ui/src/app/locale/locale.constant-en_US.json

@ -133,8 +133,13 @@
"min-polling-interval-message": "At least 1 sec polling interval is allowed.", "min-polling-interval-message": "At least 1 sec polling interval is allowed.",
"aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }", "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }",
"aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?", "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?",
"aknowledge-alarm-title": "Acknowledge Alarm",
"aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?",
"clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }", "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }",
"clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?" "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?",
"clear-alarm-title": "Clear Alarm",
"clear-alarm-text": "Are you sure you want to clear Alarm?",
"alarm-status-filter": "Alarm Status Filter"
}, },
"alias": { "alias": {
"add": "Add alias", "add": "Add alias",
@ -754,7 +759,8 @@
"entity-name": "Entity name", "entity-name": "Entity name",
"details": "Entity details", "details": "Entity details",
"no-entities-prompt": "No entities found", "no-entities-prompt": "No entities found",
"no-data": "No data to display" "no-data": "No data to display",
"columns-to-display": "Columns to Display"
}, },
"entity-view": { "entity-view": {
"entity-view": "Entity View", "entity-view": "Entity View",

2
ui/src/app/widget/lib/CanvasDigitalGauge.js

@ -209,7 +209,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
this.elementValueClone.renderedValue = this._value; this.elementValueClone.renderedValue = this._value;
} }
if (angular.isUndefined(this.elementValueClone.renderedValue)) { if (angular.isUndefined(this.elementValueClone.renderedValue)) {
this.elementValueClone.renderedValue = options.minValue; this.elementValueClone.renderedValue = this.value;
} }
let context = this.contextValueClone; let context = this.contextValueClone;
// clear the cache // clear the cache

31
ui/src/app/widget/lib/alarm-status-filter-panel.scss

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2018 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-alarm-status-filter-panel {
min-width: 300px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
md-content {
overflow: hidden;
background-color: #fff;
}
}

28
ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-content style="height: 100%" flex layout="column" class="md-padding">
<label class="tb-title" translate>alarm.alarm-status-filter</label>
<md-radio-group ng-model="vm.subscription.alarmSearchStatus" class="md-primary">
<md-radio-button ng-value="searchStatus"
aria-label="{{ ('alarm.search-status.' + searchStatus) | translate }}"
class="md-primary md-align-top-left md-radio-interactive" ng-repeat="searchStatus in vm.types.alarmSearchStatus">
{{ ('alarm.search-status.' + searchStatus) | translate }}
</md-radio-button>
</md-radio-group>
</md-content>

188
ui/src/app/widget/lib/alarms-table-widget.js

@ -14,11 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import './alarms-table-widget.scss'; import './alarms-table-widget.scss';
import './display-columns-panel.scss';
import './alarm-status-filter-panel.scss';
/* eslint-disable import/no-unresolved, import/default */ /* eslint-disable import/no-unresolved, import/default */
import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html'; import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html';
import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html'; import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html';
import displayColumnsPanelTemplate from './display-columns-panel.tpl.html';
import alarmStatusFilterPanelTemplate from './alarm-status-filter-panel.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
@ -45,7 +49,7 @@ function AlarmsTableWidget() {
} }
/*@ngInject*/ /*@ngInject*/
function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, $timeout, alarmService, utils, types) { function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $mdPanel, $document, $translate, $q, $timeout, alarmService, utils, types) {
var vm = this; var vm = this;
vm.stylesInfo = {}; vm.stylesInfo = {};
@ -60,6 +64,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
vm.selectedAlarms = [] vm.selectedAlarms = []
vm.alarmSource = null; vm.alarmSource = null;
vm.alarmSearchStatus = null;
vm.allAlarms = []; vm.allAlarms = [];
vm.currentAlarm = null; vm.currentAlarm = null;
@ -95,14 +100,20 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
vm.onPaginate = onPaginate; vm.onPaginate = onPaginate;
vm.onRowClick = onRowClick; vm.onRowClick = onRowClick;
vm.onActionButtonClick = onActionButtonClick; vm.onActionButtonClick = onActionButtonClick;
vm.actionEnabled = actionEnabled;
vm.isCurrent = isCurrent; vm.isCurrent = isCurrent;
vm.openAlarmDetails = openAlarmDetails; vm.openAlarmDetails = openAlarmDetails;
vm.ackAlarms = ackAlarms; vm.ackAlarms = ackAlarms;
vm.ackAlarm = ackAlarm;
vm.clearAlarms = clearAlarms; vm.clearAlarms = clearAlarms;
vm.clearAlarm = clearAlarm;
vm.cellStyle = cellStyle; vm.cellStyle = cellStyle;
vm.cellContent = cellContent; vm.cellContent = cellContent;
vm.editAlarmStatusFilter = editAlarmStatusFilter;
vm.editColumnsToDisplay = editColumnsToDisplay;
$scope.$watch('vm.ctx', function() { $scope.$watch('vm.ctx', function() {
if (vm.ctx) { if (vm.ctx) {
vm.settings = vm.ctx.settings; vm.settings = vm.ctx.settings;
@ -158,7 +169,41 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
vm.ctx.widgetActions = [ vm.searchAction ]; vm.ctx.widgetActions = [ vm.searchAction ];
vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton'); vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true;
vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true;
vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true;
if (vm.displayDetails) {
vm.actionCellDescriptors.push(
{
displayName: $translate.instant('alarm.details'),
icon: 'more_horiz',
details: true
}
);
}
if (vm.allowAcknowledgment) {
vm.actionCellDescriptors.push(
{
displayName: $translate.instant('alarm.acknowledge'),
icon: 'done',
acknowledge: true
}
);
}
if (vm.allowClear) {
vm.actionCellDescriptors.push(
{
displayName: $translate.instant('alarm.clear'),
icon: 'clear',
clear: true
}
);
}
vm.actionCellDescriptors = vm.actionCellDescriptors.concat(vm.ctx.actionsApi.getActionDescriptors('actionCellButton'));
if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle); vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle);
@ -170,9 +215,6 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true;
vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true;
vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true;
vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true;
if (!vm.allowAcknowledgment && !vm.allowClear) { if (!vm.allowAcknowledgment && !vm.allowClear) {
vm.enableSelection = false; vm.enableSelection = false;
} }
@ -305,16 +347,35 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
} }
function onActionButtonClick($event, alarm, actionDescriptor) { function onActionButtonClick($event, alarm, actionDescriptor) {
if ($event) { if (actionDescriptor.details) {
$event.stopPropagation(); vm.openAlarmDetails($event, alarm);
} else if (actionDescriptor.acknowledge) {
vm.ackAlarm($event, alarm);
} else if (actionDescriptor.clear) {
vm.clearAlarm($event, alarm);
} else {
if ($event) {
$event.stopPropagation();
}
var entityId;
var entityName;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
}
vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm: alarm});
} }
var entityId; }
var entityName;
if (alarm && alarm.originator) { function actionEnabled(alarm, actionDescriptor) {
entityId = alarm.originator; if (actionDescriptor.acknowledge) {
entityName = alarm.originatorName; return (alarm.status == types.alarmStatus.activeUnack ||
alarm.status == types.alarmStatus.clearedUnack);
} else if (actionDescriptor.clear) {
return (alarm.status == types.alarmStatus.activeAck ||
alarm.status == types.alarmStatus.activeUnack);
} }
vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, { alarm: alarm }); return true;
} }
function isCurrent(alarm) { function isCurrent(alarm) {
@ -387,6 +448,25 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
} }
} }
function ackAlarm($event, alarm) {
if ($event) {
$event.stopPropagation();
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('alarm.aknowledge-alarm-title'))
.htmlContent($translate.instant('alarm.aknowledge-alarm-text'))
.ariaLabel($translate.instant('alarm.acknowledge'))
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
alarmService.ackAlarm(alarm.id.id).then(function () {
vm.selectedAlarms = [];
vm.subscription.update();
});
});
}
function clearAlarms($event) { function clearAlarms($event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -420,6 +500,24 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
} }
} }
function clearAlarm($event, alarm) {
if ($event) {
$event.stopPropagation();
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('alarm.clear-alarm-title'))
.htmlContent($translate.instant('alarm.clear-alarm-text'))
.ariaLabel($translate.instant('alarm.clear'))
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
alarmService.clearAlarm(alarm.id.id).then(function () {
vm.selectedAlarms = [];
vm.subscription.update();
});
});
}
function updateAlarms(preserveSelections) { function updateAlarms(preserveSelections) {
if (!preserveSelections) { if (!preserveSelections) {
@ -558,6 +656,54 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
} }
} }
function editAlarmStatusFilter($event) {
var element = angular.element($event.target);
var position = $mdPanel.newPanelPosition()
.relativeTo(element)
.addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW);
var config = {
attachTo: angular.element($document[0].body),
controller: AlarmStatusFilterPanelController,
controllerAs: 'vm',
templateUrl: alarmStatusFilterPanelTemplate,
panelClass: 'tb-alarm-status-filter-panel',
position: position,
fullscreen: false,
locals: {
'subscription': vm.subscription
},
openFrom: $event,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config);
}
function editColumnsToDisplay($event) {
var element = angular.element($event.target);
var position = $mdPanel.newPanelPosition()
.relativeTo(element)
.addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW);
var config = {
attachTo: angular.element($document[0].body),
controller: DisplayColumnsPanelController,
controllerAs: 'vm',
templateUrl: displayColumnsPanelTemplate,
panelClass: 'tb-display-columns-panel',
position: position,
fullscreen: false,
locals: {
'columns': vm.alarmSource.dataKeys
},
openFrom: $event,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config);
}
function updateAlarmSource() { function updateAlarmSource() {
vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle); vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle);
@ -570,6 +716,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
var dataKey = vm.alarmSource.dataKeys[d]; var dataKey = vm.alarmSource.dataKeys[d];
dataKey.title = utils.customTranslation(dataKey.label, dataKey.label); dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
dataKey.display = true;
var keySettings = dataKey.settings; var keySettings = dataKey.settings;
@ -619,3 +766,18 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
} }
} }
/*@ngInject*/
function DisplayColumnsPanelController(columns) { //eslint-disable-line
var vm = this;
vm.columns = columns;
}
/*@ngInject*/
function AlarmStatusFilterPanelController(subscription, types) { //eslint-disable-line
var vm = this;
vm.types = types;
vm.subscription = subscription;
}

30
ui/src/app/widget/lib/alarms-table-widget.scss

@ -44,6 +44,27 @@
&.tb-data-table { &.tb-data-table {
table.md-table, table.md-table,
table.md-table.md-row-select { table.md-table.md-row-select {
th.md-column {
&.tb-action-cell {
.md-button {
/* stylelint-disable-next-line selector-max-class */
&.md-icon-button {
width: 36px;
height: 36px;
padding: 6px;
margin: 0;
/* stylelint-disable-next-line selector-max-class */
md-icon {
width: 24px;
height: 24px;
font-size: 24px !important;
line-height: 24px !important;
}
}
}
}
}
tbody { tbody {
tr { tr {
td { td {
@ -51,6 +72,15 @@
width: 36px; width: 36px;
min-width: 36px; min-width: 36px;
max-width: 36px; max-width: 36px;
.md-button[disabled] {
&.md-icon-button {
/* stylelint-disable-next-line selector-max-class */
md-icon {
color: rgba(0, 0, 0, .38);
}
}
}
} }
} }
} }

40
ui/src/app/widget/lib/alarms-table-widget.tpl.html

@ -62,33 +62,45 @@
<table md-table md-row-select="vm.enableSelection" multiple="" ng-model="vm.selectedAlarms"> <table md-table md-row-select="vm.enableSelection" multiple="" ng-model="vm.selectedAlarms">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row> <tr md-row>
<th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.title }}</span></th> <th ng-if="key.display" md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.title }}</span></th>
<th md-column ng-if="vm.displayDetails"><span>&nbsp</span></th> <th md-column class="tb-action-cell" layout="row" layout-align="end center">
<th md-column ng-if="vm.actionCellDescriptors.length"><span>&nbsp</span></th> <md-button class="md-icon-button"
aria-label="{{'alarm.alarm-status-filter' | translate}}"
ng-click="vm.editAlarmStatusFilter($event)">
<md-icon aria-label="{{'alarm.alarm-status-filter' | translate}}"
class="material-icons">filter_list
</md-icon>
<md-tooltip md-direction="top">
{{'alarm.alarm-status-filter' | translate}}
</md-tooltip>
</md-button>
<md-button class="md-icon-button"
aria-label="{{'entity.columns-to-display' | translate}}"
ng-click="vm.editColumnsToDisplay($event)">
<md-icon aria-label="{{'entity.columns-to-display' | translate}}"
class="material-icons">view_column
</md-icon>
<md-tooltip md-direction="top">
{{'entity.columns-to-display' | translate}}
</md-tooltip>
</md-button>
</th>
</tr> </tr>
</thead> </thead>
<tbody md-body> <tbody md-body>
<tr ng-show="vm.alarms.length" md-row md-select="alarm" <tr ng-show="vm.alarms.length" md-row md-select="alarm"
md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms" md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms"
ng-click="vm.onRowClick($event, alarm)" ng-class="{'tb-current': vm.isCurrent(alarm)}"> ng-click="vm.onRowClick($event, alarm)" ng-class="{'tb-current': vm.isCurrent(alarm)}">
<td md-cell flex ng-repeat="key in vm.alarmSource.dataKeys" <td ng-if="key.display" md-cell flex ng-repeat="key in vm.alarmSource.dataKeys"
ng-style="vm.cellStyle(alarm, key)" ng-style="vm.cellStyle(alarm, key)"
ng-bind-html="vm.cellContent(alarm, key)"> ng-bind-html="vm.cellContent(alarm, key)">
</td> </td>
<td md-cell ng-if="vm.displayDetails" class="tb-action-cell"> <td md-cell class="tb-action-cell"
<md-button class="md-icon-button" aria-label="{{ 'alarm.details' | translate }}"
ng-click="vm.openAlarmDetails($event, alarm)" ng-disabled="$root.loading">
<md-icon aria-label="{{ 'alarm.details' | translate }}" class="material-icons">more_horiz</md-icon>
<md-tooltip md-direction="top">
{{ 'alarm.details' | translate }}
</md-tooltip>
</md-button>
</td>
<td md-cell ng-if="vm.actionCellDescriptors.length" class="tb-action-cell"
ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px', ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px',
maxWidth: vm.actionCellDescriptors.length*36+'px', maxWidth: vm.actionCellDescriptors.length*36+'px',
width: vm.actionCellDescriptors.length*36+'px'}"> width: vm.actionCellDescriptors.length*36+'px'}">
<md-button class="md-icon-button" ng-repeat="actionDescriptor in vm.actionCellDescriptors" <md-button class="md-icon-button" ng-repeat="actionDescriptor in vm.actionCellDescriptors"
ng-disabled="!vm.actionEnabled(alarm, actionDescriptor)"
aria-label="{{ actionDescriptor.displayName }}" aria-label="{{ actionDescriptor.displayName }}"
ng-click="vm.onActionButtonClick($event, alarm, actionDescriptor)" ng-disabled="$root.loading"> ng-click="vm.onActionButtonClick($event, alarm, actionDescriptor)" ng-disabled="$root.loading">
<md-icon aria-label="{{ actionDescriptor.displayName }}" class="material-icons">{{actionDescriptor.icon}}</md-icon> <md-icon aria-label="{{ actionDescriptor.displayName }}" class="material-icons">{{actionDescriptor.icon}}</md-icon>

31
ui/src/app/widget/lib/display-columns-panel.scss

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2018 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-display-columns-panel {
min-width: 300px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
md-content {
overflow: hidden;
background-color: #fff;
}
}

24
ui/src/app/widget/lib/display-columns-panel.tpl.html

@ -0,0 +1,24 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-content style="height: 100%" flex layout="column" class="md-padding">
<label class="tb-title" translate>entity.columns-to-display</label>
<md-checkbox aria-label="{{ 'entity.columns-to-display' | translate }}" ng-repeat="column in vm.columns"
ng-model="column.display">{{ column.title }}
</md-checkbox>
</md-content>

77
ui/src/app/widget/lib/entities-table-widget.js

@ -14,11 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import './entities-table-widget.scss'; import './entities-table-widget.scss';
import './display-columns-panel.scss';
/* eslint-disable import/no-unresolved, import/default */ /* eslint-disable import/no-unresolved, import/default */
import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html'; import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html';
//import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html'; //import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html';
import displayColumnsPanelTemplate from './display-columns-panel.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
@ -45,7 +47,7 @@ function EntitiesTableWidget() {
} }
/*@ngInject*/ /*@ngInject*/
function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, $timeout, utils, types) { function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types) {
var vm = this; var vm = this;
vm.stylesInfo = {}; vm.stylesInfo = {};
@ -98,6 +100,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
vm.cellStyle = cellStyle; vm.cellStyle = cellStyle;
vm.cellContent = cellContent; vm.cellContent = cellContent;
vm.editColumnsToDisplay = editColumnsToDisplay;
$scope.$watch('vm.ctx', function() { $scope.$watch('vm.ctx', function() {
if (vm.ctx && vm.ctx.defaultSubscription) { if (vm.ctx && vm.ctx.defaultSubscription) {
vm.settings = vm.ctx.settings; vm.settings = vm.ctx.settings;
@ -414,12 +418,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
} }
} }
function editColumnsToDisplay($event) {
var element = angular.element($event.target);
var position = $mdPanel.newPanelPosition()
.relativeTo(element)
.addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW);
var config = {
attachTo: angular.element($document[0].body),
controller: DisplayColumnsPanelController,
controllerAs: 'vm',
templateUrl: displayColumnsPanelTemplate,
panelClass: 'tb-display-columns-panel',
position: position,
fullscreen: false,
locals: {
'columns': vm.columns
},
openFrom: $event,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config);
}
function updateDatasources() { function updateDatasources() {
vm.stylesInfo = {}; vm.stylesInfo = {};
vm.contentsInfo = {}; vm.contentsInfo = {};
vm.columnWidth = {}; vm.columnWidth = {};
vm.dataKeys = []; vm.dataKeys = [];
vm.columns = [];
vm.allEntities = []; vm.allEntities = [];
var datasource; var datasource;
@ -429,6 +458,42 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle); vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle);
if (vm.displayEntityName) {
vm.columns.push(
{
name: 'entityName',
label: 'entityName',
title: vm.entityNameColumnTitle,
display: true
}
);
vm.contentsInfo['entityName'] = {
useCellContentFunction: false
};
vm.stylesInfo['entityName'] = {
useCellStyleFunction: false
};
vm.columnWidth['entityName'] = '0px';
}
if (vm.displayEntityType) {
vm.columns.push(
{
name: 'entityType',
label: 'entityType',
title: $translate.instant('entity.entity-type'),
display: true
}
);
vm.contentsInfo['entityType'] = {
useCellContentFunction: false
};
vm.stylesInfo['entityType'] = {
useCellStyleFunction: false
};
vm.columnWidth['entityType'] = '0px';
}
for (var d = 0; d < datasource.dataKeys.length; d++ ) { for (var d = 0; d < datasource.dataKeys.length; d++ ) {
dataKey = angular.copy(datasource.dataKeys[d]); dataKey = angular.copy(datasource.dataKeys[d]);
if (dataKey.type == types.dataKeyType.function) { if (dataKey.type == types.dataKeyType.function) {
@ -482,6 +547,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
vm.columnWidth[dataKey.label] = columnWidth; vm.columnWidth[dataKey.label] = columnWidth;
dataKey.display = true;
vm.columns.push(dataKey);
} }
for (var i=0;i<vm.datasources.length;i++) { for (var i=0;i<vm.datasources.length;i++) {
@ -512,3 +580,10 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
} }
} }
/*@ngInject*/
function DisplayColumnsPanelController(columns) { //eslint-disable-line
var vm = this;
vm.columns = columns;
}

30
ui/src/app/widget/lib/entities-table-widget.scss

@ -44,6 +44,27 @@
&.tb-data-table { &.tb-data-table {
table.md-table, table.md-table,
table.md-table.md-row-select { table.md-table.md-row-select {
th.md-column {
&.tb-action-cell {
.md-button {
/* stylelint-disable-next-line selector-max-class */
&.md-icon-button {
width: 36px;
height: 36px;
padding: 6px;
margin: 0;
/* stylelint-disable-next-line selector-max-class */
md-icon {
width: 24px;
height: 24px;
font-size: 24px !important;
line-height: 24px !important;
}
}
}
}
}
tbody { tbody {
tr { tr {
td { td {
@ -51,6 +72,15 @@
width: 36px; width: 36px;
min-width: 36px; min-width: 36px;
max-width: 36px; max-width: 36px;
.md-button[disabled] {
&.md-icon-button {
/* stylelint-disable-next-line selector-max-class */
md-icon {
color: rgba(0, 0, 0, .38);
}
}
}
} }
} }
} }

27
ui/src/app/widget/lib/entities-table-widget.tpl.html

@ -41,23 +41,30 @@
<table md-table> <table md-table>
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row> <tr md-row>
<th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span>{{vm.entityNameColumnTitle}}</span></th> <th ng-if="column.display" md-column md-order-by="{{ column.name }}" ng-repeat="column in vm.columns"><span>{{ column.title }}</span></th>
<th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th> <th md-column class="tb-action-cell" layout="row" layout-align="end center">
<th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th> <md-button class="md-icon-button"
<th md-column ng-if="vm.actionCellDescriptors.length"><span>&nbsp</span></th> aria-label="{{'entity.columns-to-display' | translate}}"
ng-click="vm.editColumnsToDisplay($event)">
<md-icon aria-label="{{'entity.columns-to-display' | translate}}"
class="material-icons">view_column
</md-icon>
<md-tooltip md-direction="top">
{{'entity.columns-to-display' | translate}}
</md-tooltip>
</md-button>
</th>
</tr> </tr>
</thead> </thead>
<tbody md-body> <tbody md-body>
<tr ng-show="vm.entities.length" md-row md-select="entity" <tr ng-show="vm.entities.length" md-row md-select="entity"
md-select-id="id.id" md-auto-select="false" ng-repeat="entity in vm.entities" md-select-id="id.id" md-auto-select="false" ng-repeat="entity in vm.entities"
ng-click="vm.onRowClick($event, entity)" ng-class="{'tb-current': vm.isCurrent(entity)}"> ng-click="vm.onRowClick($event, entity)" ng-class="{'tb-current': vm.isCurrent(entity)}">
<td md-cell flex ng-if="vm.displayEntityName">{{entity.entityName}}</td> <td ng-if="column.display" md-cell flex ng-repeat="column in vm.columns"
<td md-cell flex ng-if="vm.displayEntityType">{{entity.entityType}}</td> ng-style="vm.cellStyle(entity, column)"
<td md-cell flex ng-repeat="key in vm.dataKeys" ng-bind-html="vm.cellContent(entity, column)">
ng-style="vm.cellStyle(entity, key)"
ng-bind-html="vm.cellContent(entity, key)">
</td> </td>
<td md-cell ng-if="vm.actionCellDescriptors.length" class="tb-action-cell" <td md-cell class="tb-action-cell"
ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px', ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px',
maxWidth: vm.actionCellDescriptors.length*36+'px', maxWidth: vm.actionCellDescriptors.length*36+'px',
width: vm.actionCellDescriptors.length*36+'px'}"> width: vm.actionCellDescriptors.length*36+'px'}">

Loading…
Cancel
Save