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/widget.service.js b/ui/src/app/api/widget.service.js
index f8f27ed..e23e916 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 81ce0fa..1a51a1a 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -389,6 +389,30 @@ export default angular.module('thingsboard.types', [])
                     }
                 }
             },
+            widgetActionSources: {
+                'headerButton': {
+                    name: 'widget-action.header-button',
+                    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 a3433c2..e6d48ae 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,14 @@ 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 commonUsedMaterialIcons = [ 'more_horiz', 'close', 'play_arrow' ];
 
     predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));";
     predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));";
@@ -122,6 +132,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
         getDefaultDatasourceJson: getDefaultDatasourceJson,
         getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
         getMaterialColor: getMaterialColor,
+        getMaterialIcons: getMaterialIcons,
+        getCommonMaterialIcons: getCommonMaterialIcons,
         getPredefinedFunctionBody: getPredefinedFunctionBody,
         getPredefinedFunctionsList: getPredefinedFunctionsList,
         genMaterialColor: genMaterialColor,
@@ -154,6 +166,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 commonUsedMaterialIcons;
+    }
+
     function genMaterialColor(str) {
         var hash = Math.abs(hashCode(str));
         return getMaterialColor(hash);
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index d1427b6..e61abcc 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';
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..45ab2cd
--- /dev/null
+++ b/ui/src/app/components/material-icon-select.directive.js
@@ -0,0 +1,88 @@
+/*
+ * 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 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.tpl.html b/ui/src/app/components/material-icon-select.tpl.html
new file mode 100644
index 0000000..77ef87f
--- /dev/null
+++ b/ui/src/app/components/material-icon-select.tpl.html
@@ -0,0 +1,26 @@
+<!--
+
+    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 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>
+    </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..b273729
--- /dev/null
+++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
@@ -0,0 +1,255 @@
+/**
+ * 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: '='
+        },
+        controller: ManageWidgetActionsController,
+        controllerAs: 'vm',
+        templateUrl: manageWidgetActionsTemplate
+    };
+}
+
+/* eslint-disable angular/angularelement */
+
+
+/*@ngInject*/
+function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter,
+                              $translate, $timeout, 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;
+        }
+        $mdDialog.show({
+            controller: 'WidgetActionDialogController',
+            controllerAs: 'vm',
+            templateUrl: widgetActionDialogTemplate,
+            parent: angular.element($document[0].body),
+            locals: {isAdd: isAdd, actionSources: vm.actionSources, 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) {
+        action.actionSourceName = vm.actionSources[action.actionSourceId].name;
+        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 = {
+                    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)
+                };
+                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..948308b
--- /dev/null
+++ b/ui/src/app/components/widget/action/widget-action-dialog.controller.js
@@ -0,0 +1,46 @@
+/*
+ * 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, types, utils, isAdd, actionSources, action) {
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.isAdd = isAdd;
+    vm.actionSources = actionSources;
+
+    if (vm.isAdd) {
+        vm.action = {
+            id: utils.guid()
+        };
+    } else {
+        vm.action = action;
+    }
+
+    vm.cancel = cancel;
+    vm.save = save;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(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..ccb9d41
--- /dev/null
+++ b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html
@@ -0,0 +1,81 @@
+<!--
+
+    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">
+                        <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}}
+                                </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>
+                        </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>
+                    </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.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index 894c23d..3920f91 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"
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index dcf05e2..5d32706 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 = {};
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index 74f5755..c6bfd5a 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"
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/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/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..63f20e5 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1103,6 +1103,13 @@ export default angular.module('thingsboard.locale', [])
                     "undo": "Undo widget changes",
                     "export": "Export widget"
                 },
+                "widget-action": {
+                    "header-button": "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"
+                },
                 "widgets-bundle": {
                     "current": "Current bundle",
                     "widgets-bundles": "Widgets Bundles",
@@ -1158,7 +1165,22 @@ 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-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 +1190,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",