Commit 13a6c9a14210b5f5b814bea191fae5411befbc7c

Authored by Igor Kulikov
Committed by GitHub
2 parents dff1bfe2 2fdb372c

Merge pull request #177 from thingsboard/feature/TB-64

TB-64: Widget actions
Showing 40 changed files with 1543 additions and 92 deletions
... ... @@ -111,6 +111,7 @@
111 111 "ngtemplate-loader": "^1.3.1",
112 112 "node-sass": "^3.9.3",
113 113 "postcss-loader": "^0.13.0",
  114 + "raw-loader": "^0.5.1",
114 115 "react-hot-loader": "^3.0.0-beta.6",
115 116 "sass-loader": "^4.0.2",
116 117 "style-loader": "^0.13.1",
... ...
... ... @@ -171,6 +171,48 @@ export default class Subscription {
171 171 return deferred.promise;
172 172 }
173 173
  174 + getFirstEntityInfo() {
  175 + var entityId;
  176 + var entityName;
  177 + if (this.type === this.ctx.types.widgetType.rpc.value) {
  178 + if (this.targetDeviceId) {
  179 + entityId = {
  180 + entityType: this.ctx.entityType.device,
  181 + id: this.targetDeviceId
  182 + }
  183 + entityName = this.targetDeviceName;
  184 + }
  185 + } else if (this.type == this.ctx.types.widgetType.alarm.value) {
  186 + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) {
  187 + entityId = {
  188 + entityType: this.alarmSource.entityType,
  189 + id: this.alarmSource.entityId
  190 + }
  191 + entityName = this.alarmSource.entityName;
  192 + }
  193 + } else {
  194 + for (var i=0;i<this.datasources.length;i++) {
  195 + var datasource = this.datasources[i];
  196 + if (datasource && datasource.entityType && datasource.entityId) {
  197 + entityId = {
  198 + entityType: datasource.entityType,
  199 + id: datasource.entityId
  200 + }
  201 + entityName = datasource.entityName;
  202 + break;
  203 + }
  204 + }
  205 + }
  206 + if (entityId) {
  207 + return {
  208 + entityId: entityId,
  209 + entityName: entityName
  210 + };
  211 + } else {
  212 + return null;
  213 + }
  214 + }
  215 +
174 216 initAlarmSubscription() {
175 217 var deferred = this.ctx.$q.defer();
176 218 if (!this.ctx.aliasController) {
... ... @@ -342,6 +384,7 @@ export default class Subscription {
342 384 function success(aliasInfo) {
343 385 if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType == subscription.ctx.types.entityType.device) {
344 386 subscription.targetDeviceId = aliasInfo.currentEntity.id;
  387 + subscription.targetDeviceName = aliasInfo.currentEntity.name;
345 388 if (subscription.targetDeviceId) {
346 389 subscription.rpcEnabled = true;
347 390 } else {
... ...
... ... @@ -40,7 +40,7 @@ export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsbo
40 40 .name;
41 41
42 42 /*@ngInject*/
43   -function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, types, utils) {
  43 +function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $translate, types, utils) {
44 44
45 45 $window.$ = $;
46 46 $window.jQuery = $;
... ... @@ -548,13 +548,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
548 548 ' }\n\n' +
549 549
550 550 ' self.typeParameters = function() {\n\n' +
551   - {
552   - useCustomDatasources: false,
553   - maxDatasources: -1 //unlimited
554   - maxDataKeys: -1 //unlimited
555   - }
  551 + return {
  552 + useCustomDatasources: false,
  553 + maxDatasources: -1 //unlimited
  554 + maxDataKeys: -1 //unlimited
  555 + };
556 556 ' }\n\n' +
557 557
  558 + ' self.actionSources = function() {\n\n' +
  559 + return {
  560 + 'headerButton': {
  561 + name: 'Header button',
  562 + multiple: true
  563 + }
  564 + };
  565 + }\n\n' +
558 566 ' self.onResize = function() {\n\n' +
559 567
560 568 ' }\n\n' +
... ... @@ -611,6 +619,16 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
611 619 if (angular.isUndefined(result.typeParameters.maxDataKeys)) {
612 620 result.typeParameters.maxDataKeys = -1;
613 621 }
  622 + if (angular.isFunction(widgetTypeInstance.actionSources)) {
  623 + result.actionSources = widgetTypeInstance.actionSources();
  624 + } else {
  625 + result.actionSources = {};
  626 + }
  627 + for (var actionSourceId in types.widgetActionSources) {
  628 + result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]);
  629 + result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name) + '';
  630 + }
  631 +
614 632 return result;
615 633 } catch (e) {
616 634 utils.processWidgetException(e);
... ... @@ -650,6 +668,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
650 668 widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
651 669 }
652 670 widgetInfo.typeParameters = widgetType.typeParameters;
  671 + widgetInfo.actionSources = widgetType.actionSources;
653 672 putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
654 673 putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
655 674 deferred.resolve(widgetInfo);
... ...
... ... @@ -399,6 +399,31 @@ export default angular.module('thingsboard.types', [])
399 399 }
400 400 }
401 401 },
  402 + widgetActionSources: {
  403 + headerButton: {
  404 + name: 'widget-action.header-button',
  405 + value: 'headerButton',
  406 + multiple: true
  407 + }
  408 + },
  409 + widgetActionTypes: {
  410 + openDashboardState: {
  411 + name: 'widget-action.open-dashboard-state',
  412 + value: 'openDashboardState'
  413 + },
  414 + updateDashboardState: {
  415 + name: 'widget-action.update-dashboard-state',
  416 + value: 'updateDashboardState'
  417 + },
  418 + openDashboard: {
  419 + name: 'widget-action.open-dashboard',
  420 + value: 'openDashboard'
  421 + },
  422 + custom: {
  423 + name: 'widget-action.custom',
  424 + value: 'custom'
  425 + }
  426 + },
402 427 systemBundleAlias: {
403 428 charts: "charts",
404 429 cards: "cards"
... ...
... ... @@ -13,6 +13,13 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import materialIconsCodepoints from 'raw-loader!material-design-icons/iconfont/codepoints';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
16 23 import tinycolor from "tinycolor2";
17 24 import jsonSchemaDefaults from "json-schema-defaults";
18 25 import thingsboardTypes from "./types.constant";
... ... @@ -24,11 +31,18 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
24 31 const varsRegex = /\$\{([^\}]*)\}/g;
25 32
26 33 /*@ngInject*/
27   -function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
  34 +function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, types) {
28 35
29 36 var predefinedFunctions = {},
30 37 predefinedFunctionsList = [],
31   - materialColors = [];
  38 + materialColors = [],
  39 + materialIcons = [];
  40 +
  41 + var commonMaterialIcons = [ 'more_horiz', 'more_vert', 'open_in_new', 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
  42 + 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
  43 + 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
  44 + 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
  45 + 'share', 'add', 'edit', 'done' ];
32 46
33 47 predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));";
34 48 predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));";
... ... @@ -122,6 +136,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
122 136 getDefaultDatasourceJson: getDefaultDatasourceJson,
123 137 getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
124 138 getMaterialColor: getMaterialColor,
  139 + getMaterialIcons: getMaterialIcons,
  140 + getCommonMaterialIcons: getCommonMaterialIcons,
125 141 getPredefinedFunctionBody: getPredefinedFunctionBody,
126 142 getPredefinedFunctionsList: getPredefinedFunctionsList,
127 143 genMaterialColor: genMaterialColor,
... ... @@ -136,7 +152,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
136 152 validateDatasources: validateDatasources,
137 153 createKey: createKey,
138 154 createLabelFromDatasource: createLabelFromDatasource,
139   - insertVariable: insertVariable
  155 + insertVariable: insertVariable,
  156 + customTranslation: customTranslation
140 157 }
141 158
142 159 return service;
... ... @@ -154,6 +171,31 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
154 171 return materialColors[colorIndex].value;
155 172 }
156 173
  174 + function getMaterialIcons() {
  175 + var deferred = $q.defer();
  176 + if (materialIcons.length) {
  177 + deferred.resolve(materialIcons);
  178 + } else {
  179 + $timeout(function() {
  180 + var codepointsArray = materialIconsCodepoints.split("\n");
  181 + codepointsArray.forEach(function (codepoint) {
  182 + if (codepoint && codepoint.length) {
  183 + var values = codepoint.split(' ');
  184 + if (values && values.length == 2) {
  185 + materialIcons.push(values[0]);
  186 + }
  187 + }
  188 + });
  189 + deferred.resolve(materialIcons);
  190 + });
  191 + }
  192 + return deferred.promise;
  193 + }
  194 +
  195 + function getCommonMaterialIcons() {
  196 + return commonMaterialIcons;
  197 + }
  198 +
157 199 function genMaterialColor(str) {
158 200 var hash = Math.abs(hashCode(str));
159 201 return getMaterialColor(hash);
... ... @@ -432,4 +474,16 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
432 474 return result;
433 475 }
434 476
  477 + function customTranslation(translationValue, defaultValue) {
  478 + var result = '';
  479 + var translationId = types.translate.customTranslationsPrefix + translationValue;
  480 + var translation = $translate.instant(translationId);
  481 + if (translation != translationId) {
  482 + result = translation + '';
  483 + } else {
  484 + result = defaultValue;
  485 + }
  486 + return result;
  487 + }
  488 +
435 489 }
... ...
... ... @@ -87,23 +87,32 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
87 87 dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then(
88 88 function success(dashboard) {
89 89 scope.dashboard = dashboard;
  90 + startWatchers();
90 91 },
91 92 function fail() {
92 93 scope.dashboard = null;
  94 + scope.updateView();
  95 + startWatchers();
93 96 }
94 97 );
95 98 } else {
96 99 scope.dashboard = null;
  100 + startWatchers();
97 101 }
98 102 }
99 103
100   - scope.$watch('dashboard', function () {
101   - scope.updateView();
102   - });
103   -
104   - scope.$watch('disabled', function () {
105   - scope.updateView();
106   - });
  104 + function startWatchers() {
  105 + scope.$watch('dashboard', function (newVal, prevVal) {
  106 + if (!angular.equals(newVal, prevVal)) {
  107 + scope.updateView();
  108 + }
  109 + });
  110 + scope.$watch('disabled', function (newVal, prevVal) {
  111 + if (!angular.equals(newVal, prevVal)) {
  112 + scope.updateView();
  113 + }
  114 + });
  115 + }
107 116
108 117 if (scope.selectFirstDashboard) {
109 118 var pageLink = {limit: 1, textSearch: ''};
... ... @@ -111,6 +120,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
111 120 var dashboards = result.data;
112 121 if (dashboards.length > 0) {
113 122 scope.dashboard = dashboards[0];
  123 + scope.updateView();
114 124 }
115 125 }, function fail() {
116 126 });
... ...
... ... @@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
20 20 import angularGridster from 'angular-gridster';
21 21 import thingsboardTypes from '../common/types.constant';
22 22 import thingsboardApiWidget from '../api/widget.service';
23   -import thingsboardWidget from './widget.directive';
  23 +import thingsboardWidget from './widget/widget.directive';
24 24 import thingsboardToast from '../services/toast';
25 25 import thingsboardTimewindow from './timewindow.directive';
26 26 import thingsboardEvents from './tb-event-directives';
... ... @@ -187,6 +187,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
187 187 vm.showWidgetActions = showWidgetActions;
188 188 vm.widgetTitleStyle = widgetTitleStyle;
189 189 vm.widgetTitle = widgetTitle;
  190 + vm.customWidgetHeaderActions = customWidgetHeaderActions;
190 191 vm.widgetActions = widgetActions;
191 192 vm.dropWidgetShadow = dropWidgetShadow;
192 193 vm.enableWidgetFullscreen = enableWidgetFullscreen;
... ... @@ -875,6 +876,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
875 876 }
876 877 }
877 878
  879 + function customWidgetHeaderActions(widget) {
  880 + var ctx = widgetContext(widget);
  881 + if (ctx && ctx.customHeaderActions && ctx.customHeaderActions.length) {
  882 + return ctx.customHeaderActions;
  883 + } else {
  884 + return [];
  885 + }
  886 + }
  887 +
878 888 function widgetActions(widget) {
879 889 var ctx = widgetContext(widget);
880 890 if (ctx && ctx.widgetActions && ctx.widgetActions.length) {
... ...
... ... @@ -52,6 +52,16 @@
52 52 <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
53 53 </div>
54 54 <div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
  55 + <md-button ng-repeat="action in vm.customWidgetHeaderActions(widget)"
  56 + aria-label="{{action.displayName}}"
  57 + ng-show="!vm.isEdit"
  58 + ng-click="action.onAction($event)"
  59 + class="md-icon-button">
  60 + <md-tooltip md-direction="top">
  61 + {{action.displayName}}
  62 + </md-tooltip>
  63 + <ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
  64 + </md-button>
55 65 <md-button ng-repeat="action in vm.widgetActions(widget)"
56 66 aria-label="{{ action.name | translate }}"
57 67 ng-show="!vm.isEdit && action.show"
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +export default angular.module('thingsboard.directives.finishRender', [])
  18 + .directive('tbOnFinishRender', OnFinishRender)
  19 + .name;
  20 +
  21 +/*@ngInject*/
  22 +function OnFinishRender($timeout) {
  23 + return {
  24 + restrict: 'A',
  25 + link: function (scope, element, attr) {
  26 + if (scope.$last === true) {
  27 + $timeout(function () {
  28 + scope.$emit(attr.tbOnFinishRender);
  29 + });
  30 + }
  31 + }
  32 + };
  33 +}
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './material-icon-select.scss';
  18 +
  19 +import MaterialIconsDialogController from './material-icons-dialog.controller';
  20 +
  21 +/* eslint-disable import/no-unresolved, import/default */
  22 +
  23 +import materialIconSelectTemplate from './material-icon-select.tpl.html';
  24 +import materialIconsDialogTemplate from './material-icons-dialog.tpl.html';
  25 +
  26 +/* eslint-enable import/no-unresolved, import/default */
  27 +
  28 +
  29 +export default angular.module('thingsboard.directives.materialIconSelect', [])
  30 + .controller('MaterialIconsDialogController', MaterialIconsDialogController)
  31 + .directive('tbMaterialIconSelect', MaterialIconSelect)
  32 + .name;
  33 +
  34 +/*@ngInject*/
  35 +function MaterialIconSelect($compile, $templateCache, $document, $mdDialog) {
  36 +
  37 + var linker = function (scope, element, attrs, ngModelCtrl) {
  38 + var template = $templateCache.get(materialIconSelectTemplate);
  39 + element.html(template);
  40 +
  41 + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
  42 + scope.icon = null;
  43 +
  44 + scope.updateView = function () {
  45 + ngModelCtrl.$setViewValue(scope.icon);
  46 + }
  47 +
  48 + ngModelCtrl.$render = function () {
  49 + if (ngModelCtrl.$viewValue) {
  50 + scope.icon = ngModelCtrl.$viewValue;
  51 + }
  52 + if (!scope.icon || !scope.icon.length) {
  53 + scope.icon = 'more_horiz';
  54 + }
  55 + }
  56 +
  57 + scope.$watch('icon', function () {
  58 + scope.updateView();
  59 + });
  60 +
  61 + scope.openIconDialog = function($event) {
  62 + if ($event) {
  63 + $event.stopPropagation();
  64 + }
  65 + $mdDialog.show({
  66 + controller: 'MaterialIconsDialogController',
  67 + controllerAs: 'vm',
  68 + templateUrl: materialIconsDialogTemplate,
  69 + parent: angular.element($document[0].body),
  70 + locals: {icon: scope.icon},
  71 + skipHide: true,
  72 + fullscreen: true,
  73 + targetEvent: $event
  74 + }).then(function (icon) {
  75 + scope.icon = icon;
  76 + });
  77 + }
  78 +
  79 + $compile(element.contents())(scope);
  80 + }
  81 +
  82 + return {
  83 + restrict: "E",
  84 + require: "^ngModel",
  85 + link: linker,
  86 + scope: {
  87 + tbRequired: '=?',
  88 + }
  89 + };
  90 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-material-icon-select {
  18 + md-icon {
  19 + padding: 4px;
  20 + margin: 8px 4px 4px;
  21 + cursor: pointer;
  22 + border: solid 1px rgba(0,0,0,0.27);
  23 + }
  24 + md-input-container {
  25 + margin-bottom: 0px;
  26 + }
  27 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-material-icon-select" layout="row">
  19 + <md-icon class="material-icons" ng-click="openIconDialog($event)">{{icon}}</md-icon>
  20 + <md-input-container flex>
  21 + <label translate>icon.icon</label>
  22 + <input ng-mousedown="openIconDialog($event)" ng-model="icon">
  23 + </md-input-container>
  24 +</div>
\ No newline at end of file
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './material-icons-dialog.scss';
  18 +
  19 +/*@ngInject*/
  20 +export default function MaterialIconsDialogController($scope, $mdDialog, $timeout, utils, icon) {
  21 +
  22 + var vm = this;
  23 +
  24 + vm.selectedIcon = icon;
  25 +
  26 + vm.showAll = false;
  27 + vm.loadingIcons = false;
  28 +
  29 + $scope.$watch('vm.showAll', function(showAll) {
  30 + if (showAll) {
  31 + vm.loadingIcons = true;
  32 + $timeout(function() {
  33 + utils.getMaterialIcons().then(
  34 + function success(icons) {
  35 + vm.icons = icons;
  36 + }
  37 + );
  38 + });
  39 + } else {
  40 + vm.icons = utils.getCommonMaterialIcons();
  41 + }
  42 + });
  43 +
  44 + $scope.$on('iconsLoadFinished', function() {
  45 + vm.loadingIcons = false;
  46 + });
  47 +
  48 + vm.cancel = cancel;
  49 + vm.selectIcon = selectIcon;
  50 +
  51 + function cancel() {
  52 + $mdDialog.cancel();
  53 + }
  54 +
  55 + function selectIcon($event, icon) {
  56 + vm.selectedIcon = icon;
  57 + $mdDialog.hide(vm.selectedIcon);
  58 + }
  59 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-material-icons-dialog {
  18 + button.md-icon-button.tb-select-icon-button {
  19 + border: solid 1px orange;
  20 + border-radius: 0%;
  21 + padding: 16px;
  22 + height: 56px;
  23 + width: 56px;
  24 + margin: 10px;
  25 + }
  26 + .tb-icons-load {
  27 + top: 64px;
  28 + background: rgba(255,255,255,0.75);
  29 + z-index: 3;
  30 + }
  31 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog class="tb-material-icons-dialog" aria-label="{{'icon.material-icons' | translate }}" style="min-width: 600px;">
  19 + <form>
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2>{{ 'icon.select-icon' | translate }}</h2>
  23 + <span flex></span>
  24 + <section layout="row" layout-align="start center">
  25 + <md-switch ng-model="vm.showAll"
  26 + aria-label="{{ 'icon.show-all' | translate }}">
  27 + </md-switch>
  28 + <label translate>icon.show-all</label>
  29 + </section>
  30 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  31 + <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
  32 + </md-button>
  33 + </div>
  34 + </md-toolbar>
  35 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
  36 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  37 + <div class="tb-absolute-fill tb-icons-load" ng-show="vm.loadingIcons" layout="column" layout-align="center center">
  38 + <md-progress-circular md-mode="indeterminate" ng-disabled="!vm.loadingIcons" class="md-accent" md-diameter="40"></md-progress-circular>
  39 + </div>
  40 + <md-dialog-content>
  41 + <div class="md-dialog-content">
  42 + <md-content class="md-padding" layout="column">
  43 + <fieldset ng-disabled="loading">
  44 + <md-button ng-class="{'md-primary md-raised': icon == vm.selectedIcon}" class="tb-select-icon-button md-icon-button"
  45 + ng-repeat="icon in vm.icons" ng-click="vm.selectIcon($event, icon)" tb-on-finish-render="iconsLoadFinished">
  46 + <md-icon class="material-icons">{{icon}}</md-icon>
  47 + <md-tooltip md-direction="bottom">
  48 + {{ icon }}
  49 + </md-tooltip>
  50 + </md-button>
  51 + </fieldset>
  52 + </md-content>
  53 + </div>
  54 + </md-dialog-content>
  55 + <md-dialog-actions layout="row">
  56 + <span flex></span>
  57 + <md-button ng-disabled="loading" ng-click="vm.cancel()">
  58 + {{ 'action.cancel' | translate }}
  59 + </md-button>
  60 + </md-dialog-actions>
  61 + </form>
  62 +</md-dialog>
... ...
  1 +/**
  2 + * Created by igor on 6/20/17.
  3 + */
  4 +/*
  5 + * Copyright © 2016-2017 The Thingsboard Authors
  6 + *
  7 + * Licensed under the Apache License, Version 2.0 (the "License");
  8 + * you may not use this file except in compliance with the License.
  9 + * You may obtain a copy of the License at
  10 + *
  11 + * http://www.apache.org/licenses/LICENSE-2.0
  12 + *
  13 + * Unless required by applicable law or agreed to in writing, software
  14 + * distributed under the License is distributed on an "AS IS" BASIS,
  15 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16 + * See the License for the specific language governing permissions and
  17 + * limitations under the License.
  18 + */
  19 +
  20 +import './manage-widget-actions.scss';
  21 +
  22 +import thingsboardMaterialIconSelect from '../../material-icon-select.directive';
  23 +
  24 +import WidgetActionDialogController from './widget-action-dialog.controller';
  25 +
  26 +/* eslint-disable import/no-unresolved, import/default */
  27 +
  28 +import manageWidgetActionsTemplate from './manage-widget-actions.tpl.html';
  29 +import widgetActionDialogTemplate from './widget-action-dialog.tpl.html';
  30 +
  31 +/* eslint-enable import/no-unresolved, import/default */
  32 +
  33 +export default angular.module('thingsboard.directives.widgetActions', [thingsboardMaterialIconSelect])
  34 + .controller('WidgetActionDialogController', WidgetActionDialogController)
  35 + .directive('tbManageWidgetActions', ManageWidgetActions)
  36 + .name;
  37 +
  38 +/*@ngInject*/
  39 +function ManageWidgetActions() {
  40 + return {
  41 + restrict: "E",
  42 + scope: true,
  43 + bindToController: {
  44 + actionSources: '=',
  45 + widgetActions: '=',
  46 + fetchDashboardStates: '&',
  47 + },
  48 + controller: ManageWidgetActionsController,
  49 + controllerAs: 'vm',
  50 + templateUrl: manageWidgetActionsTemplate
  51 + };
  52 +}
  53 +
  54 +/* eslint-disable angular/angularelement */
  55 +
  56 +
  57 +/*@ngInject*/
  58 +function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter,
  59 + $translate, $timeout, utils, types) {
  60 +
  61 + let vm = this;
  62 +
  63 + vm.allActions = [];
  64 +
  65 + vm.actions = [];
  66 + vm.actionsCount = 0;
  67 +
  68 + vm.query = {
  69 + order: 'actionSourceName',
  70 + limit: 10,
  71 + page: 1,
  72 + search: null
  73 + };
  74 +
  75 + vm.enterFilterMode = enterFilterMode;
  76 + vm.exitFilterMode = exitFilterMode;
  77 + vm.onReorder = onReorder;
  78 + vm.onPaginate = onPaginate;
  79 + vm.addAction = addAction;
  80 + vm.editAction = editAction;
  81 + vm.deleteAction = deleteAction;
  82 +
  83 + $timeout(function(){
  84 + $scope.manageWidgetActionsForm.querySearchInput.$pristine = false;
  85 + });
  86 +
  87 + $scope.$watch('vm.widgetActions', function() {
  88 + if (vm.widgetActions) {
  89 + reloadActions();
  90 + }
  91 + });
  92 +
  93 + $scope.$watch("vm.query.search", function(newVal, prevVal) {
  94 + if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
  95 + updateActions();
  96 + }
  97 + });
  98 +
  99 + function enterFilterMode () {
  100 + vm.query.search = '';
  101 + }
  102 +
  103 + function exitFilterMode () {
  104 + vm.query.search = null;
  105 + updateActions();
  106 + }
  107 +
  108 + function onReorder () {
  109 + updateActions();
  110 + }
  111 +
  112 + function onPaginate () {
  113 + updateActions();
  114 + }
  115 +
  116 + function addAction($event) {
  117 + if ($event) {
  118 + $event.stopPropagation();
  119 + }
  120 + openWidgetActionDialog($event, null, true);
  121 + }
  122 +
  123 + function editAction ($event, action) {
  124 + if ($event) {
  125 + $event.stopPropagation();
  126 + }
  127 + openWidgetActionDialog($event, action, false);
  128 + }
  129 +
  130 + function deleteAction($event, action) {
  131 + if ($event) {
  132 + $event.stopPropagation();
  133 + }
  134 + if (action) {
  135 + var title = $translate.instant('widget-config.delete-action-title');
  136 + var content = $translate.instant('widget-config.delete-action-text', {actionName: action.name});
  137 + var confirm = $mdDialog.confirm()
  138 + .targetEvent($event)
  139 + .title(title)
  140 + .htmlContent(content)
  141 + .ariaLabel(title)
  142 + .cancel($translate.instant('action.no'))
  143 + .ok($translate.instant('action.yes'));
  144 +
  145 + confirm._options.skipHide = true;
  146 + confirm._options.fullscreen = true;
  147 +
  148 + $mdDialog.show(confirm).then(function () {
  149 + var index = getActionIndex(action.id, vm.allActions);
  150 + if (index > -1) {
  151 + vm.allActions.splice(index, 1);
  152 + }
  153 + var targetActions = vm.widgetActions[action.actionSourceId];
  154 + index = getActionIndex(action.id, targetActions);
  155 + if (index > -1) {
  156 + targetActions.splice(index, 1);
  157 + }
  158 + $scope.manageWidgetActionsForm.$setDirty();
  159 + updateActions();
  160 + });
  161 + }
  162 + }
  163 +
  164 + function openWidgetActionDialog($event, action, isAdd) {
  165 + var prevActionId = null;
  166 + if (!isAdd) {
  167 + prevActionId = action.id;
  168 + }
  169 + var availableActionSources = {};
  170 + for (var id in vm.actionSources) {
  171 + var actionSource = vm.actionSources[id];
  172 + if (actionSource.multiple) {
  173 + availableActionSources[id] = actionSource;
  174 + } else {
  175 + if (!isAdd && action.actionSourceId == id) {
  176 + availableActionSources[id] = actionSource;
  177 + } else {
  178 + var result = $filter('filter')(vm.allActions, {actionSourceId: id});
  179 + if (!result || !result.length) {
  180 + availableActionSources[id] = actionSource;
  181 + }
  182 + }
  183 + }
  184 + }
  185 + $mdDialog.show({
  186 + controller: 'WidgetActionDialogController',
  187 + controllerAs: 'vm',
  188 + templateUrl: widgetActionDialogTemplate,
  189 + parent: angular.element($document[0].body),
  190 + locals: {isAdd: isAdd, fetchDashboardStates: vm.fetchDashboardStates,
  191 + actionSources: availableActionSources, widgetActions: vm.widgetActions,
  192 + action: angular.copy(action)},
  193 + skipHide: true,
  194 + fullscreen: true,
  195 + targetEvent: $event
  196 + }).then(function (action) {
  197 + saveAction(action, prevActionId);
  198 + updateActions();
  199 + });
  200 + }
  201 +
  202 + function getActionIndex(id, actions) {
  203 + var result = $filter('filter')(actions, {id: id}, true);
  204 + if (result && result.length) {
  205 + return actions.indexOf(result[0]);
  206 + }
  207 + return -1;
  208 + }
  209 +
  210 + function saveAction(action, prevActionId) {
  211 + var actionSourceName = vm.actionSources[action.actionSourceId].name;
  212 + action.actionSourceName = utils.customTranslation(actionSourceName, actionSourceName);
  213 + action.typeName = $translate.instant(types.widgetActionTypes[action.type].name);
  214 + var actionSourceId = action.actionSourceId;
  215 + var widgetAction = angular.copy(action);
  216 + delete widgetAction.actionSourceId;
  217 + delete widgetAction.actionSourceName;
  218 + delete widgetAction.typeName;
  219 + var targetActions = vm.widgetActions[actionSourceId];
  220 + if (!targetActions) {
  221 + targetActions = [];
  222 + vm.widgetActions[actionSourceId] = targetActions;
  223 + }
  224 + if (prevActionId) {
  225 + var index = getActionIndex(prevActionId, vm.allActions);
  226 + if (index > -1) {
  227 + vm.allActions[index] = action;
  228 + }
  229 + index = getActionIndex(prevActionId, targetActions);
  230 + if (index > -1) {
  231 + targetActions[index] = widgetAction;
  232 + }
  233 + } else {
  234 + vm.allActions.push(action);
  235 + targetActions.push(widgetAction);
  236 + }
  237 + $scope.manageWidgetActionsForm.$setDirty();
  238 + }
  239 +
  240 + function reloadActions() {
  241 + vm.allActions = [];
  242 + vm.actions = [];
  243 + vm.actionsCount = 0;
  244 +
  245 + for (var actionSourceId in vm.widgetActions) {
  246 + var actionSource = vm.actionSources[actionSourceId];
  247 + var actionSourceActions = vm.widgetActions[actionSourceId];
  248 + for (var i=0;i<actionSourceActions.length;i++) {
  249 + var actionSourceAction = actionSourceActions[i];
  250 + var action = angular.copy(actionSourceAction);
  251 + action.actionSourceId = actionSourceId;
  252 + action.actionSourceName = utils.customTranslation(actionSource.name, actionSource.name);
  253 + action.typeName = $translate.instant(types.widgetActionTypes[actionSourceAction.type].name);
  254 + vm.allActions.push(action);
  255 + }
  256 + }
  257 +
  258 + updateActions ();
  259 + }
  260 +
  261 + function updateActions () {
  262 + var result = $filter('orderBy')(vm.allActions, vm.query.order);
  263 + if (vm.query.search != null) {
  264 + result = $filter('filter')(result, {$: vm.query.search});
  265 + }
  266 + vm.actionsCount = result.length;
  267 + var startIndex = vm.query.limit * (vm.query.page - 1);
  268 + vm.actions = result.slice(startIndex, startIndex + vm.query.limit);
  269 + }
  270 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.tb-manage-widget-actions {
  17 + table.md-table {
  18 + tbody {
  19 + tr {
  20 + td {
  21 + &.tb-action-cell {
  22 + overflow: hidden;
  23 + text-overflow: ellipsis;
  24 + white-space: nowrap;
  25 + min-width: 100px;
  26 + max-width: 100px;
  27 + width: 100px;
  28 + }
  29 + }
  30 + }
  31 + }
  32 + }
  33 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div ng-form="manageWidgetActionsForm" class="tb-manage-widget-actions md-whiteframe-z1" layout="column">
  19 + <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search === null">
  20 + <div class="md-toolbar-tools">
  21 + <span translate>widget-config.actions</span>
  22 + <span flex></span>
  23 + <md-button class="md-icon-button" ng-click="vm.addAction($event)">
  24 + <md-icon>add</md-icon>
  25 + <md-tooltip md-direction="top">
  26 + {{ 'widget-config.add-action' | translate }}
  27 + </md-tooltip>
  28 + </md-button>
  29 + <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
  30 + <md-icon>search</md-icon>
  31 + <md-tooltip md-direction="top">
  32 + {{ 'action.search' | translate }}
  33 + </md-tooltip>
  34 + </md-button>
  35 + </div>
  36 + </md-toolbar>
  37 + <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
  38 + <div class="md-toolbar-tools">
  39 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  40 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  41 + <md-tooltip md-direction="top">
  42 + {{ 'widget-config.search-actions' | translate }}
  43 + </md-tooltip>
  44 + </md-button>
  45 + <md-input-container flex>
  46 + <label>&nbsp;</label>
  47 + <input ng-model="vm.query.search" name="querySearchInput" placeholder="{{ 'widget-config.search-actions' | translate }}"/>
  48 + </md-input-container>
  49 + <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
  50 + <md-icon aria-label="Close" class="material-icons">close</md-icon>
  51 + <md-tooltip md-direction="top">
  52 + {{ 'action.close' | translate }}
  53 + </md-tooltip>
  54 + </md-button>
  55 + </div>
  56 + </md-toolbar>
  57 + <md-table-container>
  58 + <table md-table>
  59 + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
  60 + <tr md-row>
  61 + <th md-column md-order-by="actionSourceName"><span translate>widget-config.action-source</span></th>
  62 + <th md-column md-order-by="name"><span translate>widget-config.action-name</span></th>
  63 + <th md-column md-order-by="icon"><span translate>widget-config.action-icon</span></th>
  64 + <th md-column md-order-by="typeName"><span translate>widget-config.action-type</span></th>
  65 + <th md-column><span>&nbsp</span></th>
  66 + </tr>
  67 + </thead>
  68 + <tbody md-body>
  69 + <tr md-row ng-repeat="action in vm.actions">
  70 + <td md-cell>{{action.actionSourceName}}</td>
  71 + <td md-cell>{{action.name}}</td>
  72 + <td md-cell>
  73 + <md-icon aria-label="{{ 'widget-config.action-icon' | translate }}" class="material-icons">{{action.icon}}</md-icon>
  74 + </td>
  75 + <td md-cell>{{action.typeName}}</td>
  76 + <td md-cell class="tb-action-cell">
  77 + <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
  78 + ng-click="vm.editAction($event, action)">
  79 + <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
  80 + <md-tooltip md-direction="top">
  81 + {{ 'widget-config.edit-action' | translate }}
  82 + </md-tooltip>
  83 + </md-button>
  84 + <md-button class="md-icon-button" aria-label="{{'action.delete' | translate}}" ng-click="vm.deleteAction($event, action)">
  85 + <md-icon aria-label="Delete" class="material-icons">delete</md-icon>
  86 + <md-tooltip md-direction="top">
  87 + {{ 'widget-config.delete-action' | translate }}
  88 + </md-tooltip>
  89 + </md-button>
  90 + </td>
  91 + </tr>
  92 + </tbody>
  93 + </table>
  94 + </md-table-container>
  95 + <md-table-pagination md-limit="vm.query.limit" md-limit-options="[10, 15, 20]"
  96 + md-page="vm.query.page" md-total="{{vm.actionsCount}}"
  97 + md-on-paginate="vm.onPaginate" md-page-select>
  98 + </md-table-pagination>
  99 +</div>
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/*@ngInject*/
  18 +export default function WidgetActionDialogController($scope, $mdDialog, $filter, $q, dashboardService, dashboardUtils, types, utils,
  19 + isAdd, fetchDashboardStates, actionSources, widgetActions, action) {
  20 +
  21 + var vm = this;
  22 +
  23 + vm.types = types;
  24 +
  25 + vm.isAdd = isAdd;
  26 + vm.fetchDashboardStates = fetchDashboardStates;
  27 + vm.actionSources = actionSources;
  28 + vm.widgetActions = widgetActions;
  29 +
  30 + vm.targetDashboardStateSearchText = '';
  31 +
  32 + vm.selectedDashboardStateIds = [];
  33 +
  34 + if (vm.isAdd) {
  35 + vm.action = {
  36 + id: utils.guid()
  37 + };
  38 + } else {
  39 + vm.action = action;
  40 + }
  41 +
  42 + vm.actionSourceName = actionSourceName;
  43 +
  44 + vm.targetDashboardStateSearchTextChanged = function() {
  45 + }
  46 +
  47 + vm.dashboardStateSearch = dashboardStateSearch;
  48 + vm.cancel = cancel;
  49 + vm.save = save;
  50 +
  51 + $scope.$watch("vm.action.name", function(newVal, prevVal) {
  52 + if (!angular.equals(newVal, prevVal) && vm.action.name != null) {
  53 + checkActionName();
  54 + }
  55 + });
  56 +
  57 + $scope.$watch("vm.action.actionSourceId", function(newVal, prevVal) {
  58 + if (!angular.equals(newVal, prevVal) && vm.action.actionSourceId != null) {
  59 + checkActionName();
  60 + }
  61 + });
  62 +
  63 + $scope.$watch("vm.action.targetDashboardId", function() {
  64 + vm.selectedDashboardStateIds = [];
  65 + if (vm.action.targetDashboardId) {
  66 + dashboardService.getDashboard(vm.action.targetDashboardId).then(
  67 + function success(dashboard) {
  68 + dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
  69 + var states = dashboard.configuration.states;
  70 + vm.selectedDashboardStateIds = Object.keys(states);
  71 + }
  72 + );
  73 + }
  74 + });
  75 +
  76 + $scope.$watch('vm.action.type', function(newType) {
  77 + if (newType) {
  78 + switch (newType) {
  79 + case vm.types.widgetActionTypes.openDashboardState.value:
  80 + case vm.types.widgetActionTypes.updateDashboardState.value:
  81 + case vm.types.widgetActionTypes.openDashboard.value:
  82 + if (angular.isUndefined(vm.action.setEntityId)) {
  83 + vm.action.setEntityId = true;
  84 + }
  85 + break;
  86 + }
  87 + }
  88 + });
  89 +
  90 + function checkActionName() {
  91 + var actionNameIsUnique = true;
  92 + if (vm.action.actionSourceId && vm.action.name) {
  93 + var sourceActions = vm.widgetActions[vm.action.actionSourceId];
  94 + if (sourceActions) {
  95 + var result = $filter('filter')(sourceActions, {name: vm.action.name}, true);
  96 + if (result && result.length && result[0].id !== vm.action.id) {
  97 + actionNameIsUnique = false;
  98 + }
  99 + }
  100 + }
  101 + $scope.theForm.name.$setValidity('actionNameNotUnique', actionNameIsUnique);
  102 + }
  103 +
  104 + function actionSourceName (actionSource) {
  105 + if (actionSource) {
  106 + return utils.customTranslation(actionSource.name, actionSource.name);
  107 + } else {
  108 + return '';
  109 + }
  110 + }
  111 +
  112 + function dashboardStateSearch (query) {
  113 + if (vm.action.type == vm.types.widgetActionTypes.openDashboard.value) {
  114 + var deferred = $q.defer();
  115 + var result = query ? vm.selectedDashboardStateIds.filter(
  116 + createFilterForDashboardState(query)) : vm.selectedDashboardStateIds;
  117 + if (result && result.length) {
  118 + deferred.resolve(result);
  119 + } else {
  120 + deferred.resolve([query]);
  121 + }
  122 + return deferred.promise;
  123 + } else {
  124 + return vm.fetchDashboardStates({query: query});
  125 + }
  126 + }
  127 +
  128 + function createFilterForDashboardState (query) {
  129 + var lowercaseQuery = angular.lowercase(query);
  130 + return function filterFn(stateId) {
  131 + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
  132 + };
  133 + }
  134 +
  135 + function cleanupAction(action) {
  136 + var result = {};
  137 + result.id = action.id;
  138 + result.actionSourceId = action.actionSourceId;
  139 + result.name = action.name;
  140 + result.icon = action.icon;
  141 + result.type = action.type;
  142 + switch (action.type) {
  143 + case vm.types.widgetActionTypes.openDashboardState.value:
  144 + case vm.types.widgetActionTypes.updateDashboardState.value:
  145 + result.targetDashboardStateId = action.targetDashboardStateId;
  146 + result.openRightLayout = action.openRightLayout;
  147 + result.setEntityId = action.setEntityId;
  148 + break;
  149 + case vm.types.widgetActionTypes.openDashboard.value:
  150 + result.targetDashboardId = action.targetDashboardId;
  151 + result.targetDashboardStateId = action.targetDashboardStateId;
  152 + result.setEntityId = action.setEntityId;
  153 + break;
  154 + case vm.types.widgetActionTypes.custom.value:
  155 + result.customFunction = action.customFunction;
  156 + break;
  157 + }
  158 + return result;
  159 + }
  160 +
  161 + function cancel() {
  162 + $mdDialog.cancel();
  163 + }
  164 +
  165 + function save() {
  166 + $scope.theForm.$setPristine();
  167 + $mdDialog.hide(cleanupAction(vm.action));
  168 + }
  169 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog class="tb-widget-action-dialog" aria-label="{{'widget-config.action' | translate }}" style="min-width: 600px;">
  19 + <form name="theForm" ng-submit="vm.save()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2>{{ (vm.isAdd ? 'widget-config.add-action' : 'widget-config.edit-action') | translate }}</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
  30 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  31 + <md-dialog-content>
  32 + <div class="md-dialog-content">
  33 + <md-content class="md-padding" layout="column">
  34 + <fieldset ng-disabled="loading" layout="column">
  35 + <md-input-container class="md-block">
  36 + <label translate>widget-config.action-source</label>
  37 + <md-select name="actionSource" required aria-label="{{ 'widget-config.action-source' | translate }}" ng-model="vm.action.actionSourceId">
  38 + <md-option ng-repeat="(actionSourceId, actionSource) in vm.actionSources" ng-value="actionSourceId">
  39 + {{vm.actionSourceName(actionSource)}}
  40 + </md-option>
  41 + </md-select>
  42 + <div ng-messages="theForm.actionSource.$error">
  43 + <div ng-message="required" translate>widget-config.action-source-required</div>
  44 + </div>
  45 + </md-input-container>
  46 + <md-input-container class="md-block">
  47 + <label translate>widget-config.action-name</label>
  48 + <input name="name" required ng-model="vm.action.name">
  49 + <div ng-messages="theForm.name.$error">
  50 + <div ng-message="required" translate>widget-config.action-name-required</div>
  51 + <div ng-message="actionNameNotUnique" translate>widget-config.action-name-not-unique</div>
  52 + </div>
  53 + </md-input-container>
  54 + <tb-material-icon-select ng-model="vm.action.icon">
  55 + </tb-material-icon-select>
  56 + <md-input-container class="md-block">
  57 + <label translate>widget-config.action-type</label>
  58 + <md-select name="actionType" required aria-label="{{ 'widget-config.action-type' | translate }}" ng-model="vm.action.type">
  59 + <md-option ng-repeat="actionType in vm.types.widgetActionTypes" ng-value="actionType.value">
  60 + {{ actionType.name | translate }}
  61 + </md-option>
  62 + </md-select>
  63 + <div ng-messages="theForm.actionType.$error">
  64 + <div ng-message="required" translate>widget-config.action-type-required</div>
  65 + </div>
  66 + </md-input-container>
  67 + <div layout="column"
  68 + style="padding-bottom: 20px;"
  69 + ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboard.value">
  70 + <div class="md-caption tb-required"
  71 + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
  72 + <tb-dashboard-autocomplete the-form="theForm"
  73 + tb-required="true"
  74 + ng-model="vm.action.targetDashboardId"
  75 + select-first-dashboard="false">
  76 + </tb-dashboard-autocomplete>
  77 + </div>
  78 + <md-autocomplete ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  79 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
  80 + vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
  81 + ng-required="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value"
  82 + md-no-cache="true"
  83 + md-input-name="targetDashboardState"
  84 + ng-model="vm.action.targetDashboardStateId"
  85 + md-selected-item="vm.action.targetDashboardStateId"
  86 + md-search-text="vm.targetDashboardStateSearchText"
  87 + md-search-text-change="vm.targetDashboardStateSearchTextChanged()"
  88 + md-items="item in vm.dashboardStateSearch(vm.targetDashboardStateSearchText)"
  89 + md-item-text="item"
  90 + md-min-length="0"
  91 + md-floating-label="{{ 'widget-action.target-dashboard-state' | translate }}"
  92 + md-select-on-match="true">
  93 + <md-item-template>
  94 + <div>
  95 + <span md-highlight-text="vm.targetDashboardStateSearchText" md-highlight-flags="^i">{{item}}</span>
  96 + </div>
  97 + </md-item-template>
  98 + <div ng-messages="theForm.targetDashboardState.$error">
  99 + <div translate ng-message="required">widget-action.target-dashboard-state-required</div>
  100 + </div>
  101 + </md-autocomplete>
  102 + <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  103 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value"
  104 + flex aria-label="{{ 'widget-action.open-right-layout' | translate }}"
  105 + ng-model="vm.action.openRightLayout">{{ 'widget-action.open-right-layout' | translate }}
  106 + </md-checkbox>
  107 + <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  108 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
  109 + vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
  110 + flex aria-label="{{ 'widget-action.set-entity-from-widget' | translate }}"
  111 + ng-model="vm.action.setEntityId">{{ 'widget-action.set-entity-from-widget' | translate }}
  112 + </md-checkbox>
  113 + <tb-js-func ng-if="vm.action.type == vm.types.widgetActionTypes.custom.value"
  114 + ng-model="vm.action.customFunction"
  115 + function-args="{{ ['$event', 'widgetContext', 'entityId'] }}"
  116 + validation-args="{{ [] }}">
  117 + </tb-js-func>
  118 + </fieldset>
  119 + </md-content>
  120 + </div>
  121 + </md-dialog-content>
  122 + <md-dialog-actions layout="row">
  123 + <span flex></span>
  124 + <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
  125 + class="md-raised md-primary">
  126 + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
  127 + </md-button>
  128 + <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
  129 + {{ 'action.cancel' | translate }}
  130 + </md-button>
  131 + </md-dialog-actions>
  132 + </form>
  133 +</md-dialog>
... ...
ui/src/app/components/widget/widget-config.directive.js renamed from ui/src/app/components/widget-config.directive.js
... ... @@ -14,13 +14,14 @@
14 14 * limitations under the License.
15 15 */
16 16 import jsonSchemaDefaults from 'json-schema-defaults';
17   -import thingsboardTypes from '../common/types.constant';
18   -import thingsboardUtils from '../common/utils.service';
19   -import thingsboardEntityAliasSelect from './entity-alias-select.directive';
20   -import thingsboardDatasource from './datasource.directive';
21   -import thingsboardTimewindow from './timewindow.directive';
22   -import thingsboardLegendConfig from './legend-config.directive';
23   -import thingsboardJsonForm from "./json-form.directive";
  17 +import thingsboardTypes from '../../common/types.constant';
  18 +import thingsboardUtils from '../../common/utils.service';
  19 +import thingsboardEntityAliasSelect from '../entity-alias-select.directive';
  20 +import thingsboardDatasource from '../datasource.directive';
  21 +import thingsboardTimewindow from '../timewindow.directive';
  22 +import thingsboardLegendConfig from '../legend-config.directive';
  23 +import thingsboardJsonForm from '../json-form.directive';
  24 +import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
24 25 import 'angular-ui-ace';
25 26
26 27 /* eslint-disable import/no-unresolved, import/default */
... ... @@ -38,6 +39,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar
38 39 thingsboardDatasource,
39 40 thingsboardTimewindow,
40 41 thingsboardLegendConfig,
  42 + thingsboardManageWidgetActions,
41 43 'ui.ace'])
42 44 .directive('tbWidgetConfig', WidgetConfig)
43 45 .name;
... ... @@ -117,6 +119,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
117 119 scope.showLegend = angular.isDefined(config.showLegend) ?
118 120 config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
119 121 scope.legendConfig = config.legendConfig;
  122 + scope.actions = config.actions;
  123 + if (!scope.actions) {
  124 + scope.actions = {};
  125 + }
120 126 if (scope.widgetType !== types.widgetType.rpc.value &&
121 127 scope.widgetType !== types.widgetType.alarm.value &&
122 128 scope.widgetType !== types.widgetType.static.value
... ... @@ -324,6 +330,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
324 330 }
325 331 });
326 332
  333 + scope.$watch('actions', function () {
  334 + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config) {
  335 + var value = ngModelCtrl.$viewValue;
  336 + var config = value.config;
  337 + config.actions = scope.actions;
  338 + ngModelCtrl.$setViewValue(value);
  339 + scope.updateValidity();
  340 + /*if (scope.theForm) {
  341 + scope.theForm.$setDirty();
  342 + }*/
  343 + }
  344 + }, true);
  345 +
327 346 scope.addDatasource = function () {
328 347 var newDatasource;
329 348 if (scope.functionsOnly) {
... ... @@ -443,11 +462,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
443 462 isDataEnabled: '=?',
444 463 widgetType: '=',
445 464 typeParameters: '=',
  465 + actionSources: '=',
446 466 widgetSettingsSchema: '=',
447 467 datakeySettingsSchema: '=',
448 468 aliasController: '=',
449 469 functionsOnly: '=',
450 470 fetchEntityKeys: '&',
  471 + fetchDashboardStates: '&',
451 472 onCreateEntityAlias: '&',
452 473 theForm: '='
453 474 },
... ...
ui/src/app/components/widget/widget-config.tpl.html renamed from ui/src/app/components/widget-config.tpl.html
... ... @@ -275,4 +275,13 @@
275 275 </ng-form>
276 276 </md-content>
277 277 </md-tab>
  278 + <md-tab label="{{ 'widget-config.actions' | translate }}">
  279 + <md-content class="md-padding" layout="column">
  280 + <tb-manage-widget-actions
  281 + action-sources="actionSources"
  282 + widget-actions="actions"
  283 + fetch-dashboard-states="fetchDashboardStates({query: query})">
  284 + </tb-manage-widget-actions>
  285 + </md-content>
  286 + </md-tab>
278 287 </md-tabs>
... ...
ui/src/app/components/widget/widget.controller.js renamed from ui/src/app/components/widget.controller.js
... ... @@ -15,12 +15,12 @@
15 15 */
16 16 import $ from 'jquery';
17 17 import 'javascript-detect-element-resize/detect-element-resize';
18   -import Subscription from '../api/subscription';
  18 +import Subscription from '../../api/subscription';
19 19
20 20 /* eslint-disable angular/angularelement */
21 21
22 22 /*@ngInject*/
23   -export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
  23 +export default function WidgetController($scope, $state, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
24 24 datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow,
25 25 dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
26 26
... ... @@ -44,6 +44,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
44 44
45 45 var cafs = {};
46 46
  47 + var actionDescriptorsBySourceId = {};
  48 + if (widget.config.actions) {
  49 + for (var actionSourceId in widget.config.actions) {
  50 + var descriptors = widget.config.actions[actionSourceId];
  51 + var actionDescriptors = [];
  52 + descriptors.forEach(function(descriptor) {
  53 + var actionDescriptor = angular.copy(descriptor);
  54 + actionDescriptor.displayName = utils.customTranslation(descriptor.name, descriptor.name);
  55 + actionDescriptors.push(actionDescriptor);
  56 + });
  57 + actionDescriptorsBySourceId[actionSourceId] = actionDescriptors;
  58 + }
  59 + }
  60 +
47 61 var widgetContext = {
48 62 inited: false,
49 63 $container: null,
... ... @@ -103,9 +117,32 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
103 117 utils: {
104 118 formatValue: formatValue
105 119 },
  120 + actionsApi: {
  121 + actionDescriptorsBySourceId: actionDescriptorsBySourceId,
  122 + getActionDescriptors: getActionDescriptors,
  123 + handleWidgetAction: handleWidgetAction
  124 + },
106 125 stateController: stateController
107 126 };
108 127
  128 + widgetContext.customHeaderActions = [];
  129 + var headerActionsDescriptors = getActionDescriptors(types.widgetActionSources.headerButton.value);
  130 + for (var i=0;i<headerActionsDescriptors.length;i++) {
  131 + var descriptor = headerActionsDescriptors[i];
  132 + var headerAction = {};
  133 + headerAction.name = descriptor.name;
  134 + headerAction.displayName = descriptor.displayName;
  135 + headerAction.icon = descriptor.icon;
  136 + headerAction.descriptor = descriptor;
  137 + headerAction.onAction = function($event) {
  138 + var entityInfo = getFirstEntityInfo();
  139 + var entityId = entityInfo ? entityInfo.entityId : null;
  140 + var entityName = entityInfo ? entityInfo.entityName : null;
  141 + handleWidgetAction($event, this.descriptor, entityId, entityName);
  142 + }
  143 + widgetContext.customHeaderActions.push(headerAction);
  144 + }
  145 +
109 146 var subscriptionContext = {
110 147 $scope: $scope,
111 148 $q: $q,
... ... @@ -376,6 +413,87 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
376 413 return deferred.promise;
377 414 }
378 415
  416 + function getActionDescriptors(actionSourceId) {
  417 + var result = widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId];
  418 + if (!result) {
  419 + result = [];
  420 + }
  421 + return result;
  422 + }
  423 +
  424 + function handleWidgetAction($event, descriptor, entityId, entityName) {
  425 + var type = descriptor.type;
  426 + switch (type) {
  427 + case types.widgetActionTypes.openDashboardState.value:
  428 + case types.widgetActionTypes.updateDashboardState.value:
  429 + var targetDashboardStateId = descriptor.targetDashboardStateId;
  430 + var targetEntityId;
  431 + if (descriptor.setEntityId) {
  432 + targetEntityId = entityId;
  433 + }
  434 + var params = {};
  435 + if (targetEntityId) {
  436 + params.entityId = targetEntityId;
  437 + if (entityName) {
  438 + params.entityName = entityName;
  439 + }
  440 + }
  441 + if (type == types.widgetActionTypes.openDashboardState.value) {
  442 + widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
  443 + } else {
  444 + widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout);
  445 + }
  446 + break;
  447 + case types.widgetActionTypes.openDashboard.value:
  448 + var targetDashboardId = descriptor.targetDashboardId;
  449 + targetDashboardStateId = descriptor.targetDashboardStateId;
  450 + targetEntityId;
  451 + if (descriptor.setEntityId) {
  452 + targetEntityId = entityId;
  453 + }
  454 + var stateObject = {};
  455 + stateObject.params = {};
  456 + if (targetEntityId) {
  457 + stateObject.params.entityId = targetEntityId;
  458 + if (entityName) {
  459 + stateObject.params.entityName = entityName;
  460 + }
  461 + }
  462 + if (targetDashboardStateId) {
  463 + stateObject.id = targetDashboardStateId;
  464 + }
  465 + var stateParams = {
  466 + dashboardId: targetDashboardId,
  467 + state: angular.toJson([ stateObject ])
  468 + }
  469 + $state.go('home.dashboards.dashboard', stateParams);
  470 + break;
  471 + case types.widgetActionTypes.custom.value:
  472 + var customFunction = descriptor.customFunction;
  473 + if (angular.isDefined(customFunction) && customFunction.length > 0) {
  474 + try {
  475 + var customActionFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', customFunction);
  476 + customActionFunction($event, widgetContext, entityId, entityName);
  477 + } catch (e) {
  478 + //
  479 + }
  480 + }
  481 + break;
  482 + }
  483 + }
  484 +
  485 + function getFirstEntityInfo() {
  486 + var entityInfo;
  487 + for (var id in widgetContext.subscriptions) {
  488 + var subscription = widgetContext.subscriptions[id];
  489 + entityInfo = subscription.getFirstEntityInfo();
  490 + if (entityInfo) {
  491 + break;
  492 + }
  493 + }
  494 + return entityInfo;
  495 + }
  496 +
379 497 function configureWidgetElement() {
380 498
381 499 $scope.displayLegend = angular.isDefined(widget.config.showLegend) ?
... ...
ui/src/app/components/widget/widget.directive.js renamed from ui/src/app/components/widget.directive.js
... ... @@ -16,9 +16,9 @@
16 16
17 17 import './widget.scss';
18 18
19   -import thingsboardLegend from './legend.directive';
20   -import thingsboardTypes from '../common/types.constant';
21   -import thingsboardApiDatasource from '../api/datasource.service';
  19 +import thingsboardLegend from '../legend.directive';
  20 +import thingsboardTypes from '../../common/types.constant';
  21 +import thingsboardApiDatasource from '../../api/datasource.service';
22 22
23 23 import WidgetController from './widget.controller';
24 24
... ...
ui/src/app/components/widget/widget.scss renamed from ui/src/app/components/widget.scss
... ... @@ -36,6 +36,7 @@ export default function AddWidgetController($scope, widgetService, entityService
36 36 vm.add = add;
37 37 vm.cancel = cancel;
38 38 vm.fetchEntityKeys = fetchEntityKeys;
  39 + vm.fetchDashboardStates = fetchDashboardStates;
39 40 vm.createEntityAlias = createEntityAlias;
40 41
41 42 vm.widgetConfig = {
... ... @@ -128,6 +129,26 @@ export default function AddWidgetController($scope, widgetService, entityService
128 129 return deferred.promise;
129 130 }
130 131
  132 + function fetchDashboardStates (query) {
  133 + var deferred = $q.defer();
  134 + var stateIds = Object.keys(vm.dashboard.configuration.states);
  135 + var result = query ? stateIds.filter(
  136 + createFilterForDashboardState(query)) : stateIds;
  137 + if (result && result.length) {
  138 + deferred.resolve(result);
  139 + } else {
  140 + deferred.resolve([query]);
  141 + }
  142 + return deferred.promise;
  143 + }
  144 +
  145 + function createFilterForDashboardState (query) {
  146 + var lowercaseQuery = angular.lowercase(query);
  147 + return function filterFn(stateId) {
  148 + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
  149 + };
  150 + }
  151 +
131 152 function createEntityAlias (event, alias, allowedEntityTypes) {
132 153
133 154 var deferred = $q.defer();
... ...
... ... @@ -34,6 +34,7 @@
34 34 <fieldset ng-disabled="loading" style="position: relative; height: 600px;">
35 35 <tb-widget-config widget-type="vm.widget.type"
36 36 type-parameters="vm.widgetInfo.typeParameters"
  37 + action-sources="vm.widgetInfo.actionSources"
37 38 force-expand-datasources="true"
38 39 ng-model="vm.widgetConfig"
39 40 widget-settings-schema="vm.settingsSchema"
... ... @@ -41,6 +42,7 @@
41 42 alias-controller="vm.aliasController"
42 43 functions-only="vm.functionsOnly"
43 44 fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
  45 + fetch-dashboard-states="vm.fetchDashboardStates(query)"
44 46 on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)"
45 47 the-form="theForm"></tb-widget-config>
46 48 </fieldset>
... ...
... ... @@ -41,6 +41,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
41 41 var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
42 42 var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
43 43 scope.typeParameters = widgetInfo.typeParameters;
  44 + scope.actionSources = widgetInfo.actionSources;
44 45 scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources;
45 46 if (!settingsSchema || settingsSchema === '') {
46 47 scope.settingsSchema = {};
... ... @@ -93,6 +94,26 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
93 94 return deferred.promise;
94 95 };
95 96
  97 + scope.fetchDashboardStates = function(query) {
  98 + var deferred = $q.defer();
  99 + var stateIds = Object.keys(scope.dashboard.configuration.states);
  100 + var result = query ? stateIds.filter(
  101 + createFilterForDashboardState(query)) : stateIds;
  102 + if (result && result.length) {
  103 + deferred.resolve(result);
  104 + } else {
  105 + deferred.resolve([query]);
  106 + }
  107 + return deferred.promise;
  108 + }
  109 +
  110 + function createFilterForDashboardState (query) {
  111 + var lowercaseQuery = angular.lowercase(query);
  112 + return function filterFn(stateId) {
  113 + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
  114 + };
  115 + }
  116 +
96 117 scope.createEntityAlias = function (event, alias, allowedEntityTypes) {
97 118
98 119 var deferred = $q.defer();
... ...
... ... @@ -18,6 +18,7 @@
18 18 <fieldset ng-disabled="loading">
19 19 <tb-widget-config widget-type="widget.type"
20 20 type-parameters="typeParameters"
  21 + action-sources="actionSources"
21 22 ng-model="widgetConfig"
22 23 is-data-enabled="isDataEnabled"
23 24 widget-settings-schema="settingsSchema"
... ... @@ -25,6 +26,7 @@
25 26 alias-controller="aliasController"
26 27 functions-only="widgetEditMode"
27 28 fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
  29 + fetch-dashboard-states="fetchDashboardStates(query)"
28 30 on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
29 31 the-form="theForm"></tb-widget-config>
30 32 </fieldset>
... ...
... ... @@ -23,7 +23,7 @@ import thingsboardApiUser from '../api/user.service';
23 23 import thingsboardApiDashboard from '../api/dashboard.service';
24 24 import thingsboardApiCustomer from '../api/customer.service';
25 25 import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
26   -import thingsboardWidgetConfig from '../components/widget-config.directive';
  26 +import thingsboardWidgetConfig from '../components/widget/widget-config.directive';
27 27 import thingsboardDashboardSelect from '../components/dashboard-select.directive';
28 28 import thingsboardRelatedEntityAutocomplete from '../components/related-entity-autocomplete.directive';
29 29 import thingsboardDashboard from '../components/dashboard.directive';
... ...
... ... @@ -59,10 +59,10 @@
59 59 <span flex></span>
60 60 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
61 61 class="md-raised md-primary">
62   - {{ vm.isAdd ? 'Add' : 'Save' }}
  62 + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
63 63 </md-button>
64 64 <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
65   - Cancel
  65 + {{ 'action.cancel' | translate }}
66 66 </md-button>
67 67 </md-dialog-actions>
68 68 </form>
... ...
... ... @@ -15,7 +15,7 @@
15 15 */
16 16
17 17 /*@ngInject*/
18   -export default function DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) {
  18 +export default function DefaultStateController($scope, $location, $state, $stateParams, utils, types, dashboardUtils) {
19 19
20 20 var vm = this;
21 21
... ... @@ -50,6 +50,9 @@ export default function DefaultStateController($scope, $location, $state, $state
50 50 }
51 51
52 52 function updateState(id, params, openRightLayout) {
  53 + if (!id) {
  54 + id = getStateId();
  55 + }
53 56 if (vm.states && vm.states[id]) {
54 57 if (!params) {
55 58 params = {};
... ... @@ -110,15 +113,7 @@ export default function DefaultStateController($scope, $location, $state, $state
110 113 }
111 114
112 115 function getStateName(id, state) {
113   - var result = '';
114   - var translationId = types.translate.customTranslationsPrefix + state.name;
115   - var translation = $translate.instant(translationId);
116   - if (translation != translationId) {
117   - result = translation + '';
118   - } else {
119   - result = id;
120   - }
121   - return result;
  116 + return utils.customTranslation(state.name, id);
122 117 }
123 118
124 119 function parseState(stateJson) {
... ...
... ... @@ -55,6 +55,9 @@ export default function EntityStateController($scope, $location, $state, $stateP
55 55 }
56 56
57 57 function updateState(id, params, openRightLayout) {
  58 + if (!id) {
  59 + id = getStateId();
  60 + }
58 61 if (vm.states && vm.states[id]) {
59 62 resolveEntity(params).then(
60 63 function success(entityName) {
... ... @@ -121,17 +124,10 @@ export default function EntityStateController($scope, $location, $state, $stateP
121 124 var result = '';
122 125 if (vm.stateObject[index]) {
123 126 var stateName = vm.states[vm.stateObject[index].id].name;
124   - var translationId = types.translate.customTranslationsPrefix + stateName;
125   - var translation = $translate.instant(translationId);
126   - if (translation != translationId) {
127   - stateName = translation + '';
128   - }
  127 + stateName = utils.customTranslation(stateName, stateName);
129 128 var params = vm.stateObject[index].params;
130   - if (params && params.entityName) {
131   - result = utils.insertVariable(stateName, 'entityName', params.entityName);
132   - } else {
133   - result = stateName;
134   - }
  129 + var entityName = params && params.entityName ? params.entityName : '';
  130 + result = utils.insertVariable(stateName, 'entityName', entityName);
135 131 }
136 132 return result;
137 133 }
... ...
... ... @@ -175,8 +175,6 @@ export default function ManageDashboardStatesController($scope, $mdDialog, $filt
175 175 $scope.theForm.$setDirty();
176 176 updateStates();
177 177 });
178   -
179   -
180 178 }
181 179 }
182 180
... ...
... ... @@ -102,6 +102,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
102 102 if (vm.addToDashboardType === 0) {
103 103 dashboardService.getDashboard(vm.dashboardId).then(
104 104 function success(dashboard) {
  105 + dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
105 106 selectTargetState($event, dashboard).then(
106 107 function(targetState) {
107 108 selectTargetLayout($event, dashboard, targetState).then(
... ...
... ... @@ -26,6 +26,7 @@ import thingsboardApiLogin from '../api/login.service';
26 26 import thingsboardApiUser from '../api/user.service';
27 27
28 28 import thingsboardNoAnimate from '../components/no-animate.directive';
  29 +import thingsboardOnFinishRender from '../components/finish-render.directive';
29 30 import thingsboardSideMenu from '../components/side-menu.directive';
30 31 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
31 32
... ... @@ -81,6 +82,7 @@ export default angular.module('thingsboard.home', [
81 82 thingsboardApiLogin,
82 83 thingsboardApiUser,
83 84 thingsboardNoAnimate,
  85 + thingsboardOnFinishRender,
84 86 thingsboardSideMenu,
85 87 thingsboardDashboardAutocomplete
86 88 ])
... ...
... ... @@ -1103,6 +1103,18 @@ export default angular.module('thingsboard.locale', [])
1103 1103 "undo": "Undo widget changes",
1104 1104 "export": "Export widget"
1105 1105 },
  1106 + "widget-action": {
  1107 + "header-button": "Widget header button",
  1108 + "open-dashboard-state": "Navigate to new dashboard state",
  1109 + "update-dashboard-state": "Update current dashboard state",
  1110 + "open-dashboard": "Navigate to other dashboard",
  1111 + "custom": "Custom action",
  1112 + "target-dashboard-state": "Target dashboard state",
  1113 + "target-dashboard-state-required": "Target dashboard state is required",
  1114 + "set-entity-from-widget": "Set entity from widget",
  1115 + "target-dashboard": "Target dashboard",
  1116 + "open-right-layout": "Open right dashboard layout (mobile view)"
  1117 + },
1106 1118 "widgets-bundle": {
1107 1119 "current": "Current bundle",
1108 1120 "widgets-bundles": "Widgets Bundles",
... ... @@ -1158,7 +1170,23 @@ export default angular.module('thingsboard.locale', [])
1158 1170 "remove-datasource": "Remove datasource",
1159 1171 "add-datasource": "Add datasource",
1160 1172 "target-device": "Target device",
1161   - "alarm-source": "Alarm source"
  1173 + "alarm-source": "Alarm source",
  1174 + "actions": "Actions",
  1175 + "action": "Action",
  1176 + "add-action": "Add action",
  1177 + "search-actions": "Search actions",
  1178 + "action-source": "Action source",
  1179 + "action-source-required": "Action source is required.",
  1180 + "action-name": "Name",
  1181 + "action-name-required": "Action name is required.",
  1182 + "action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.",
  1183 + "action-icon": "Icon",
  1184 + "action-type": "Type",
  1185 + "action-type-required": "Action type is required.",
  1186 + "edit-action": "Edit action",
  1187 + "delete-action": "Delete action",
  1188 + "delete-action-title": "Delete widget action",
  1189 + "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?"
1162 1190 },
1163 1191 "widget-type": {
1164 1192 "import": "Import widget type",
... ... @@ -1168,6 +1196,12 @@ export default angular.module('thingsboard.locale', [])
1168 1196 "widget-type-file": "Widget type file",
1169 1197 "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure."
1170 1198 },
  1199 + "icon": {
  1200 + "icon": "Icon",
  1201 + "select-icon": "Select icon",
  1202 + "material-icons": "Material icons",
  1203 + "show-all": "Show all icons"
  1204 + },
1171 1205 "language": {
1172 1206 "language": "Language",
1173 1207 "en_US": "English",
... ... @@ -1177,6 +1211,10 @@ export default angular.module('thingsboard.locale', [])
1177 1211 "es_ES": "Spanish"
1178 1212 },
1179 1213 "custom": {
  1214 + "widget-action": {
  1215 + "action-cell-button": "Action cell button",
  1216 + "row-click": "On row click"
  1217 + }
1180 1218 }
1181 1219 }
1182 1220 }
... ...
... ... @@ -158,13 +158,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
158 158 vm.ctx.widgetActions = [ vm.searchAction ];
159 159
160 160 if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
161   - var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle;
162   - var translation = $translate.instant(translationId);
163   - if (translation != translationId) {
164   - vm.alarmsTitle = translation + '';
165   - } else {
166   - vm.alarmsTitle = vm.settings.alarmsTitle;
167   - }
  161 + vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle);
168 162 } else {
169 163 vm.alarmsTitle = $translate.instant('alarm.alarms');
170 164 }
... ... @@ -226,6 +220,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
226 220 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
227 221 'border-color: ' + mdDarkSecondary + ';\n'+
228 222 '}\n'+
  223 + 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+
  224 + 'color: ' + mdDarkSecondary + ';\n'+
  225 + '}\n'+
229 226 'table.md-table td.md-cell.md-placeholder {\n'+
230 227 'color: ' + mdDarkDisabled + ';\n'+
231 228 '}\n'+
... ... @@ -539,13 +536,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
539 536 for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
540 537 var dataKey = vm.alarmSource.dataKeys[d];
541 538
542   - var translationId = types.translate.customTranslationsPrefix + dataKey.label;
543   - var translation = $translate.instant(translationId);
544   - if (translation != translationId) {
545   - dataKey.title = translation + '';
546   - } else {
547   - dataKey.title = dataKey.label;
548   - }
  539 + dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
549 540
550 541 var keySettings = dataKey.settings;
551 542
... ...
... ... @@ -65,8 +65,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
65 65 vm.currentEntity = null;
66 66
67 67 vm.displayEntityName = true;
  68 + vm.entityNameColumnTitle = '';
68 69 vm.displayEntityType = true;
69   - vm.displayActions = false; //TODO: Widget actions
  70 + vm.actionCellDescriptors = [];
70 71 vm.displayPagination = true;
71 72 vm.defaultPageSize = 10;
72 73 vm.defaultSortOrder = 'entityName';
... ... @@ -92,6 +93,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
92 93 vm.onReorder = onReorder;
93 94 vm.onPaginate = onPaginate;
94 95 vm.onRowClick = onRowClick;
  96 + vm.onActionButtonClick = onActionButtonClick;
95 97 vm.isCurrent = isCurrent;
96 98
97 99 vm.cellStyle = cellStyle;
... ... @@ -141,14 +143,10 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
141 143
142 144 vm.ctx.widgetActions = [ vm.searchAction ];
143 145
  146 + vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton');
  147 +
144 148 if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) {
145   - var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle;
146   - var translation = $translate.instant(translationId);
147   - if (translation != translationId) {
148   - vm.entitiesTitle = translation + '';
149   - } else {
150   - vm.entitiesTitle = vm.settings.entitiesTitle;
151   - }
  149 + vm.entitiesTitle = utils.customTranslation(vm.settings.entitiesTitle, vm.settings.entitiesTitle);
152 150 } else {
153 151 vm.entitiesTitle = $translate.instant('entity.entities');
154 152 }
... ... @@ -157,6 +155,13 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
157 155
158 156 vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
159 157 vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true;
  158 +
  159 + if (vm.settings.entityNameColumnTitle && vm.settings.entityNameColumnTitle.length) {
  160 + vm.entityNameColumnTitle = utils.customTranslation(vm.settings.entityNameColumnTitle, vm.settings.entityNameColumnTitle);
  161 + } else {
  162 + vm.entityNameColumnTitle = $translate.instant('entity.entity-name');
  163 + }
  164 +
160 165 vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true;
161 166 vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
162 167
... ... @@ -185,6 +190,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
185 190 //var mdDarkIcon = mdDarkSecondary;
186 191 var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
187 192
  193 + //md-icon.md-default-theme, md-icon {
  194 +
188 195 var cssString = 'table.md-table th.md-column {\n'+
189 196 'color: ' + mdDarkSecondary + ';\n'+
190 197 '}\n'+
... ... @@ -204,6 +211,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
204 211 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
205 212 'border-color: ' + mdDarkSecondary + ';\n'+
206 213 '}\n'+
  214 + 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+
  215 + 'color: ' + mdDarkSecondary + ';\n'+
  216 + '}\n'+
207 217 'table.md-table td.md-cell.md-placeholder {\n'+
208 218 'color: ' + mdDarkDisabled + ';\n'+
209 219 '}\n'+
... ... @@ -261,11 +271,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
261 271 }
262 272
263 273 function onRowClick($event, entity) {
  274 + if ($event) {
  275 + $event.stopPropagation();
  276 + }
264 277 if (vm.currentEntity != entity) {
265 278 vm.currentEntity = entity;
  279 + var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick');
  280 + if (descriptors.length) {
  281 + var entityId;
  282 + var entityName;
  283 + if (vm.currentEntity) {
  284 + entityId = vm.currentEntity.id;
  285 + entityName = vm.currentEntity.entityName;
  286 + }
  287 + vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
  288 + }
266 289 }
267 290 }
268 291
  292 + function onActionButtonClick($event, entity, actionDescriptor) {
  293 + if ($event) {
  294 + $event.stopPropagation();
  295 + }
  296 + var entityId;
  297 + var entityName;
  298 + if (entity) {
  299 + entityId = entity.id;
  300 + entityName = entity.entityName;
  301 + }
  302 + vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName);
  303 + }
  304 +
269 305 function isCurrent(entity) {
270 306 return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) &&
271 307 (vm.currentEntity.id.id === entity.id.id);
... ... @@ -393,13 +429,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
393 429 }
394 430 vm.dataKeys.push(dataKey);
395 431
396   - var translationId = types.translate.customTranslationsPrefix + dataKey.label;
397   - var translation = $translate.instant(translationId);
398   - if (translation != translationId) {
399   - dataKey.title = translation + '';
400   - } else {
401   - dataKey.title = dataKey.label;
402   - }
  432 + dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
403 433
404 434 var keySettings = dataKey.settings;
405 435
... ...
... ... @@ -41,10 +41,10 @@
41 41 <table md-table>
42 42 <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
43 43 <tr md-row>
44   - <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span translate>entity.entity-name</span></th>
  44 + <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span>{{vm.entityNameColumnTitle}}</span></th>
45 45 <th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th>
46 46 <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th>
47   - <th md-column ng-if="vm.displayActions"><span>&nbsp</span></th>
  47 + <th md-column ng-if="vm.actionCellDescriptors.length"><span>&nbsp</span></th>
48 48 </tr>
49 49 </thead>
50 50 <tbody md-body>
... ... @@ -57,14 +57,18 @@
57 57 ng-style="vm.cellStyle(entity, key)"
58 58 ng-bind-html="vm.cellContent(entity, key)">
59 59 </td>
60   - <td md-cell ng-if="vm.displayActions" class="tb-action-cell">
61   - <!--md-button class="md-icon-button" aria-label="{{ 'entity.details' | translate }}"
62   - ng-click="vm.openEntityDetails($event, entity)">
63   - <md-icon aria-label="{{ 'entity.details' | translate }}" class="material-icons">more_horiz</md-icon>
  60 + <td md-cell ng-if="vm.actionCellDescriptors.length" class="tb-action-cell"
  61 + ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px',
  62 + maxWidth: vm.actionCellDescriptors.length*36+'px',
  63 + width: vm.actionCellDescriptors.length*36+'px'}">
  64 + <md-button class="md-icon-button" ng-repeat="actionDescriptor in vm.actionCellDescriptors"
  65 + aria-label="{{ actionDescriptor.displayName }}"
  66 + ng-click="vm.onActionButtonClick($event, entity, actionDescriptor)">
  67 + <md-icon aria-label="{{ actionDescriptor.displayName }}" class="material-icons">{{actionDescriptor.icon}}</md-icon>
64 68 <md-tooltip md-direction="top">
65   - {{ 'entity.details' | translate }}
  69 + {{ actionDescriptor.displayName }}
66 70 </md-tooltip>
67   - </md-button-->
  71 + </md-button>
68 72 </td>
69 73 </tr>
70 74 </tbody>
... ...