thingsboard-memoizeit

Changes

ui/package.json 1(+1 -0)

Details

ui/package.json 1(+1 -0)

diff --git a/ui/package.json b/ui/package.json
index 73d7bcd..6884543 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -111,6 +111,7 @@
     "ngtemplate-loader": "^1.3.1",
     "node-sass": "^3.9.3",
     "postcss-loader": "^0.13.0",
+    "raw-loader": "^0.5.1",
     "react-hot-loader": "^3.0.0-beta.6",
     "sass-loader": "^4.0.2",
     "style-loader": "^0.13.1",
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 f8f27ed..a58c307 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -40,7 +40,7 @@ export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsbo
     .name;
 
 /*@ngInject*/
-function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, types, utils) {
+function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $translate, types, utils) {
 
     $window.$ = $;
     $window.jQuery = $;
@@ -548,13 +548,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
          '    }\n\n' +
 
          '    self.typeParameters = function() {\n\n' +
-                    {
-                        useCustomDatasources: false,
-                        maxDatasources: -1 //unlimited
-                        maxDataKeys: -1 //unlimited
-                    }
+                    return {
+                                useCustomDatasources: false,
+                                maxDatasources: -1 //unlimited
+                                maxDataKeys: -1 //unlimited
+                           };
          '    }\n\n' +
 
+         '    self.actionSources = function() {\n\n' +
+                    return {
+                                'headerButton': {
+                                   name: 'Header button',
+                                   multiple: true
+                                }
+                            };
+              }\n\n' +
          '    self.onResize = function() {\n\n' +
 
          '    }\n\n' +
@@ -611,6 +619,16 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
             if (angular.isUndefined(result.typeParameters.maxDataKeys)) {
                 result.typeParameters.maxDataKeys = -1;
             }
+            if (angular.isFunction(widgetTypeInstance.actionSources)) {
+                result.actionSources = widgetTypeInstance.actionSources();
+            } else {
+                result.actionSources = {};
+            }
+            for (var actionSourceId in types.widgetActionSources) {
+                result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]);
+                result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name) + '';
+            }
+
             return result;
         } catch (e) {
             utils.processWidgetException(e);
@@ -650,6 +668,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
                         widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
                     }
                     widgetInfo.typeParameters = widgetType.typeParameters;
+                    widgetInfo.actionSources = widgetType.actionSources;
                     putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
                     putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
                     deferred.resolve(widgetInfo);
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index ad36e84..61fc120 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -399,6 +399,31 @@ export default angular.module('thingsboard.types', [])
                     }
                 }
             },
+            widgetActionSources: {
+                headerButton: {
+                    name: 'widget-action.header-button',
+                    value: 'headerButton',
+                    multiple: true
+                }
+            },
+            widgetActionTypes: {
+                openDashboardState: {
+                    name: 'widget-action.open-dashboard-state',
+                    value: 'openDashboardState'
+                },
+                updateDashboardState: {
+                    name: 'widget-action.update-dashboard-state',
+                    value: 'updateDashboardState'
+                },
+                openDashboard: {
+                    name: 'widget-action.open-dashboard',
+                    value: 'openDashboard'
+                },
+                custom: {
+                    name: 'widget-action.custom',
+                    value: 'custom'
+                }
+            },
             systemBundleAlias: {
                 charts: "charts",
                 cards: "cards"
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index 0939971..7c9f97d 100644
--- a/ui/src/app/common/utils.service.js
+++ b/ui/src/app/common/utils.service.js
@@ -13,6 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import materialIconsCodepoints from 'raw-loader!material-design-icons/iconfont/codepoints';
+
+/* eslint-enable import/no-unresolved, import/default */
+
 import tinycolor from "tinycolor2";
 import jsonSchemaDefaults from "json-schema-defaults";
 import thingsboardTypes from "./types.constant";
@@ -24,11 +31,18 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
 const varsRegex = /\$\{([^\}]*)\}/g;
 
 /*@ngInject*/
-function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
+function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, types) {
 
     var predefinedFunctions = {},
         predefinedFunctionsList = [],
-        materialColors = [];
+        materialColors = [],
+        materialIcons = [];
+
+    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));";
@@ -122,6 +136,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
         getDefaultDatasourceJson: getDefaultDatasourceJson,
         getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
         getMaterialColor: getMaterialColor,
+        getMaterialIcons: getMaterialIcons,
+        getCommonMaterialIcons: getCommonMaterialIcons,
         getPredefinedFunctionBody: getPredefinedFunctionBody,
         getPredefinedFunctionsList: getPredefinedFunctionsList,
         genMaterialColor: genMaterialColor,
@@ -136,7 +152,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
         validateDatasources: validateDatasources,
         createKey: createKey,
         createLabelFromDatasource: createLabelFromDatasource,
-        insertVariable: insertVariable
+        insertVariable: insertVariable,
+        customTranslation: customTranslation
     }
 
     return service;
@@ -154,6 +171,31 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
         return materialColors[colorIndex].value;
     }
 
+    function getMaterialIcons() {
+        var deferred = $q.defer();
+        if (materialIcons.length) {
+            deferred.resolve(materialIcons);
+        } else {
+            $timeout(function() {
+                var codepointsArray = materialIconsCodepoints.split("\n");
+                codepointsArray.forEach(function (codepoint) {
+                    if (codepoint && codepoint.length) {
+                        var values = codepoint.split(' ');
+                        if (values && values.length == 2) {
+                            materialIcons.push(values[0]);
+                        }
+                    }
+                });
+                deferred.resolve(materialIcons);
+            });
+        }
+        return deferred.promise;
+    }
+
+    function getCommonMaterialIcons() {
+        return commonMaterialIcons;
+    }
+
     function genMaterialColor(str) {
         var hash = Math.abs(hashCode(str));
         return getMaterialColor(hash);
@@ -432,4 +474,16 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
         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 d1427b6..1a4cdba 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
 import angularGridster from 'angular-gridster';
 import thingsboardTypes from '../common/types.constant';
 import thingsboardApiWidget from '../api/widget.service';
-import thingsboardWidget from './widget.directive';
+import thingsboardWidget from './widget/widget.directive';
 import thingsboardToast from '../services/toast';
 import thingsboardTimewindow from './timewindow.directive';
 import thingsboardEvents from './tb-event-directives';
@@ -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/finish-render.directive.js b/ui/src/app/components/finish-render.directive.js
new file mode 100644
index 0000000..9092e2d
--- /dev/null
+++ b/ui/src/app/components/finish-render.directive.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+export default angular.module('thingsboard.directives.finishRender', [])
+    .directive('tbOnFinishRender', OnFinishRender)
+    .name;
+
+/*@ngInject*/
+function OnFinishRender($timeout) {
+    return {
+        restrict: 'A',
+        link: function (scope, element, attr) {
+            if (scope.$last === true) {
+                $timeout(function () {
+                    scope.$emit(attr.tbOnFinishRender);
+                });
+            }
+        }
+    };
+}
diff --git a/ui/src/app/components/material-icons-dialog.controller.js b/ui/src/app/components/material-icons-dialog.controller.js
new file mode 100644
index 0000000..94b18dc
--- /dev/null
+++ b/ui/src/app/components/material-icons-dialog.controller.js
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+import './material-icons-dialog.scss';
+
+/*@ngInject*/
+export default function MaterialIconsDialogController($scope, $mdDialog, $timeout, utils, icon) {
+
+    var vm = this;
+
+    vm.selectedIcon = icon;
+
+    vm.showAll = false;
+    vm.loadingIcons = false;
+
+    $scope.$watch('vm.showAll', function(showAll) {
+        if (showAll) {
+            vm.loadingIcons = true;
+            $timeout(function() {
+                utils.getMaterialIcons().then(
+                    function success(icons) {
+                        vm.icons = icons;
+                    }
+                );
+            });
+        } else {
+            vm.icons = utils.getCommonMaterialIcons();
+        }
+    });
+
+    $scope.$on('iconsLoadFinished', function() {
+        vm.loadingIcons = false;
+    });
+
+    vm.cancel = cancel;
+    vm.selectIcon = selectIcon;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function selectIcon($event, icon) {
+        vm.selectedIcon = icon;
+        $mdDialog.hide(vm.selectedIcon);
+    }
+}
diff --git a/ui/src/app/components/material-icons-dialog.scss b/ui/src/app/components/material-icons-dialog.scss
new file mode 100644
index 0000000..dbd8395
--- /dev/null
+++ b/ui/src/app/components/material-icons-dialog.scss
@@ -0,0 +1,31 @@
+/**
+ * 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-icons-dialog {
+  button.md-icon-button.tb-select-icon-button {
+    border: solid 1px orange;
+    border-radius: 0%;
+    padding: 16px;
+    height: 56px;
+    width: 56px;
+    margin: 10px;
+  }
+  .tb-icons-load {
+    top: 64px;
+    background: rgba(255,255,255,0.75);
+    z-index: 3;
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/material-icons-dialog.tpl.html b/ui/src/app/components/material-icons-dialog.tpl.html
new file mode 100644
index 0000000..8a2eb65
--- /dev/null
+++ b/ui/src/app/components/material-icons-dialog.tpl.html
@@ -0,0 +1,62 @@
+<!--
+
+    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.
+
+-->
+<md-dialog class="tb-material-icons-dialog" aria-label="{{'icon.material-icons' | translate }}" style="min-width: 600px;">
+    <form>
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2>{{ 'icon.select-icon' | translate }}</h2>
+                <span flex></span>
+                <section layout="row" layout-align="start center">
+                    <md-switch ng-model="vm.showAll"
+                               aria-label="{{ 'icon.show-all' | translate }}">
+                    </md-switch>
+                    <label translate>icon.show-all</label>
+                </section>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <div class="tb-absolute-fill tb-icons-load" ng-show="vm.loadingIcons" layout="column" layout-align="center center">
+            <md-progress-circular md-mode="indeterminate" ng-disabled="!vm.loadingIcons" class="md-accent" md-diameter="40"></md-progress-circular>
+        </div>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <md-button ng-class="{'md-primary md-raised': icon == vm.selectedIcon}" class="tb-select-icon-button md-icon-button"
+                                   ng-repeat="icon in vm.icons" ng-click="vm.selectIcon($event, icon)" tb-on-finish-render="iconsLoadFinished">
+                            <md-icon class="material-icons">{{icon}}</md-icon>
+                            <md-tooltip md-direction="bottom">
+                                {{ icon }}
+                            </md-tooltip>
+                        </md-button>
+                    </fieldset>
+                </md-content>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()">
+                {{ 'action.cancel' | translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/components/material-icon-select.directive.js b/ui/src/app/components/material-icon-select.directive.js
new file mode 100644
index 0000000..f40d1c8
--- /dev/null
+++ b/ui/src/app/components/material-icon-select.directive.js
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+import './material-icon-select.scss';
+
+import MaterialIconsDialogController from './material-icons-dialog.controller';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import materialIconSelectTemplate from './material-icon-select.tpl.html';
+import materialIconsDialogTemplate from './material-icons-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.materialIconSelect', [])
+    .controller('MaterialIconsDialogController', MaterialIconsDialogController)
+    .directive('tbMaterialIconSelect', MaterialIconSelect)
+    .name;
+
+/*@ngInject*/
+function MaterialIconSelect($compile, $templateCache, $document, $mdDialog) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(materialIconSelectTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.icon = null;
+
+        scope.updateView = function () {
+            ngModelCtrl.$setViewValue(scope.icon);
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                scope.icon = ngModelCtrl.$viewValue;
+            }
+            if (!scope.icon || !scope.icon.length) {
+                scope.icon = 'more_horiz';
+            }
+        }
+
+        scope.$watch('icon', function () {
+            scope.updateView();
+        });
+
+        scope.openIconDialog = function($event) {
+            if ($event) {
+                $event.stopPropagation();
+            }
+            $mdDialog.show({
+                controller: 'MaterialIconsDialogController',
+                controllerAs: 'vm',
+                templateUrl: materialIconsDialogTemplate,
+                parent: angular.element($document[0].body),
+                locals: {icon: scope.icon},
+                skipHide: true,
+                fullscreen: true,
+                targetEvent: $event
+            }).then(function (icon) {
+                scope.icon = icon;
+            });
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            tbRequired: '=?',
+        }
+    };
+}
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
new file mode 100644
index 0000000..c2bee3f
--- /dev/null
+++ b/ui/src/app/components/material-icon-select.tpl.html
@@ -0,0 +1,24 @@
+<!--
+
+    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.
+
+-->
+<div class="tb-material-icon-select" layout="row">
+    <md-icon class="material-icons" ng-click="openIconDialog($event)">{{icon}}</md-icon>
+    <md-input-container flex>
+        <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
new file mode 100644
index 0000000..93ca06f
--- /dev/null
+++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
@@ -0,0 +1,270 @@
+/**
+ * Created by igor on 6/20/17.
+ */
+/*
+ * 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.
+ */
+
+import './manage-widget-actions.scss';
+
+import thingsboardMaterialIconSelect from '../../material-icon-select.directive';
+
+import WidgetActionDialogController from './widget-action-dialog.controller';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import manageWidgetActionsTemplate from './manage-widget-actions.tpl.html';
+import widgetActionDialogTemplate from './widget-action-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.widgetActions', [thingsboardMaterialIconSelect])
+    .controller('WidgetActionDialogController', WidgetActionDialogController)
+    .directive('tbManageWidgetActions', ManageWidgetActions)
+    .name;
+
+/*@ngInject*/
+function ManageWidgetActions() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            actionSources: '=',
+            widgetActions: '=',
+            fetchDashboardStates: '&',
+        },
+        controller: ManageWidgetActionsController,
+        controllerAs: 'vm',
+        templateUrl: manageWidgetActionsTemplate
+    };
+}
+
+/* eslint-disable angular/angularelement */
+
+
+/*@ngInject*/
+function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter,
+                              $translate, $timeout, utils, types) {
+
+    let vm = this;
+
+    vm.allActions = [];
+
+    vm.actions = [];
+    vm.actionsCount = 0;
+
+    vm.query = {
+        order: 'actionSourceName',
+        limit: 10,
+        page: 1,
+        search: null
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.addAction = addAction;
+    vm.editAction = editAction;
+    vm.deleteAction = deleteAction;
+
+    $timeout(function(){
+        $scope.manageWidgetActionsForm.querySearchInput.$pristine = false;
+    });
+
+    $scope.$watch('vm.widgetActions', function() {
+        if (vm.widgetActions) {
+            reloadActions();
+        }
+    });
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateActions();
+        }
+    });
+
+    function enterFilterMode () {
+        vm.query.search = '';
+    }
+
+    function exitFilterMode () {
+        vm.query.search = null;
+        updateActions();
+    }
+
+    function onReorder () {
+        updateActions();
+    }
+
+    function onPaginate () {
+        updateActions();
+    }
+
+    function addAction($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openWidgetActionDialog($event, null, true);
+    }
+
+    function editAction ($event, action) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openWidgetActionDialog($event, action, false);
+    }
+
+    function deleteAction($event, action) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (action) {
+            var title = $translate.instant('widget-config.delete-action-title');
+            var content = $translate.instant('widget-config.delete-action-text', {actionName: action.name});
+            var confirm = $mdDialog.confirm()
+                .targetEvent($event)
+                .title(title)
+                .htmlContent(content)
+                .ariaLabel(title)
+                .cancel($translate.instant('action.no'))
+                .ok($translate.instant('action.yes'));
+
+            confirm._options.skipHide = true;
+            confirm._options.fullscreen = true;
+
+            $mdDialog.show(confirm).then(function () {
+                var index = getActionIndex(action.id, vm.allActions);
+                if (index > -1) {
+                    vm.allActions.splice(index, 1);
+                }
+                var targetActions = vm.widgetActions[action.actionSourceId];
+                index = getActionIndex(action.id, targetActions);
+                if (index > -1) {
+                    targetActions.splice(index, 1);
+                }
+                $scope.manageWidgetActionsForm.$setDirty();
+                updateActions();
+            });
+        }
+    }
+
+    function openWidgetActionDialog($event, action, isAdd) {
+        var prevActionId = null;
+        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, fetchDashboardStates: vm.fetchDashboardStates,
+                actionSources: availableActionSources, widgetActions: vm.widgetActions,
+                action: angular.copy(action)},
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (action) {
+            saveAction(action, prevActionId);
+            updateActions();
+        });
+    }
+
+    function getActionIndex(id, actions) {
+        var result = $filter('filter')(actions, {id: id}, true);
+        if (result && result.length) {
+            return actions.indexOf(result[0]);
+        }
+        return -1;
+    }
+
+    function saveAction(action, prevActionId) {
+        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);
+        delete widgetAction.actionSourceId;
+        delete widgetAction.actionSourceName;
+        delete widgetAction.typeName;
+        var targetActions = vm.widgetActions[actionSourceId];
+        if (!targetActions) {
+            targetActions = [];
+            vm.widgetActions[actionSourceId] = targetActions;
+        }
+        if (prevActionId) {
+            var index = getActionIndex(prevActionId, vm.allActions);
+            if (index > -1) {
+                vm.allActions[index] = action;
+            }
+            index = getActionIndex(prevActionId, targetActions);
+            if (index > -1) {
+                targetActions[index] = widgetAction;
+            }
+        } else {
+            vm.allActions.push(action);
+            targetActions.push(widgetAction);
+        }
+        $scope.manageWidgetActionsForm.$setDirty();
+    }
+
+    function reloadActions() {
+        vm.allActions = [];
+        vm.actions = [];
+        vm.actionsCount = 0;
+
+        for (var actionSourceId in vm.widgetActions) {
+            var actionSource = vm.actionSources[actionSourceId];
+            var actionSourceActions = vm.widgetActions[actionSourceId];
+            for (var i=0;i<actionSourceActions.length;i++) {
+                var actionSourceAction = actionSourceActions[i];
+                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);
+            }
+        }
+
+        updateActions ();
+    }
+
+    function updateActions () {
+        var result = $filter('orderBy')(vm.allActions, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, {$: vm.query.search});
+        }
+        vm.actionsCount = result.length;
+        var startIndex = vm.query.limit * (vm.query.page - 1);
+        vm.actions = result.slice(startIndex, startIndex + vm.query.limit);
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.scss b/ui/src/app/components/widget/action/manage-widget-actions.scss
new file mode 100644
index 0000000..aee98ad
--- /dev/null
+++ b/ui/src/app/components/widget/action/manage-widget-actions.scss
@@ -0,0 +1,33 @@
+/**
+ * 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-manage-widget-actions {
+  table.md-table {
+    tbody {
+      tr {
+        td {
+          &.tb-action-cell {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            min-width: 100px;
+            max-width: 100px;
+            width: 100px;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.tpl.html b/ui/src/app/components/widget/action/manage-widget-actions.tpl.html
new file mode 100644
index 0000000..b06959b
--- /dev/null
+++ b/ui/src/app/components/widget/action/manage-widget-actions.tpl.html
@@ -0,0 +1,99 @@
+<!--
+
+    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.
+
+-->
+<div ng-form="manageWidgetActionsForm" class="tb-manage-widget-actions md-whiteframe-z1" layout="column">
+    <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search === null">
+        <div class="md-toolbar-tools">
+            <span translate>widget-config.actions</span>
+            <span flex></span>
+            <md-button class="md-icon-button" ng-click="vm.addAction($event)">
+                <md-icon>add</md-icon>
+                <md-tooltip md-direction="top">
+                    {{ 'widget-config.add-action' | translate }}
+                </md-tooltip>
+            </md-button>
+            <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+                <md-icon>search</md-icon>
+                <md-tooltip md-direction="top">
+                    {{ 'action.search' | translate }}
+                </md-tooltip>
+            </md-button>
+        </div>
+    </md-toolbar>
+    <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
+        <div class="md-toolbar-tools">
+            <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+                <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                <md-tooltip md-direction="top">
+                    {{ 'widget-config.search-actions' | translate }}
+                </md-tooltip>
+            </md-button>
+            <md-input-container flex>
+                <label>&nbsp;</label>
+                <input ng-model="vm.query.search" name="querySearchInput" placeholder="{{ 'widget-config.search-actions' | translate }}"/>
+            </md-input-container>
+            <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+                <md-icon aria-label="Close" class="material-icons">close</md-icon>
+                <md-tooltip md-direction="top">
+                    {{ 'action.close' | translate }}
+                </md-tooltip>
+            </md-button>
+        </div>
+    </md-toolbar>
+    <md-table-container>
+        <table md-table>
+            <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+            <tr md-row>
+                <th md-column md-order-by="actionSourceName"><span translate>widget-config.action-source</span></th>
+                <th md-column md-order-by="name"><span translate>widget-config.action-name</span></th>
+                <th md-column md-order-by="icon"><span translate>widget-config.action-icon</span></th>
+                <th md-column md-order-by="typeName"><span translate>widget-config.action-type</span></th>
+                <th md-column><span>&nbsp</span></th>
+            </tr>
+            </thead>
+            <tbody md-body>
+                <tr md-row ng-repeat="action in vm.actions">
+                    <td md-cell>{{action.actionSourceName}}</td>
+                    <td md-cell>{{action.name}}</td>
+                    <td md-cell>
+                        <md-icon aria-label="{{ 'widget-config.action-icon' | translate }}" class="material-icons">{{action.icon}}</md-icon>
+                    </td>
+                    <td md-cell>{{action.typeName}}</td>
+                    <td md-cell class="tb-action-cell">
+                        <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
+                                   ng-click="vm.editAction($event, action)">
+                            <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'widget-config.edit-action' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                        <md-button class="md-icon-button" aria-label="{{'action.delete' | translate}}" ng-click="vm.deleteAction($event, action)">
+                            <md-icon aria-label="Delete" class="material-icons">delete</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'widget-config.delete-action' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </md-table-container>
+    <md-table-pagination md-limit="vm.query.limit" md-limit-options="[10, 15, 20]"
+                         md-page="vm.query.page" md-total="{{vm.actionsCount}}"
+                         md-on-paginate="vm.onPaginate" md-page-select>
+    </md-table-pagination>
+</div>
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
new file mode 100644
index 0000000..99a2f0b
--- /dev/null
+++ b/ui/src/app/components/widget/action/widget-action-dialog.controller.js
@@ -0,0 +1,169 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+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 = {
+            id: utils.guid()
+        };
+    } else {
+        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(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
new file mode 100644
index 0000000..7a20e74
--- /dev/null
+++ b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html
@@ -0,0 +1,133 @@
+<!--
+
+    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.
+
+-->
+<md-dialog class="tb-widget-action-dialog" aria-label="{{'widget-config.action' | translate }}" style="min-width: 600px;">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2>{{ (vm.isAdd ? 'widget-config.add-action' : 'widget-config.edit-action') | translate }}</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <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">
+                                    {{vm.actionSourceName(actionSource)}}
+                                </md-option>
+                            </md-select>
+                            <div ng-messages="theForm.actionSource.$error">
+                                <div ng-message="required" translate>widget-config.action-source-required</div>
+                            </div>
+                        </md-input-container>
+                        <md-input-container class="md-block">
+                            <label translate>widget-config.action-name</label>
+                            <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">
+                        </tb-material-icon-select>
+                        <md-input-container class="md-block">
+                            <label translate>widget-config.action-type</label>
+                            <md-select name="actionType" required aria-label="{{ 'widget-config.action-type' | translate }}" ng-model="vm.action.type">
+                                <md-option ng-repeat="actionType in vm.types.widgetActionTypes" ng-value="actionType.value">
+                                    {{ actionType.name | translate }}
+                                </md-option>
+                            </md-select>
+                            <div ng-messages="theForm.actionType.$error">
+                                <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>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
+                {{ 'action.cancel' | translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
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 894c23d..4ca29f0 100644
--- a/ui/src/app/dashboard/add-widget.tpl.html
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -34,6 +34,7 @@
                 <fieldset ng-disabled="loading" style="position: relative; height: 600px;">
                     <tb-widget-config widget-type="vm.widget.type"
                                       type-parameters="vm.widgetInfo.typeParameters"
+                                      action-sources="vm.widgetInfo.actionSources"
                                       force-expand-datasources="true"
                                       ng-model="vm.widgetConfig"
                                       widget-settings-schema="vm.settingsSchema"
@@ -41,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 dcf05e2..9bf1548 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -41,6 +41,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
                             var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
                             var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
                             scope.typeParameters = widgetInfo.typeParameters;
+                            scope.actionSources = widgetInfo.actionSources;
                             scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources;
                             if (!settingsSchema || settingsSchema === '') {
                                 scope.settingsSchema = {};
@@ -93,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 74f5755..1f724a8 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -18,6 +18,7 @@
 <fieldset ng-disabled="loading">
 	<tb-widget-config widget-type="widget.type"
 					  type-parameters="typeParameters"
+					  action-sources="actionSources"
 					  ng-model="widgetConfig"
 					  is-data-enabled="isDataEnabled"
 					  widget-settings-schema="settingsSchema"
@@ -25,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/index.js b/ui/src/app/dashboard/index.js
index d940f09..4089716 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -23,7 +23,7 @@ import thingsboardApiUser from '../api/user.service';
 import thingsboardApiDashboard from '../api/dashboard.service';
 import thingsboardApiCustomer from '../api/customer.service';
 import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
-import thingsboardWidgetConfig from '../components/widget-config.directive';
+import thingsboardWidgetConfig from '../components/widget/widget-config.directive';
 import thingsboardDashboardSelect from '../components/dashboard-select.directive';
 import thingsboardRelatedEntityAutocomplete from '../components/related-entity-autocomplete.directive';
 import thingsboardDashboard from '../components/dashboard.directive';
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
index 385de94..4cf0a3d 100644
--- a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
@@ -59,10 +59,10 @@
             <span flex></span>
             <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
                        class="md-raised md-primary">
-                {{ vm.isAdd ? 'Add' : 'Save' }}
+                {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
             </md-button>
             <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
-                Cancel
+                {{ 'action.cancel' | translate }}
             </md-button>
         </md-dialog-actions>
     </form>
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/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
index 3595f65..1aa95b7 100644
--- a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
@@ -175,8 +175,6 @@ export default function ManageDashboardStatesController($scope, $mdDialog, $filt
                 $scope.theForm.$setDirty();
                 updateStates();
             });
-
-
         }
     }
 
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/layout/index.js b/ui/src/app/layout/index.js
index 2a27c93..9d3e1d1 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -26,6 +26,7 @@ import thingsboardApiLogin from '../api/login.service';
 import thingsboardApiUser from '../api/user.service';
 
 import thingsboardNoAnimate from '../components/no-animate.directive';
+import thingsboardOnFinishRender from '../components/finish-render.directive';
 import thingsboardSideMenu from '../components/side-menu.directive';
 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
 
@@ -81,6 +82,7 @@ export default angular.module('thingsboard.home', [
     thingsboardApiLogin,
     thingsboardApiUser,
     thingsboardNoAnimate,
+    thingsboardOnFinishRender,
     thingsboardSideMenu,
     thingsboardDashboardAutocomplete
 ])
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 4710b92..4d4ebe2 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1103,6 +1103,18 @@ export default angular.module('thingsboard.locale', [])
                     "undo": "Undo widget changes",
                     "export": "Export widget"
                 },
+                "widget-action": {
+                    "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",
+                    "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",
                     "widgets-bundles": "Widgets Bundles",
@@ -1158,7 +1170,23 @@ export default angular.module('thingsboard.locale', [])
                     "remove-datasource": "Remove datasource",
                     "add-datasource": "Add datasource",
                     "target-device": "Target device",
-                    "alarm-source": "Alarm source"
+                    "alarm-source": "Alarm source",
+                    "actions": "Actions",
+                    "action": "Action",
+                    "add-action": "Add action",
+                    "search-actions": "Search actions",
+                    "action-source": "Action source",
+                    "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.",
+                    "edit-action": "Edit action",
+                    "delete-action": "Delete action",
+                    "delete-action-title": "Delete widget action",
+                    "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?"
                 },
                 "widget-type": {
                     "import": "Import widget type",
@@ -1168,6 +1196,12 @@ export default angular.module('thingsboard.locale', [])
                     "widget-type-file": "Widget type file",
                     "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure."
                 },
+                "icon": {
+                    "icon": "Icon",
+                    "select-icon": "Select icon",
+                    "material-icons": "Material icons",
+                    "show-all": "Show all icons"
+                },
                 "language": {
                     "language": "Language",
                     "en_US": "English",
@@ -1177,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>