thingsboard-aplcache

Changes

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");
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",
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>&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>
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>&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'}">