thingsboard-memoizeit

TB-64: Implement widget actions.

6/22/2017 11:40:26 AM

Changes

Details

diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
index 215da0f..dd93626 100644
--- a/ui/src/app/api/subscription.js
+++ b/ui/src/app/api/subscription.js
@@ -171,6 +171,48 @@ export default class Subscription {
         return deferred.promise;
     }
 
+    getFirstEntityInfo() {
+        var entityId;
+        var entityName;
+        if (this.type === this.ctx.types.widgetType.rpc.value) {
+            if (this.targetDeviceId) {
+                entityId = {
+                    entityType: this.ctx.entityType.device,
+                    id: this.targetDeviceId
+                }
+                entityName = this.targetDeviceName;
+            }
+        } else if (this.type == this.ctx.types.widgetType.alarm.value) {
+            if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) {
+                entityId = {
+                    entityType: this.alarmSource.entityType,
+                    id: this.alarmSource.entityId
+                }
+                entityName = this.alarmSource.entityName;
+            }
+        } else {
+            for (var i=0;i<this.datasources.length;i++) {
+                var datasource = this.datasources[i];
+                if (datasource && datasource.entityType && datasource.entityId) {
+                    entityId = {
+                        entityType: datasource.entityType,
+                        id: datasource.entityId
+                    }
+                    entityName = datasource.entityName;
+                    break;
+                }
+            }
+        }
+        if (entityId) {
+            return {
+                entityId: entityId,
+                entityName: entityName
+            };
+        } else {
+            return null;
+        }
+    }
+
     initAlarmSubscription() {
         var deferred = this.ctx.$q.defer();
         if (!this.ctx.aliasController) {
@@ -342,6 +384,7 @@ export default class Subscription {
                 function success(aliasInfo) {
                     if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType == subscription.ctx.types.entityType.device) {
                         subscription.targetDeviceId = aliasInfo.currentEntity.id;
+                        subscription.targetDeviceName = aliasInfo.currentEntity.name;
                         if (subscription.targetDeviceId) {
                             subscription.rpcEnabled = true;
                         } else {
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index e23e916..a58c307 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -626,7 +626,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
             }
             for (var actionSourceId in types.widgetActionSources) {
                 result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]);
-                result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name);
+                result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name) + '';
             }
 
             return result;
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 43cc0cf..61fc120 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -400,8 +400,9 @@ export default angular.module('thingsboard.types', [])
                 }
             },
             widgetActionSources: {
-                'headerButton': {
+                headerButton: {
                     name: 'widget-action.header-button',
+                    value: 'headerButton',
                     multiple: true
                 }
             },
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index e7252e0..7c9f97d 100644
--- a/ui/src/app/common/utils.service.js
+++ b/ui/src/app/common/utils.service.js
@@ -38,7 +38,11 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
         materialColors = [],
         materialIcons = [];
 
-    var commonUsedMaterialIcons = [ 'more_horiz', 'close', 'play_arrow' ];
+    var commonMaterialIcons = [ 'more_horiz', 'more_vert', 'open_in_new', 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
+        'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
+        'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
+        'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
+        'share', 'add', 'edit', 'done' ];
 
     predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));";
     predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));";
@@ -148,7 +152,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
         validateDatasources: validateDatasources,
         createKey: createKey,
         createLabelFromDatasource: createLabelFromDatasource,
-        insertVariable: insertVariable
+        insertVariable: insertVariable,
+        customTranslation: customTranslation
     }
 
     return service;
@@ -188,7 +193,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
     }
 
     function getCommonMaterialIcons() {
-        return commonUsedMaterialIcons;
+        return commonMaterialIcons;
     }
 
     function genMaterialColor(str) {
@@ -469,4 +474,16 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
         return result;
     }
 
+    function customTranslation(translationValue, defaultValue) {
+        var result = '';
+        var translationId = types.translate.customTranslationsPrefix + translationValue;
+        var translation = $translate.instant(translationId);
+        if (translation != translationId) {
+            result = translation + '';
+        } else {
+            result = defaultValue;
+        }
+        return result;
+    }
+
 }
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index e61abcc..1a4cdba 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -187,6 +187,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
     vm.showWidgetActions = showWidgetActions;
     vm.widgetTitleStyle = widgetTitleStyle;
     vm.widgetTitle = widgetTitle;
+    vm.customWidgetHeaderActions = customWidgetHeaderActions;
     vm.widgetActions = widgetActions;
     vm.dropWidgetShadow = dropWidgetShadow;
     vm.enableWidgetFullscreen = enableWidgetFullscreen;
@@ -875,6 +876,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         }
     }
 
+    function customWidgetHeaderActions(widget) {
+        var ctx = widgetContext(widget);
+        if (ctx && ctx.customHeaderActions && ctx.customHeaderActions.length) {
+            return ctx.customHeaderActions;
+        } else {
+            return [];
+        }
+    }
+
     function widgetActions(widget) {
         var ctx = widgetContext(widget);
         if (ctx && ctx.widgetActions && ctx.widgetActions.length) {
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index aea3dfc..5d46fea 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -52,6 +52,16 @@
 									<tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
 								</div>
 								<div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
+									<md-button ng-repeat="action in vm.customWidgetHeaderActions(widget)"
+											   aria-label="{{action.displayName}}"
+											   ng-show="!vm.isEdit"
+											   ng-click="action.onAction($event)"
+											   class="md-icon-button">
+										<md-tooltip md-direction="top">
+											{{action.displayName}}
+										</md-tooltip>
+										<ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
+									</md-button>
 									<md-button ng-repeat="action in vm.widgetActions(widget)"
 											   aria-label="{{ action.name | translate }}"
 											   ng-show="!vm.isEdit && action.show"
diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js
index 77e3c1c..59a56e1 100644
--- a/ui/src/app/components/dashboard-autocomplete.directive.js
+++ b/ui/src/app/components/dashboard-autocomplete.directive.js
@@ -87,23 +87,32 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
                 dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then(
                     function success(dashboard) {
                         scope.dashboard = dashboard;
+                        startWatchers();
                     },
                     function fail() {
                         scope.dashboard = null;
+                        scope.updateView();
+                        startWatchers();
                     }
                 );
             } else {
                 scope.dashboard = null;
+                startWatchers();
             }
         }
 
-        scope.$watch('dashboard', function () {
-            scope.updateView();
-        });
-
-        scope.$watch('disabled', function () {
-            scope.updateView();
-        });
+        function startWatchers() {
+            scope.$watch('dashboard', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    scope.updateView();
+                }
+            });
+            scope.$watch('disabled', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    scope.updateView();
+                }
+            });
+        }
 
         if (scope.selectFirstDashboard) {
             var pageLink = {limit: 1, textSearch: ''};
@@ -111,6 +120,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
                 var dashboards = result.data;
                 if (dashboards.length > 0) {
                     scope.dashboard = dashboards[0];
+                    scope.updateView();
                 }
             }, function fail() {
             });
diff --git a/ui/src/app/components/material-icon-select.directive.js b/ui/src/app/components/material-icon-select.directive.js
index 45ab2cd..f40d1c8 100644
--- a/ui/src/app/components/material-icon-select.directive.js
+++ b/ui/src/app/components/material-icon-select.directive.js
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import './material-icon-select.scss';
+
 import MaterialIconsDialogController from './material-icons-dialog.controller';
 
 /* eslint-disable import/no-unresolved, import/default */
diff --git a/ui/src/app/components/material-icon-select.scss b/ui/src/app/components/material-icon-select.scss
new file mode 100644
index 0000000..5196e11
--- /dev/null
+++ b/ui/src/app/components/material-icon-select.scss
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016-2017 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-material-icon-select {
+    md-icon {
+        padding: 4px;
+        margin: 8px 4px 4px;
+        cursor: pointer;
+        border: solid 1px rgba(0,0,0,0.27);
+    }
+    md-input-container {
+        margin-bottom: 0px;
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/material-icon-select.tpl.html b/ui/src/app/components/material-icon-select.tpl.html
index 77ef87f..c2bee3f 100644
--- a/ui/src/app/components/material-icon-select.tpl.html
+++ b/ui/src/app/components/material-icon-select.tpl.html
@@ -15,12 +15,10 @@
     limitations under the License.
 
 -->
-<div layout="row">
+<div class="tb-material-icon-select" layout="row">
     <md-icon class="material-icons" ng-click="openIconDialog($event)">{{icon}}</md-icon>
     <md-input-container flex>
-        <md-input-container class="md-block">
-            <label translate>icon.icon</label>
-            <input ng-click="openIconDialog($event)" ng-model="icon">
-        </md-input-container>
+        <label translate>icon.icon</label>
+        <input ng-mousedown="openIconDialog($event)" ng-model="icon">
     </md-input-container>
 </div>
\ No newline at end of file
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
index b273729..93ca06f 100644
--- a/ui/src/app/components/widget/action/manage-widget-actions.directive.js
+++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
@@ -42,7 +42,8 @@ function ManageWidgetActions() {
         scope: true,
         bindToController: {
             actionSources: '=',
-            widgetActions: '='
+            widgetActions: '=',
+            fetchDashboardStates: '&',
         },
         controller: ManageWidgetActionsController,
         controllerAs: 'vm',
@@ -55,7 +56,7 @@ function ManageWidgetActions() {
 
 /*@ngInject*/
 function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter,
-                              $translate, $timeout, types) {
+                              $translate, $timeout, utils, types) {
 
     let vm = this;
 
@@ -165,12 +166,30 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
         if (!isAdd) {
             prevActionId = action.id;
         }
+        var availableActionSources = {};
+        for (var id in vm.actionSources) {
+            var actionSource = vm.actionSources[id];
+            if (actionSource.multiple) {
+                availableActionSources[id] = actionSource;
+            } else {
+                if (!isAdd && action.actionSourceId == id) {
+                    availableActionSources[id] = actionSource;
+                } else {
+                    var result = $filter('filter')(vm.allActions, {actionSourceId: id});
+                    if (!result || !result.length) {
+                        availableActionSources[id] = actionSource;
+                    }
+                }
+            }
+        }
         $mdDialog.show({
             controller: 'WidgetActionDialogController',
             controllerAs: 'vm',
             templateUrl: widgetActionDialogTemplate,
             parent: angular.element($document[0].body),
-            locals: {isAdd: isAdd, actionSources: vm.actionSources, action: angular.copy(action)},
+            locals: {isAdd: isAdd, fetchDashboardStates: vm.fetchDashboardStates,
+                actionSources: availableActionSources, widgetActions: vm.widgetActions,
+                action: angular.copy(action)},
             skipHide: true,
             fullscreen: true,
             targetEvent: $event
@@ -189,7 +208,8 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
     }
 
     function saveAction(action, prevActionId) {
-        action.actionSourceName = vm.actionSources[action.actionSourceId].name;
+        var actionSourceName = vm.actionSources[action.actionSourceId].name;
+        action.actionSourceName = utils.customTranslation(actionSourceName, actionSourceName);
         action.typeName = $translate.instant(types.widgetActionTypes[action.type].name);
         var actionSourceId = action.actionSourceId;
         var widgetAction = angular.copy(action);
@@ -227,15 +247,10 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
             var actionSourceActions = vm.widgetActions[actionSourceId];
             for (var i=0;i<actionSourceActions.length;i++) {
                 var actionSourceAction = actionSourceActions[i];
-                var action = {
-                    id: actionSourceAction.id,
-                    actionSourceId: actionSourceId,
-                    actionSourceName: actionSource.name,
-                    name: actionSourceAction.name,
-                    icon: actionSourceAction.icon,
-                    type: actionSourceAction.type,
-                    typeName: $translate.instant(types.widgetActionTypes[actionSourceAction.type].name)
-                };
+                var action = angular.copy(actionSourceAction);
+                action.actionSourceId = actionSourceId;
+                action.actionSourceName = utils.customTranslation(actionSource.name, actionSource.name);
+                action.typeName = $translate.instant(types.widgetActionTypes[actionSourceAction.type].name);
                 vm.allActions.push(action);
             }
         }
diff --git a/ui/src/app/components/widget/action/widget-action-dialog.controller.js b/ui/src/app/components/widget/action/widget-action-dialog.controller.js
index 948308b..99a2f0b 100644
--- a/ui/src/app/components/widget/action/widget-action-dialog.controller.js
+++ b/ui/src/app/components/widget/action/widget-action-dialog.controller.js
@@ -15,14 +15,21 @@
  */
 
 /*@ngInject*/
-export default function WidgetActionDialogController($scope, $mdDialog, types, utils, isAdd, actionSources, action) {
+export default function WidgetActionDialogController($scope, $mdDialog, $filter, $q, dashboardService, dashboardUtils, types, utils,
+                                                     isAdd, fetchDashboardStates, actionSources, widgetActions, action) {
 
     var vm = this;
 
     vm.types = types;
 
     vm.isAdd = isAdd;
+    vm.fetchDashboardStates = fetchDashboardStates;
     vm.actionSources = actionSources;
+    vm.widgetActions = widgetActions;
+
+    vm.targetDashboardStateSearchText = '';
+
+    vm.selectedDashboardStateIds = [];
 
     if (vm.isAdd) {
         vm.action = {
@@ -32,15 +39,131 @@ export default function WidgetActionDialogController($scope, $mdDialog, types, u
         vm.action = action;
     }
 
+    vm.actionSourceName = actionSourceName;
+
+    vm.targetDashboardStateSearchTextChanged = function() {
+    }
+
+    vm.dashboardStateSearch = dashboardStateSearch;
     vm.cancel = cancel;
     vm.save = save;
 
+    $scope.$watch("vm.action.name", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.action.name != null) {
+            checkActionName();
+        }
+    });
+
+    $scope.$watch("vm.action.actionSourceId", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.action.actionSourceId != null) {
+            checkActionName();
+        }
+    });
+
+    $scope.$watch("vm.action.targetDashboardId", function() {
+        vm.selectedDashboardStateIds = [];
+        if (vm.action.targetDashboardId) {
+            dashboardService.getDashboard(vm.action.targetDashboardId).then(
+                function success(dashboard) {
+                    dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
+                    var states = dashboard.configuration.states;
+                    vm.selectedDashboardStateIds = Object.keys(states);
+                }
+            );
+        }
+    });
+
+    $scope.$watch('vm.action.type', function(newType) {
+        if (newType) {
+            switch (newType) {
+                case vm.types.widgetActionTypes.openDashboardState.value:
+                case vm.types.widgetActionTypes.updateDashboardState.value:
+                case vm.types.widgetActionTypes.openDashboard.value:
+                    if (angular.isUndefined(vm.action.setEntityId)) {
+                        vm.action.setEntityId = true;
+                    }
+                    break;
+            }
+        }
+    });
+
+    function checkActionName() {
+        var actionNameIsUnique = true;
+        if (vm.action.actionSourceId && vm.action.name) {
+            var sourceActions = vm.widgetActions[vm.action.actionSourceId];
+            if (sourceActions) {
+                var result = $filter('filter')(sourceActions, {name: vm.action.name}, true);
+                if (result && result.length && result[0].id !== vm.action.id) {
+                    actionNameIsUnique = false;
+                }
+            }
+        }
+        $scope.theForm.name.$setValidity('actionNameNotUnique', actionNameIsUnique);
+    }
+
+    function actionSourceName (actionSource) {
+        if (actionSource) {
+            return utils.customTranslation(actionSource.name, actionSource.name);
+        } else {
+            return '';
+        }
+    }
+
+    function dashboardStateSearch (query) {
+        if (vm.action.type == vm.types.widgetActionTypes.openDashboard.value) {
+            var deferred = $q.defer();
+            var result = query ? vm.selectedDashboardStateIds.filter(
+                createFilterForDashboardState(query)) : vm.selectedDashboardStateIds;
+            if (result && result.length) {
+                deferred.resolve(result);
+            } else {
+                deferred.resolve([query]);
+            }
+            return deferred.promise;
+        } else {
+            return vm.fetchDashboardStates({query: query});
+        }
+    }
+
+    function createFilterForDashboardState (query) {
+        var lowercaseQuery = angular.lowercase(query);
+        return function filterFn(stateId) {
+            return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
+        };
+    }
+
+    function cleanupAction(action) {
+        var result = {};
+        result.id = action.id;
+        result.actionSourceId = action.actionSourceId;
+        result.name = action.name;
+        result.icon = action.icon;
+        result.type = action.type;
+        switch (action.type) {
+            case vm.types.widgetActionTypes.openDashboardState.value:
+            case vm.types.widgetActionTypes.updateDashboardState.value:
+                result.targetDashboardStateId = action.targetDashboardStateId;
+                result.openRightLayout = action.openRightLayout;
+                result.setEntityId = action.setEntityId;
+                break;
+            case vm.types.widgetActionTypes.openDashboard.value:
+                result.targetDashboardId = action.targetDashboardId;
+                result.targetDashboardStateId = action.targetDashboardStateId;
+                result.setEntityId = action.setEntityId;
+                break;
+            case vm.types.widgetActionTypes.custom.value:
+                result.customFunction = action.customFunction;
+                break;
+        }
+        return result;
+    }
+
     function cancel() {
         $mdDialog.cancel();
     }
 
     function save() {
         $scope.theForm.$setPristine();
-        $mdDialog.hide(vm.action);
+        $mdDialog.hide(cleanupAction(vm.action));
     }
 }
diff --git a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html
index ccb9d41..7a20e74 100644
--- a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html
+++ b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html
@@ -31,12 +31,12 @@
         <md-dialog-content>
             <div class="md-dialog-content">
                 <md-content class="md-padding" layout="column">
-                    <fieldset ng-disabled="loading">
+                    <fieldset ng-disabled="loading" layout="column">
                         <md-input-container class="md-block">
                             <label translate>widget-config.action-source</label>
                             <md-select name="actionSource" required aria-label="{{ 'widget-config.action-source' | translate }}" ng-model="vm.action.actionSourceId">
                                 <md-option ng-repeat="(actionSourceId, actionSource) in vm.actionSources" ng-value="actionSourceId">
-                                    {{actionSource.name}}
+                                    {{vm.actionSourceName(actionSource)}}
                                 </md-option>
                             </md-select>
                             <div ng-messages="theForm.actionSource.$error">
@@ -48,6 +48,7 @@
                             <input name="name" required ng-model="vm.action.name">
                             <div ng-messages="theForm.name.$error">
                                 <div ng-message="required" translate>widget-config.action-name-required</div>
+                                <div ng-message="actionNameNotUnique" translate>widget-config.action-name-not-unique</div>
                             </div>
                         </md-input-container>
                         <tb-material-icon-select ng-model="vm.action.icon">
@@ -63,6 +64,57 @@
                                 <div ng-message="required" translate>widget-config.action-type-required</div>
                             </div>
                         </md-input-container>
+                        <div layout="column"
+                             style="padding-bottom: 20px;"
+                             ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboard.value">
+                            <div class="md-caption tb-required"
+                                 style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
+                            <tb-dashboard-autocomplete the-form="theForm"
+                                                       tb-required="true"
+                                                       ng-model="vm.action.targetDashboardId"
+                                                       select-first-dashboard="false">
+                            </tb-dashboard-autocomplete>
+                        </div>
+                        <md-autocomplete ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
+                                                vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
+                                                vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
+                                         ng-required="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value"
+                                         md-no-cache="true"
+                                         md-input-name="targetDashboardState"
+                                         ng-model="vm.action.targetDashboardStateId"
+                                         md-selected-item="vm.action.targetDashboardStateId"
+                                         md-search-text="vm.targetDashboardStateSearchText"
+                                         md-search-text-change="vm.targetDashboardStateSearchTextChanged()"
+                                         md-items="item in vm.dashboardStateSearch(vm.targetDashboardStateSearchText)"
+                                         md-item-text="item"
+                                         md-min-length="0"
+                                         md-floating-label="{{ 'widget-action.target-dashboard-state' | translate }}"
+                                         md-select-on-match="true">
+                            <md-item-template>
+                                <div>
+                                    <span md-highlight-text="vm.targetDashboardStateSearchText" md-highlight-flags="^i">{{item}}</span>
+                                </div>
+                            </md-item-template>
+                            <div ng-messages="theForm.targetDashboardState.$error">
+                                <div translate ng-message="required">widget-action.target-dashboard-state-required</div>
+                            </div>
+                        </md-autocomplete>
+                        <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
+                                            vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value"
+                                     flex aria-label="{{ 'widget-action.open-right-layout' | translate }}"
+                                     ng-model="vm.action.openRightLayout">{{ 'widget-action.open-right-layout' | translate }}
+                        </md-checkbox>
+                        <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
+                                            vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
+                                            vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
+                                     flex aria-label="{{ 'widget-action.set-entity-from-widget' | translate }}"
+                                     ng-model="vm.action.setEntityId">{{ 'widget-action.set-entity-from-widget' | translate }}
+                        </md-checkbox>
+                        <tb-js-func ng-if="vm.action.type == vm.types.widgetActionTypes.custom.value"
+                                    ng-model="vm.action.customFunction"
+                                    function-args="{{ ['$event', 'widgetContext', 'entityId'] }}"
+                                    validation-args="{{ [] }}">
+                        </tb-js-func>
                     </fieldset>
                 </md-content>
             </div>
diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js
index b2877f3..113399a 100644
--- a/ui/src/app/components/widget/widget.controller.js
+++ b/ui/src/app/components/widget/widget.controller.js
@@ -20,7 +20,7 @@ import Subscription from '../../api/subscription';
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
-export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
+export default function WidgetController($scope, $state, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
                                          datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow,
                                          dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
 
@@ -44,6 +44,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
 
     var cafs = {};
 
+    var actionDescriptorsBySourceId = {};
+    if (widget.config.actions) {
+        for (var actionSourceId in widget.config.actions) {
+            var descriptors = widget.config.actions[actionSourceId];
+            var actionDescriptors = [];
+            descriptors.forEach(function(descriptor) {
+                var actionDescriptor = angular.copy(descriptor);
+                actionDescriptor.displayName = utils.customTranslation(descriptor.name, descriptor.name);
+                actionDescriptors.push(actionDescriptor);
+            });
+            actionDescriptorsBySourceId[actionSourceId] = actionDescriptors;
+        }
+    }
+
     var widgetContext = {
         inited: false,
         $container: null,
@@ -103,9 +117,32 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         utils: {
             formatValue: formatValue
         },
+        actionsApi: {
+            actionDescriptorsBySourceId: actionDescriptorsBySourceId,
+            getActionDescriptors: getActionDescriptors,
+            handleWidgetAction: handleWidgetAction
+        },
         stateController: stateController
     };
 
+    widgetContext.customHeaderActions = [];
+    var headerActionsDescriptors = getActionDescriptors(types.widgetActionSources.headerButton.value);
+    for (var i=0;i<headerActionsDescriptors.length;i++) {
+        var descriptor = headerActionsDescriptors[i];
+        var headerAction = {};
+        headerAction.name = descriptor.name;
+        headerAction.displayName = descriptor.displayName;
+        headerAction.icon = descriptor.icon;
+        headerAction.descriptor = descriptor;
+        headerAction.onAction = function($event) {
+            var entityInfo = getFirstEntityInfo();
+            var entityId = entityInfo ? entityInfo.entityId : null;
+            var entityName = entityInfo ? entityInfo.entityName : null;
+            handleWidgetAction($event, this.descriptor, entityId, entityName);
+        }
+        widgetContext.customHeaderActions.push(headerAction);
+    }
+
     var subscriptionContext = {
         $scope: $scope,
         $q: $q,
@@ -376,6 +413,87 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         return deferred.promise;
     }
 
+    function getActionDescriptors(actionSourceId) {
+        var result = widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId];
+        if (!result) {
+            result = [];
+        }
+        return result;
+    }
+
+    function handleWidgetAction($event, descriptor, entityId, entityName) {
+        var type = descriptor.type;
+        switch (type) {
+            case types.widgetActionTypes.openDashboardState.value:
+            case types.widgetActionTypes.updateDashboardState.value:
+                var targetDashboardStateId = descriptor.targetDashboardStateId;
+                var targetEntityId;
+                if (descriptor.setEntityId) {
+                    targetEntityId = entityId;
+                }
+                var params = {};
+                if (targetEntityId) {
+                    params.entityId = targetEntityId;
+                    if (entityName) {
+                        params.entityName = entityName;
+                    }
+                }
+                if (type == types.widgetActionTypes.openDashboardState.value) {
+                    widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
+                } else {
+                    widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout);
+                }
+                break;
+            case types.widgetActionTypes.openDashboard.value:
+                var targetDashboardId = descriptor.targetDashboardId;
+                targetDashboardStateId = descriptor.targetDashboardStateId;
+                targetEntityId;
+                if (descriptor.setEntityId) {
+                    targetEntityId = entityId;
+                }
+                var stateObject = {};
+                stateObject.params = {};
+                if (targetEntityId) {
+                    stateObject.params.entityId = targetEntityId;
+                    if (entityName) {
+                        stateObject.params.entityName = entityName;
+                    }
+                }
+                if (targetDashboardStateId) {
+                    stateObject.id = targetDashboardStateId;
+                }
+                var stateParams = {
+                    dashboardId: targetDashboardId,
+                    state: angular.toJson([ stateObject ])
+                }
+                $state.go('home.dashboards.dashboard', stateParams);
+                break;
+            case types.widgetActionTypes.custom.value:
+                var customFunction = descriptor.customFunction;
+                if (angular.isDefined(customFunction) && customFunction.length > 0) {
+                    try {
+                        var customActionFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', customFunction);
+                        customActionFunction($event, widgetContext, entityId, entityName);
+                    } catch (e) {
+                        //
+                    }
+                }
+                break;
+        }
+    }
+
+    function getFirstEntityInfo() {
+        var entityInfo;
+        for (var id in widgetContext.subscriptions) {
+            var subscription = widgetContext.subscriptions[id];
+            entityInfo = subscription.getFirstEntityInfo();
+            if (entityInfo) {
+                break;
+            }
+        }
+        return entityInfo;
+    }
+
     function configureWidgetElement() {
 
         $scope.displayLegend = angular.isDefined(widget.config.showLegend) ?
diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js
index 3df9263..afdaf8d 100644
--- a/ui/src/app/components/widget/widget-config.directive.js
+++ b/ui/src/app/components/widget/widget-config.directive.js
@@ -468,6 +468,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
             aliasController: '=',
             functionsOnly: '=',
             fetchEntityKeys: '&',
+            fetchDashboardStates: '&',
             onCreateEntityAlias: '&',
             theForm: '='
         },
diff --git a/ui/src/app/components/widget/widget-config.tpl.html b/ui/src/app/components/widget/widget-config.tpl.html
index 2cc264c..f2ee0f1 100644
--- a/ui/src/app/components/widget/widget-config.tpl.html
+++ b/ui/src/app/components/widget/widget-config.tpl.html
@@ -277,7 +277,10 @@
     </md-tab>
     <md-tab label="{{ 'widget-config.actions' | translate }}">
         <md-content class="md-padding" layout="column">
-            <tb-manage-widget-actions action-sources="actionSources" widget-actions="actions">
+            <tb-manage-widget-actions
+                    action-sources="actionSources"
+                    widget-actions="actions"
+                    fetch-dashboard-states="fetchDashboardStates({query: query})">
             </tb-manage-widget-actions>
         </md-content>
     </md-tab>
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index fd2ada7..c15492d 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -36,6 +36,7 @@ export default function AddWidgetController($scope, widgetService, entityService
     vm.add = add;
     vm.cancel = cancel;
     vm.fetchEntityKeys = fetchEntityKeys;
+    vm.fetchDashboardStates = fetchDashboardStates;
     vm.createEntityAlias = createEntityAlias;
 
     vm.widgetConfig = {
@@ -128,6 +129,26 @@ export default function AddWidgetController($scope, widgetService, entityService
         return deferred.promise;
     }
 
+    function fetchDashboardStates (query) {
+        var deferred = $q.defer();
+        var stateIds = Object.keys(vm.dashboard.configuration.states);
+        var result = query ? stateIds.filter(
+            createFilterForDashboardState(query)) : stateIds;
+        if (result && result.length) {
+            deferred.resolve(result);
+        } else {
+            deferred.resolve([query]);
+        }
+        return deferred.promise;
+    }
+
+    function createFilterForDashboardState (query) {
+        var lowercaseQuery = angular.lowercase(query);
+        return function filterFn(stateId) {
+            return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
+        };
+    }
+
     function createEntityAlias (event, alias, allowedEntityTypes) {
 
         var deferred = $q.defer();
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index 3920f91..4ca29f0 100644
--- a/ui/src/app/dashboard/add-widget.tpl.html
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -42,6 +42,7 @@
                                       alias-controller="vm.aliasController"
                                       functions-only="vm.functionsOnly"
                                       fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
+                                      fetch-dashboard-states="vm.fetchDashboardStates(query)"
                                       on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)"
                                       the-form="theForm"></tb-widget-config>
                 </fieldset>
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 5d32706..9bf1548 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -94,6 +94,26 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
             return deferred.promise;
         };
 
+        scope.fetchDashboardStates = function(query) {
+            var deferred = $q.defer();
+            var stateIds = Object.keys(scope.dashboard.configuration.states);
+            var result = query ? stateIds.filter(
+                createFilterForDashboardState(query)) : stateIds;
+            if (result && result.length) {
+                deferred.resolve(result);
+            } else {
+                deferred.resolve([query]);
+            }
+            return deferred.promise;
+        }
+
+        function createFilterForDashboardState (query) {
+            var lowercaseQuery = angular.lowercase(query);
+            return function filterFn(stateId) {
+                return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
+            };
+        }
+
         scope.createEntityAlias = function (event, alias, allowedEntityTypes) {
 
             var deferred = $q.defer();
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index c6bfd5a..1f724a8 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -26,6 +26,7 @@
 					  alias-controller="aliasController"
 					  functions-only="widgetEditMode"
 					  fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
+					  fetch-dashboard-states="fetchDashboardStates(query)"
 					  on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
 					  the-form="theForm"></tb-widget-config>
 </fieldset>
diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js
index 716d333..d39478f 100644
--- a/ui/src/app/dashboard/states/default-state-controller.js
+++ b/ui/src/app/dashboard/states/default-state-controller.js
@@ -15,7 +15,7 @@
  */
 
 /*@ngInject*/
-export default function DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) {
+export default function DefaultStateController($scope, $location, $state, $stateParams, utils, types, dashboardUtils) {
 
     var vm = this;
 
@@ -50,6 +50,9 @@ export default function DefaultStateController($scope, $location, $state, $state
     }
 
     function updateState(id, params, openRightLayout) {
+        if (!id) {
+            id = getStateId();
+        }
         if (vm.states && vm.states[id]) {
             if (!params) {
                 params = {};
@@ -110,15 +113,7 @@ export default function DefaultStateController($scope, $location, $state, $state
     }
 
     function getStateName(id, state) {
-        var result = '';
-        var translationId = types.translate.customTranslationsPrefix + state.name;
-        var translation = $translate.instant(translationId);
-        if (translation != translationId) {
-            result = translation + '';
-        } else {
-            result = id;
-        }
-        return result;
+        return utils.customTranslation(state.name, id);
     }
 
     function parseState(stateJson) {
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
index dd295cc..4510449 100644
--- a/ui/src/app/dashboard/states/entity-state-controller.js
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -55,6 +55,9 @@ export default function EntityStateController($scope, $location, $state, $stateP
     }
 
     function updateState(id, params, openRightLayout) {
+        if (!id) {
+            id = getStateId();
+        }
         if (vm.states && vm.states[id]) {
             resolveEntity(params).then(
                 function success(entityName) {
@@ -121,17 +124,10 @@ export default function EntityStateController($scope, $location, $state, $stateP
         var result = '';
         if (vm.stateObject[index]) {
             var stateName = vm.states[vm.stateObject[index].id].name;
-            var translationId = types.translate.customTranslationsPrefix + stateName;
-            var translation = $translate.instant(translationId);
-            if (translation != translationId) {
-                stateName = translation + '';
-            }
+            stateName = utils.customTranslation(stateName, stateName);
             var params = vm.stateObject[index].params;
-            if (params && params.entityName) {
-                result = utils.insertVariable(stateName, 'entityName', params.entityName);
-            } else {
-                result = stateName;
-            }
+            var entityName = params && params.entityName ? params.entityName : '';
+            result = utils.insertVariable(stateName, 'entityName', entityName);
         }
         return result;
     }
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
index bcf37dd..a133829 100644
--- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -102,6 +102,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
         if (vm.addToDashboardType === 0) {
             dashboardService.getDashboard(vm.dashboardId).then(
                 function success(dashboard) {
+                    dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
                     selectTargetState($event, dashboard).then(
                         function(targetState) {
                             selectTargetLayout($event, dashboard, targetState).then(
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 63f20e5..4d4ebe2 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1104,11 +1104,16 @@ export default angular.module('thingsboard.locale', [])
                     "export": "Export widget"
                 },
                 "widget-action": {
-                    "header-button": "Header button",
+                    "header-button": "Widget header button",
                     "open-dashboard-state": "Navigate to new dashboard state",
                     "update-dashboard-state": "Update current dashboard state",
                     "open-dashboard": "Navigate to other dashboard",
-                    "custom": "Custom action"
+                    "custom": "Custom action",
+                    "target-dashboard-state": "Target dashboard state",
+                    "target-dashboard-state-required": "Target dashboard state is required",
+                    "set-entity-from-widget": "Set entity from widget",
+                    "target-dashboard": "Target dashboard",
+                    "open-right-layout": "Open right dashboard layout (mobile view)"
                 },
                 "widgets-bundle": {
                     "current": "Current bundle",
@@ -1174,6 +1179,7 @@ export default angular.module('thingsboard.locale', [])
                     "action-source-required": "Action source is required.",
                     "action-name": "Name",
                     "action-name-required": "Action name is required.",
+                    "action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.",
                     "action-icon": "Icon",
                     "action-type": "Type",
                     "action-type-required": "Action type is required.",
@@ -1205,6 +1211,10 @@ export default angular.module('thingsboard.locale', [])
                     "es_ES": "Spanish"
                 },
                 "custom": {
+                    "widget-action": {
+                        "action-cell-button": "Action cell button",
+                        "row-click": "On row click"
+                    }
                 }
             }
         }
diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js
index 5a65af2..7618e6a 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.js
+++ b/ui/src/app/widget/lib/alarms-table-widget.js
@@ -158,13 +158,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
         vm.ctx.widgetActions = [ vm.searchAction ];
 
         if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
-            var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle;
-            var translation = $translate.instant(translationId);
-            if (translation != translationId) {
-                vm.alarmsTitle = translation + '';
-            } else {
-                vm.alarmsTitle = vm.settings.alarmsTitle;
-            }
+            vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle);
         } else {
             vm.alarmsTitle = $translate.instant('alarm.alarms');
         }
@@ -226,6 +220,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
             'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
             'border-color: ' + mdDarkSecondary + ';\n'+
             '}\n'+
+            'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
             'table.md-table td.md-cell.md-placeholder {\n'+
             'color: ' + mdDarkDisabled + ';\n'+
             '}\n'+
@@ -539,13 +536,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
         for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
             var dataKey = vm.alarmSource.dataKeys[d];
 
-            var translationId = types.translate.customTranslationsPrefix + dataKey.label;
-            var translation = $translate.instant(translationId);
-            if (translation != translationId) {
-                dataKey.title = translation + '';
-            } else {
-                dataKey.title = dataKey.label;
-            }
+            dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
 
             var keySettings = dataKey.settings;
 
diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js
index 4b8aa6e..733e7cb 100644
--- a/ui/src/app/widget/lib/entities-table-widget.js
+++ b/ui/src/app/widget/lib/entities-table-widget.js
@@ -65,8 +65,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
     vm.currentEntity = null;
 
     vm.displayEntityName = true;
+    vm.entityNameColumnTitle = '';
     vm.displayEntityType = true;
-    vm.displayActions = false; //TODO: Widget actions
+    vm.actionCellDescriptors = [];
     vm.displayPagination = true;
     vm.defaultPageSize = 10;
     vm.defaultSortOrder = 'entityName';
@@ -92,6 +93,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
     vm.onReorder = onReorder;
     vm.onPaginate = onPaginate;
     vm.onRowClick = onRowClick;
+    vm.onActionButtonClick = onActionButtonClick;
     vm.isCurrent = isCurrent;
 
     vm.cellStyle = cellStyle;
@@ -141,14 +143,10 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
 
         vm.ctx.widgetActions = [ vm.searchAction ];
 
+        vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton');
+
         if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) {
-            var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle;
-            var translation = $translate.instant(translationId);
-            if (translation != translationId) {
-                vm.entitiesTitle = translation + '';
-            } else {
-                vm.entitiesTitle = vm.settings.entitiesTitle;
-            }
+            vm.entitiesTitle = utils.customTranslation(vm.settings.entitiesTitle, vm.settings.entitiesTitle);
         } else {
             vm.entitiesTitle = $translate.instant('entity.entities');
         }
@@ -157,6 +155,13 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
 
         vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
         vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true;
+
+        if (vm.settings.entityNameColumnTitle && vm.settings.entityNameColumnTitle.length) {
+            vm.entityNameColumnTitle = utils.customTranslation(vm.settings.entityNameColumnTitle, vm.settings.entityNameColumnTitle);
+        } else {
+            vm.entityNameColumnTitle = $translate.instant('entity.entity-name');
+        }
+
         vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true;
         vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
 
@@ -185,6 +190,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
         //var mdDarkIcon = mdDarkSecondary;
         var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
 
+        //md-icon.md-default-theme, md-icon {
+
         var cssString = 'table.md-table th.md-column {\n'+
             'color: ' + mdDarkSecondary + ';\n'+
             '}\n'+
@@ -204,6 +211,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
             'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
             'border-color: ' + mdDarkSecondary + ';\n'+
             '}\n'+
+            'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
             'table.md-table td.md-cell.md-placeholder {\n'+
             'color: ' + mdDarkDisabled + ';\n'+
             '}\n'+
@@ -261,11 +271,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
     }
 
     function onRowClick($event, entity) {
+        if ($event) {
+            $event.stopPropagation();
+        }
         if (vm.currentEntity != entity) {
             vm.currentEntity = entity;
+            var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick');
+            if (descriptors.length) {
+                var entityId;
+                var entityName;
+                if (vm.currentEntity) {
+                    entityId = vm.currentEntity.id;
+                    entityName = vm.currentEntity.entityName;
+                }
+                vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
+            }
         }
     }
 
+    function onActionButtonClick($event, entity, actionDescriptor) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var entityId;
+        var entityName;
+        if (entity) {
+            entityId = entity.id;
+            entityName = entity.entityName;
+        }
+        vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName);
+    }
+
     function isCurrent(entity) {
         return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) &&
             (vm.currentEntity.id.id === entity.id.id);
@@ -393,13 +429,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
             }
             vm.dataKeys.push(dataKey);
 
-            var translationId = types.translate.customTranslationsPrefix + dataKey.label;
-            var translation = $translate.instant(translationId);
-            if (translation != translationId) {
-                dataKey.title = translation + '';
-            } else {
-                dataKey.title = dataKey.label;
-            }
+            dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
 
             var keySettings = dataKey.settings;
 
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 247d479..162e5f7 100644
--- a/ui/src/app/widget/lib/entities-table-widget.tpl.html
+++ b/ui/src/app/widget/lib/entities-table-widget.tpl.html
@@ -41,10 +41,10 @@
             <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 translate>entity.entity-name</span></th>
+                    <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.displayActions"><span>&nbsp</span></th>
+                    <th md-column ng-if="vm.actionCellDescriptors.length"><span>&nbsp</span></th>
                 </tr>
                 </thead>
                 <tbody md-body>
@@ -57,14 +57,18 @@
                         ng-style="vm.cellStyle(entity, key)"
                         ng-bind-html="vm.cellContent(entity, key)">
                     </td>
-                    <td md-cell ng-if="vm.displayActions" class="tb-action-cell">
-                        <!--md-button class="md-icon-button" aria-label="{{ 'entity.details' | translate }}"
-                                   ng-click="vm.openEntityDetails($event, entity)">
-                            <md-icon aria-label="{{ 'entity.details' | translate }}" class="material-icons">more_horiz</md-icon>
+                    <td md-cell ng-if="vm.actionCellDescriptors.length" 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"
+                                   aria-label="{{ actionDescriptor.displayName }}"
+                                   ng-click="vm.onActionButtonClick($event, entity, actionDescriptor)">
+                            <md-icon aria-label="{{ actionDescriptor.displayName }}" class="material-icons">{{actionDescriptor.icon}}</md-icon>
                             <md-tooltip md-direction="top">
-                                {{ 'entity.details' | translate }}
+                                {{ actionDescriptor.displayName }}
                             </md-tooltip>
-                        </md-button-->
+                        </md-button>
                     </td>
                 </tr>
                 </tbody>