thingsboard-aplcache
Changes
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java 32(+30 -2)
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java 218(+145 -73)
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java 13(+8 -5)
application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java 95(+87 -8)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java 23(+13 -10)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java 6(+6 -0)
ui/src/app/api/subscription.js 8(+8 -0)
ui/src/app/widget/lib/alarms-table-widget.js 190(+176 -14)
Details
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index acfc269..4e779d4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/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
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index 586e8c3..00885e3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/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,
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 86b8fda..82ab170 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/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":
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
index c3ffbab..6f51cee 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
+++ b/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);
diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
index 724f652..1d89c9d 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
+++ b/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;
+ }
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java
index ee86c62..5e1c676 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java
+++ b/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);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
index 2ba87ec..767dc05 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
+++ b/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);
}
}
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 978a570..dcfc930 100644
--- a/application/src/main/resources/logback.xml
+++ b/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>
diff --git a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
index ea70384..88961dc 100644
--- a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
+++ b/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();
+ }
+ }
+
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 1eb2f00..7611386 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/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
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
index 2b39d25..4c743e5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
+++ b/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,
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
index fe35ef6..260669f 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
+++ b/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);
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
index bac68ff..8a596d7 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
+++ b/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");
ui/src/app/api/subscription.js 8(+8 -0)
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
index 350b1ef..a66c4b1 100644
--- a/ui/src/app/api/subscription.js
+++ b/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() {
diff --git a/ui/src/app/components/datetime-period.tpl.html b/ui/src/app/components/datetime-period.tpl.html
index 605c3a8..7479576 100644
--- a/ui/src/app/components/datetime-period.tpl.html
+++ b/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>
\ No newline at end of file
diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js
index 5749271..b016f57 100644
--- a/ui/src/app/components/json-form.directive.js
+++ b/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);
diff --git a/ui/src/app/components/react/json-form-rc-select.jsx b/ui/src/app/components/react/json-form-rc-select.jsx
index ffa0331..83fa929 100644
--- a/ui/src/app/components/react/json-form-rc-select.jsx
+++ b/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}
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
index e006eb9..09712f2 100644
--- a/ui/src/app/components/react/json-form-schema-form.jsx
+++ b/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}/>
}
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index eef7d89..481d677 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/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",
ui/src/app/widget/lib/alarms-table-widget.js 190(+176 -14)
diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js
index 0696a7b..6ae17e0 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.js
+++ b/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
}
}
-}
\ No newline at end of file
+}
+
+/*@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;
+}
diff --git a/ui/src/app/widget/lib/alarms-table-widget.scss b/ui/src/app/widget/lib/alarms-table-widget.scss
index 2922996..a031523 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.scss
+++ b/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);
+ }
+ }
+ }
}
}
}
diff --git a/ui/src/app/widget/lib/alarms-table-widget.tpl.html b/ui/src/app/widget/lib/alarms-table-widget.tpl.html
index 8480058..39843e1 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.tpl.html
+++ b/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> </span></th>
- <th md-column ng-if="vm.actionCellDescriptors.length"><span> </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>
diff --git a/ui/src/app/widget/lib/alarm-status-filter-panel.scss b/ui/src/app/widget/lib/alarm-status-filter-panel.scss
new file mode 100644
index 0000000..4bf6eee
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html b/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html
new file mode 100644
index 0000000..45e8414
--- /dev/null
+++ b/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>
diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js
index ee8b0ed..331ce6c 100644
--- a/ui/src/app/widget/lib/CanvasDigitalGauge.js
+++ b/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
diff --git a/ui/src/app/widget/lib/display-columns-panel.scss b/ui/src/app/widget/lib/display-columns-panel.scss
new file mode 100644
index 0000000..2e518cb
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/ui/src/app/widget/lib/display-columns-panel.tpl.html b/ui/src/app/widget/lib/display-columns-panel.tpl.html
new file mode 100644
index 0000000..42e2471
--- /dev/null
+++ b/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>
diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js
index d0b629d..620f313 100644
--- a/ui/src/app/widget/lib/entities-table-widget.js
+++ b/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
}
-}
\ No newline at end of file
+}
+
+/*@ngInject*/
+function DisplayColumnsPanelController(columns) { //eslint-disable-line
+
+ var vm = this;
+ vm.columns = columns;
+}
diff --git a/ui/src/app/widget/lib/entities-table-widget.scss b/ui/src/app/widget/lib/entities-table-widget.scss
index 85648d6..d745529 100644
--- a/ui/src/app/widget/lib/entities-table-widget.scss
+++ b/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);
+ }
+ }
+ }
}
}
}
diff --git a/ui/src/app/widget/lib/entities-table-widget.tpl.html b/ui/src/app/widget/lib/entities-table-widget.tpl.html
index 474d536..66932a0 100644
--- a/ui/src/app/widget/lib/entities-table-widget.tpl.html
+++ b/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> </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'}">