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. 190
      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. 79
      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
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

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));
logEntityAction(savedAlarm.getId(), savedAlarm,
getCurrentUser().getCustomerId(),
savedAlarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedAlarm;
} catch (Exception e) {
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 = "";
ScriptEngine engine = null;
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);
switch (scriptType) {
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.PathChildrenCacheEvent;
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.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode;
@ -127,12 +129,38 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress()));
log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
client.getConnectionStateListenable().addListener(checkReconnect(self));
} catch (Exception e) {
log.error("Failed to create ZK node", 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
public void unpublishCurrentServer() {
try {
@ -156,7 +184,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
.filter(cd -> !cd.getPath().equals(nodePath))
.map(cd -> {
try {
return new ServerInstance( (ServerAddress) SerializationUtils.deserialize(cd.getData()));
return new ServerInstance((ServerAddress) SerializationUtils.deserialize(cd.getData()));
} catch (NoSuchElementException e) {
log.error("Failed to decode ZK node", e);
throw new RuntimeException(e);
@ -198,7 +226,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
}
ServerInstance instance;
try {
ServerAddress serverAddress = SerializationUtils.deserialize(data.getData());
ServerAddress serverAddress = SerializationUtils.deserialize(data.getData());
instance = new ServerInstance(serverAddress);
} catch (SerializationException 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
* limitations under the License.
*/
package org.thingsboard.server.service.script;
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.NashornSandboxes;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
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.PreDestroy;
@ -44,9 +47,10 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
private ExecutorService monitorExecutorService;
private final Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
private final Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>();
private final Map<String, Pair<UUID, AtomicInteger>> scriptToId = new ConcurrentHashMap<>();
private final Map<UUID, AtomicInteger> scriptIdToCount = new ConcurrentHashMap<>();
private final Map<BlackListKey, BlackListInfo> blackListedFunctions = new ConcurrentHashMap<>();
private final Map<String, ScriptInfo> scriptKeyToInfo = new ConcurrentHashMap<>();
private final Map<UUID, ScriptInfo> scriptIdToInfo = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
@ -65,7 +69,7 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
@PreDestroy
public void stop() {
if (monitorExecutorService != null) {
if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow();
}
}
@ -80,90 +84,107 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
@Override
public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
Pair<UUID, AtomicInteger> deduplicated = deduplicate(scriptType, scriptBody);
UUID scriptId = deduplicated.getLeft();
AtomicInteger duplicateCount = deduplicated.getRight();
if(duplicateCount.compareAndSet(0, 1)) {
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
try {
if (useJsSandbox()) {
sandbox.eval(jsScript);
} else {
engine.eval(jsScript);
ScriptInfo scriptInfo = deduplicate(scriptType, scriptBody);
UUID scriptId = scriptInfo.getId();
AtomicInteger duplicateCount = scriptInfo.getCount();
synchronized (scriptInfo.getLock()) {
if (duplicateCount.compareAndSet(0, 1)) {
try {
evaluate(scriptId, scriptType, scriptBody, argNames);
} catch (Exception e) {
duplicateCount.decrementAndGet();
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
return Futures.immediateFailedFuture(e);
}
functionsMap.put(scriptId, functionName);
} catch (Exception e) {
duplicateCount.decrementAndGet();
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
return Futures.immediateFailedFuture(e);
} else {
duplicateCount.incrementAndGet();
}
} else {
duplicateCount.incrementAndGet();
}
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
public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) {
public ListenableFuture<Object> invokeFunction(UUID scriptId, EntityId entityId, Object... args) {
String functionName = functionsMap.get(scriptId);
if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
}
if (!isBlackListed(scriptId)) {
try {
Object result;
if (useJsSandbox()) {
result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
} else {
result = ((Invocable)engine).invokeFunction(functionName, args);
}
return Futures.immediateFuture(result);
} catch (Exception e) {
blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
return Futures.immediateFailedFuture(e);
}
String message = "No compiled script found for scriptId: [" + scriptId + "]!";
log.warn(message);
return Futures.immediateFailedFuture(new RuntimeException(message));
}
BlackListInfo blackListInfo = blackListedFunctions.get(new BlackListKey(scriptId, entityId));
if (blackListInfo != null && blackListInfo.getCount() >= getMaxErrors()) {
RuntimeException throwable = new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!", blackListInfo.getCause());
throwable.printStackTrace();
return Futures.immediateFailedFuture(throwable);
}
try {
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 {
return Futures.immediateFailedFuture(
new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!"));
result = ((Invocable) engine).invokeFunction(functionName, args);
}
return Futures.immediateFuture(result);
}
@Override
public ListenableFuture<Void> release(UUID scriptId) {
AtomicInteger count = scriptIdToCount.get(scriptId);
if(count != null) {
if(count.decrementAndGet() > 0) {
public ListenableFuture<Void> release(UUID scriptId, EntityId entityId) {
ScriptInfo scriptInfo = scriptIdToInfo.get(scriptId);
if (scriptInfo == null) {
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);
}
}
String functionName = functionsMap.get(scriptId);
if (functionName != null) {
try {
if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;");
} else {
engine.eval(functionName + " = undefined;");
String functionName = functionsMap.get(scriptId);
if (functionName != null) {
try {
if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;");
} else {
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);
blackListedFunctions.remove(scriptId);
} catch (ScriptException e) {
return Futures.immediateFailedFuture(e);
} else {
log.warn("Function name do not exist for script [{}] [{}]", scriptId, remainingDuplicates);
}
}
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) {
switch (scriptType) {
@ -174,15 +195,66 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
}
}
private Pair<UUID, AtomicInteger> deduplicate(JsScriptType scriptType, String scriptBody) {
Pair<UUID, AtomicInteger> precomputed = Pair.of(UUID.randomUUID(), new AtomicInteger());
Pair<UUID, AtomicInteger> pair = scriptToId.computeIfAbsent(deduplicateKey(scriptType, scriptBody), i -> precomputed);
AtomicInteger duplicateCount = scriptIdToCount.computeIfAbsent(pair.getLeft(), i -> pair.getRight());
return Pair.of(pair.getLeft(), duplicateCount);
private ScriptInfo deduplicate(JsScriptType scriptType, String scriptBody) {
ScriptInfo meta = ScriptInfo.preInit();
String key = deduplicateKey(scriptType, scriptBody);
ScriptInfo latestMeta = scriptKeyToInfo.computeIfAbsent(key, i -> meta);
return scriptIdToInfo.computeIfAbsent(latestMeta.getId(), i -> latestMeta);
}
private String deduplicateKey(JsScriptType scriptType, String 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;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.UUID;
@ -24,8 +25,8 @@ public interface JsSandboxService {
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 lombok.extern.slf4j.Slf4j;
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.TbMsgMetaData;
@ -39,9 +40,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private final JsSandboxService sandboxService;
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.entityId = entityId;
try {
this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
} catch (Exception e) {
@ -162,20 +165,20 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private JsonNode executeScript(TbMsg msg) throws ScriptException {
try {
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);
} catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) {
throw (ScriptException)e.getCause();
} else {
throw new ScriptException("Failed to execute js script: " + e.getMessage());
throw new ScriptException(e);
}
} catch (Exception e) {
throw new ScriptException("Failed to execute js script: " + e.getMessage());
throw new ScriptException(e);
}
}
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>
<configuration>
<configuration scan="true" scanPeriod="10 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<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.Test;
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.TbMsgMetaData;
import javax.script.ScriptException;
import java.util.Map;
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.*;
@ -35,6 +41,8 @@ public class RuleNodeJsScriptEngineTest {
private ScriptEngine scriptEngine;
private TestNashornJsSandboxService jsSandboxService;
private EntityId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
@Before
public void beforeTest() throws Exception {
jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
@ -48,7 +56,7 @@ public class RuleNodeJsScriptEngineTest {
@Test
public void msgCanBeUpdated() throws ScriptException {
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();
metaData.putValue("temp", "7");
@ -65,7 +73,7 @@ public class RuleNodeJsScriptEngineTest {
@Test
public void newAttributesCanBeAddedInMsg() throws ScriptException {
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();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");
@ -81,7 +89,7 @@ public class RuleNodeJsScriptEngineTest {
@Test
public void payloadCanBeUpdated() throws ScriptException {
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();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");
@ -99,7 +107,7 @@ public class RuleNodeJsScriptEngineTest {
@Test
public void metadataAccessibleForFilter() throws ScriptException {
String function = "return metadata.humidity < 15;";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");
@ -113,7 +121,7 @@ public class RuleNodeJsScriptEngineTest {
@Test
public void dataAccessibleForFilter() throws ScriptException {
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();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");
@ -134,7 +142,7 @@ public class RuleNodeJsScriptEngineTest {
"};\n" +
"\n" +
"return nextRelation(metadata, msg);";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10");
metaData.putValue("humidity", "99");
@ -156,7 +164,7 @@ public class RuleNodeJsScriptEngineTest {
"};\n" +
"\n" +
"return nextRelation(metadata, msg);";
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10");
metaData.putValue("humidity", "99");
@ -168,4 +176,75 @@ public class RuleNodeJsScriptEngineTest {
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.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.EntityId;
@ -238,7 +239,9 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
query.getKey(),
query.getStartTs(),
query.getEndTs(),
new PageRequest(0, query.getLimit()))));
new PageRequest(0, query.getLimit(),
new Sort(Sort.Direction.fromString(
query.getOrderBy()), "ts")))));
}
@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 " +
"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,
@Param("entityType") EntityType entityType,
@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.TbNodeUtils;
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.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@ -39,8 +37,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
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.MAX_FETCH_SIZE;
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.*;
import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
/**
@ -64,6 +61,7 @@ public class TbGetTelemetryNode implements TbNode {
private long endTsOffset;
private int limit;
private ObjectMapper mapper;
private String fetchMode;
@Override
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());
endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval());
limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1;
fetchMode = config.getFetchMode();
mapper = new ObjectMapper();
mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
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() {
long ts = System.currentTimeMillis();
long startTs = ts - startTsOffset;
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()
.map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE))
.map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE, orderBy))
.collect(Collectors.toList());
}
@ -116,7 +119,7 @@ public class TbGetTelemetryNode implements TbNode {
}
for (String key : tsKeyNames) {
if(resultNode.has(key)){
if (resultNode.has(key)) {
msg.getMetaData().putValue(key, resultNode.get(key).toString());
}
}
@ -127,11 +130,11 @@ public class TbGetTelemetryNode implements TbNode {
}
private void processArray(ObjectNode node, TsKvEntry entry) {
if(node.has(entry.getKey())){
if (node.has(entry.getKey())) {
ArrayNode arrayNode = (ArrayNode) node.get(entry.getKey());
ObjectNode obj = buildNode(entry);
arrayNode.add(obj);
}else {
} else {
ArrayNode arrayNode = mapper.createArrayNode();
ObjectNode obj = buildNode(entry);
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
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());
if (!featureType.isPresent()) {
log.trace("Missing feature type parameter");

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

@ -267,6 +267,14 @@ export default class Subscription {
} else {
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() {

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

@ -18,14 +18,14 @@
<section layout="column" layout-align="start start">
<section layout="row" layout-align="start start">
<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-max-date="maxStartDate" mdp-auto-switch="true"></mdp-time-picker>
mdp-auto-switch="true"></mdp-time-picker>
</section>
<section layout="row" layout-align="start start">
<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-min-date="minEndDate" mdp-max-date="maxEndDate" mdp-auto-switch="true"></mdp-time-picker>
mdp-auto-switch="true"></mdp-time-picker>
</section>
</section>

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

@ -82,6 +82,7 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
val = undefined;
}
selectOrSet(key, scope.model, val);
scope.formProps.model = scope.model;
},
onColorClick: function(event, key, 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.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
let emptyValue = this.props.form.schema.type === 'array'? [] : null;
this.state = {
currentValue: this.props.value || emptyValue,
currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'),
items: this.props.form.items,
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) {
if(this.props.form.schema.type === 'array') {
let v = this.state.currentValue;
v.push(value);
v.push(this.keyToCurrentValue(value.key, false));
this.setState({
currentValue: v
});
this.props.onChangeValidate(v);
this.props.onChangeValidate(this.arrayValues(v));
} else {
this.setState({currentValue: value});
this.props.onChangeValidate({target: {value: value}});
this.setState({currentValue: this.keyToCurrentValue(value.key, false)});
this.props.onChangeValidate({target: {value: value.key}});
}
}
onDeselect(value, option) {
if (this.props.form.schema.type === 'array') {
let v = this.state.currentValue;
let index = v.indexOf(value);
let index = this.keyIndex(v, value.key);
if (index > -1) {
v.splice(index, 1);
}
this.setState({
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}
disabled={this.props.form.readonly}
value={this.state.currentValue}
labelInValue={true}
onSelect={this.onSelect}
onDeselect={this.onDeselect}
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.onColorClick = this.onColorClick.bind(this);
this.hasConditions = false;
}
onChange(key, val) {
//console.log('SchemaForm.onChange', key, val);
this.props.onModelChange(key, val);
if (this.hasConditions) {
this.forceUpdate();
}
}
onColorClick(event, key, val) {
@ -81,8 +85,11 @@ class ThingsboardSchemaForm extends React.Component {
console.log('Invalid field: \"' + form.key[0] + '\"!');
return null;
}
if(form.condition && eval(form.condition) === false) {
return null;
if(form.condition) {
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}/>
}

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.",
"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-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-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": {
"add": "Add alias",
@ -754,7 +759,8 @@
"entity-name": "Entity name",
"details": "Entity details",
"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",

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

@ -209,7 +209,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
this.elementValueClone.renderedValue = this._value;
}
if (angular.isUndefined(this.elementValueClone.renderedValue)) {
this.elementValueClone.renderedValue = options.minValue;
this.elementValueClone.renderedValue = this.value;
}
let context = this.contextValueClone;
// 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>

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

@ -14,11 +14,15 @@
* limitations under the License.
*/
import './alarms-table-widget.scss';
import './display-columns-panel.scss';
import './alarm-status-filter-panel.scss';
/* eslint-disable import/no-unresolved, import/default */
import alarmsTableWidgetTemplate from './alarms-table-widget.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 */
@ -45,7 +49,7 @@ function AlarmsTableWidget() {
}
/*@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;
vm.stylesInfo = {};
@ -60,6 +64,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
vm.selectedAlarms = []
vm.alarmSource = null;
vm.alarmSearchStatus = null;
vm.allAlarms = [];
vm.currentAlarm = null;
@ -95,14 +100,20 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
vm.onPaginate = onPaginate;
vm.onRowClick = onRowClick;
vm.onActionButtonClick = onActionButtonClick;
vm.actionEnabled = actionEnabled;
vm.isCurrent = isCurrent;
vm.openAlarmDetails = openAlarmDetails;
vm.ackAlarms = ackAlarms;
vm.ackAlarm = ackAlarm;
vm.clearAlarms = clearAlarms;
vm.clearAlarm = clearAlarm;
vm.cellStyle = cellStyle;
vm.cellContent = cellContent;
vm.editAlarmStatusFilter = editAlarmStatusFilter;
vm.editColumnsToDisplay = editColumnsToDisplay;
$scope.$watch('vm.ctx', function() {
if (vm.ctx) {
vm.settings = vm.ctx.settings;
@ -158,7 +169,41 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
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) {
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.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) {
vm.enableSelection = false;
}
@ -305,16 +347,35 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
}
function onActionButtonClick($event, alarm, actionDescriptor) {
if ($event) {
$event.stopPropagation();
if (actionDescriptor.details) {
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) {
entityId = alarm.originator;
entityName = alarm.originatorName;
}
function actionEnabled(alarm, actionDescriptor) {
if (actionDescriptor.acknowledge) {
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) {
@ -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) {
if ($event) {
$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) {
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() {
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];
dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
dataKey.display = true;
var keySettings = dataKey.settings;
@ -618,4 +765,19 @@ 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 {
table.md-table,
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 {
tr {
td {
@ -51,6 +72,15 @@
width: 36px;
min-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">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
<th 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 ng-if="vm.actionCellDescriptors.length"><span>&nbsp</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 class="tb-action-cell" layout="row" layout-align="end center">
<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>
</thead>
<tbody md-body>
<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"
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-bind-html="vm.cellContent(alarm, key)">
</td>
<td md-cell ng-if="vm.displayDetails" 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"
<td md-cell class="tb-action-cell"
ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px',
maxWidth: vm.actionCellDescriptors.length*36+'px',
width: vm.actionCellDescriptors.length*36+'px'}">
<md-button class="md-icon-button" ng-repeat="actionDescriptor in vm.actionCellDescriptors"
ng-disabled="!vm.actionEnabled(alarm, actionDescriptor)"
aria-label="{{ actionDescriptor.displayName }}"
ng-click="vm.onActionButtonClick($event, alarm, actionDescriptor)" ng-disabled="$root.loading">
<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>

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

@ -14,11 +14,13 @@
* limitations under the License.
*/
import './entities-table-widget.scss';
import './display-columns-panel.scss';
/* eslint-disable import/no-unresolved, import/default */
import entitiesTableWidgetTemplate from './entities-table-widget.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 */
@ -45,7 +47,7 @@ function EntitiesTableWidget() {
}
/*@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;
vm.stylesInfo = {};
@ -98,6 +100,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
vm.cellStyle = cellStyle;
vm.cellContent = cellContent;
vm.editColumnsToDisplay = editColumnsToDisplay;
$scope.$watch('vm.ctx', function() {
if (vm.ctx && vm.ctx.defaultSubscription) {
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() {
vm.stylesInfo = {};
vm.contentsInfo = {};
vm.columnWidth = {};
vm.dataKeys = [];
vm.columns = [];
vm.allEntities = [];
var datasource;
@ -429,6 +458,42 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
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++ ) {
dataKey = angular.copy(datasource.dataKeys[d]);
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';
vm.columnWidth[dataKey.label] = columnWidth;
dataKey.display = true;
vm.columns.push(dataKey);
}
for (var i=0;i<vm.datasources.length;i++) {
@ -511,4 +579,11 @@ 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 {
table.md-table,
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 {
tr {
td {
@ -51,6 +72,15 @@
width: 36px;
min-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>
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
<th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span>{{vm.entityNameColumnTitle}}</span></th>
<th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th>
<th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th>
<th md-column ng-if="vm.actionCellDescriptors.length"><span>&nbsp</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 class="tb-action-cell" layout="row" layout-align="end center">
<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>
</thead>
<tbody md-body>
<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"
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 md-cell flex ng-if="vm.displayEntityType">{{entity.entityType}}</td>
<td md-cell flex ng-repeat="key in vm.dataKeys"
ng-style="vm.cellStyle(entity, key)"
ng-bind-html="vm.cellContent(entity, key)">
<td ng-if="column.display" md-cell flex ng-repeat="column in vm.columns"
ng-style="vm.cellStyle(entity, column)"
ng-bind-html="vm.cellContent(entity, column)">
</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',
maxWidth: vm.actionCellDescriptors.length*36+'px',
width: vm.actionCellDescriptors.length*36+'px'}">

Loading…
Cancel
Save