Commit 531f42450f94c1e70d34e4c30fca51248fdbc6df

Authored by Igor Kulikov
1 parent 0726fbb5

TB-63: Alarms widget initial implementation.

@@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', []) @@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', [])
18 .name; 18 .name;
19 19
20 /*@ngInject*/ 20 /*@ngInject*/
21 -function AlarmService($http, $q, $interval, $filter) { 21 +function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) {
  22 +
  23 + var alarmSourceListeners = {};
  24 +
  25 + var simulatedAlarm = {
  26 + createdTime: (new Date).getTime(),
  27 + startTs: (new Date).getTime(),
  28 + endTs: 0,
  29 + ackTs: 0,
  30 + clearTs: 0,
  31 + originatorName: 'Simulated',
  32 + originator: {
  33 + entityType: "DEVICE",
  34 + id: "1"
  35 + },
  36 + type: 'TEMPERATURE',
  37 + severity: "MAJOR",
  38 + status: types.alarmStatus.activeUnack,
  39 + details: {
  40 + message: "Temperature is high!"
  41 + }
  42 + };
  43 +
22 var service = { 44 var service = {
23 getAlarm: getAlarm, 45 getAlarm: getAlarm,
24 getAlarmInfo: getAlarmInfo, 46 getAlarmInfo: getAlarmInfo,
@@ -27,7 +49,9 @@ function AlarmService($http, $q, $interval, $filter) { @@ -27,7 +49,9 @@ function AlarmService($http, $q, $interval, $filter) {
27 clearAlarm: clearAlarm, 49 clearAlarm: clearAlarm,
28 getAlarms: getAlarms, 50 getAlarms: getAlarms,
29 pollAlarms: pollAlarms, 51 pollAlarms: pollAlarms,
30 - cancelPollAlarms: cancelPollAlarms 52 + cancelPollAlarms: cancelPollAlarms,
  53 + subscribeForAlarms: subscribeForAlarms,
  54 + unsubscribeFromAlarms: unsubscribeFromAlarms
31 } 55 }
32 56
33 return service; 57 return service;
@@ -171,12 +195,21 @@ function AlarmService($http, $q, $interval, $filter) { @@ -171,12 +195,21 @@ function AlarmService($http, $q, $interval, $filter) {
171 pageLink = { 195 pageLink = {
172 limit: alarmsQuery.limit 196 limit: alarmsQuery.limit
173 }; 197 };
174 - } else { 198 + } else if (alarmsQuery.interval) {
175 pageLink = { 199 pageLink = {
176 limit: 100, 200 limit: 100,
177 startTime: time - alarmsQuery.interval 201 startTime: time - alarmsQuery.interval
178 }; 202 };
  203 + } else if (alarmsQuery.startTime) {
  204 + pageLink = {
  205 + limit: 100,
  206 + startTime: alarmsQuery.startTime
  207 + }
  208 + if (alarmsQuery.endTime) {
  209 + pageLink.endTime = alarmsQuery.endTime;
  210 + }
179 } 211 }
  212 +
180 fetchAlarms(alarmsQuery, pageLink, deferred); 213 fetchAlarms(alarmsQuery, pageLink, deferred);
181 return deferred.promise; 214 return deferred.promise;
182 } 215 }
@@ -211,4 +244,59 @@ function AlarmService($http, $q, $interval, $filter) { @@ -211,4 +244,59 @@ function AlarmService($http, $q, $interval, $filter) {
211 } 244 }
212 } 245 }
213 246
  247 + function subscribeForAlarms(alarmSourceListener) {
  248 + alarmSourceListener.id = utils.guid();
  249 + alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener;
  250 + var alarmSource = alarmSourceListener.alarmSource;
  251 + if (alarmSource.type == types.datasourceType.function) {
  252 + $timeout(function() {
  253 + alarmSourceListener.alarmsUpdated([simulatedAlarm], false);
  254 + });
  255 + } else {
  256 + var pollingInterval = 5000; //TODO:
  257 + alarmSourceListener.alarmsQuery = {
  258 + entityType: alarmSource.entityType,
  259 + entityId: alarmSource.entityId,
  260 + alarmSearchStatus: null, //TODO:
  261 + alarmStatus: null
  262 + }
  263 + var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'});
  264 + if (originatorKeys && originatorKeys.length) {
  265 + alarmSourceListener.alarmsQuery.fetchOriginator = true;
  266 + }
  267 + var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow;
  268 + if (subscriptionTimewindow.realtimeWindowMs) {
  269 + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs;
  270 + } else {
  271 + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs;
  272 + alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs;
  273 + }
  274 + alarmSourceListener.alarmsQuery.onAlarms = function(alarms) {
  275 + if (subscriptionTimewindow.realtimeWindowMs) {
  276 + var now = Date.now();
  277 + if (alarmSourceListener.lastUpdateTs) {
  278 + var interval = now - alarmSourceListener.lastUpdateTs;
  279 + alarmSourceListener.alarmsQuery.startTime += interval;
  280 + } else {
  281 + alarmSourceListener.lastUpdateTs = now;
  282 + }
  283 + }
  284 + alarmSourceListener.alarmsUpdated(alarms, false);
  285 + }
  286 + onPollAlarms(alarmSourceListener.alarmsQuery);
  287 + alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval,
  288 + 0, false, alarmSourceListener.alarmsQuery);
  289 + }
  290 +
  291 + }
  292 +
  293 + function unsubscribeFromAlarms(alarmSourceListener) {
  294 + if (alarmSourceListener && alarmSourceListener.id) {
  295 + if (alarmSourceListener.pollPromise) {
  296 + $interval.cancel(alarmSourceListener.pollPromise);
  297 + alarmSourceListener.pollPromise = null;
  298 + }
  299 + delete alarmSourceListeners[alarmSourceListener.id];
  300 + }
  301 + }
214 } 302 }
@@ -14,8 +14,6 @@ @@ -14,8 +14,6 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
17 -const varsRegex = /\$\{([^\}]*)\}/g;  
18 -  
19 export default class AliasController { 17 export default class AliasController {
20 18
21 constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) { 19 constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) {
@@ -113,14 +111,14 @@ export default class AliasController { @@ -113,14 +111,14 @@ export default class AliasController {
113 } 111 }
114 } 112 }
115 113
116 - resolveDatasource(datasource) { 114 + resolveDatasource(datasource, isSingle) {
117 var deferred = this.$q.defer(); 115 var deferred = this.$q.defer();
118 if (datasource.type === this.types.datasourceType.entity) { 116 if (datasource.type === this.types.datasourceType.entity) {
119 if (datasource.entityAliasId) { 117 if (datasource.entityAliasId) {
120 this.getAliasInfo(datasource.entityAliasId).then( 118 this.getAliasInfo(datasource.entityAliasId).then(
121 function success(aliasInfo) { 119 function success(aliasInfo) {
122 datasource.aliasName = aliasInfo.alias; 120 datasource.aliasName = aliasInfo.alias;
123 - if (aliasInfo.resolveMultiple) { 121 + if (aliasInfo.resolveMultiple && !isSingle) {
124 var newDatasource; 122 var newDatasource;
125 var resolvedEntities = aliasInfo.resolvedEntities; 123 var resolvedEntities = aliasInfo.resolvedEntities;
126 if (resolvedEntities && resolvedEntities.length) { 124 if (resolvedEntities && resolvedEntities.length) {
@@ -178,30 +176,44 @@ export default class AliasController { @@ -178,30 +176,44 @@ export default class AliasController {
178 return deferred.promise; 176 return deferred.promise;
179 } 177 }
180 178
  179 + resolveAlarmSource(alarmSource) {
  180 + var deferred = this.$q.defer();
  181 + var aliasCtrl = this;
  182 + this.resolveDatasource(alarmSource, true).then(
  183 + function success(datasources) {
  184 + var datasource = datasources[0];
  185 + if (datasource.type === aliasCtrl.types.datasourceType.function) {
  186 + var name;
  187 + if (datasource.name && datasource.name.length) {
  188 + name = datasource.name;
  189 + } else {
  190 + name = aliasCtrl.types.datasourceType.function;
  191 + }
  192 + datasource.name = name;
  193 + datasource.aliasName = name;
  194 + datasource.entityName = name;
  195 + } else if (datasource.unresolvedStateEntity) {
  196 + datasource.name = "Unresolved";
  197 + datasource.entityName = "Unresolved";
  198 + }
  199 + deferred.resolve(datasource);
  200 + },
  201 + function fail() {
  202 + deferred.reject();
  203 + }
  204 + );
  205 + return deferred.promise;
  206 + }
  207 +
181 resolveDatasources(datasources) { 208 resolveDatasources(datasources) {
182 209
  210 + var aliasCtrl = this;
  211 +
183 function updateDataKeyLabel(dataKey, datasource) { 212 function updateDataKeyLabel(dataKey, datasource) {
184 if (!dataKey.pattern) { 213 if (!dataKey.pattern) {
185 dataKey.pattern = angular.copy(dataKey.label); 214 dataKey.pattern = angular.copy(dataKey.label);
186 } 215 }
187 - var pattern = dataKey.pattern;  
188 - var label = dataKey.pattern;  
189 - var match = varsRegex.exec(pattern);  
190 - while (match !== null) {  
191 - var variable = match[0];  
192 - var variableName = match[1];  
193 - if (variableName === 'dsName') {  
194 - label = label.split(variable).join(datasource.name);  
195 - } else if (variableName === 'entityName') {  
196 - label = label.split(variable).join(datasource.entityName);  
197 - } else if (variableName === 'deviceName') {  
198 - label = label.split(variable).join(datasource.entityName);  
199 - } else if (variableName === 'aliasName') {  
200 - label = label.split(variable).join(datasource.aliasName);  
201 - }  
202 - match = varsRegex.exec(pattern);  
203 - }  
204 - dataKey.label = label; 216 + dataKey.label = aliasCtrl.utils.createLabelFromDatasource(datasource, dataKey.pattern);
205 } 217 }
206 218
207 function updateDatasourceKeyLabels(datasource) { 219 function updateDatasourceKeyLabels(datasource) {
@@ -213,7 +225,7 @@ export default class AliasController { @@ -213,7 +225,7 @@ export default class AliasController {
213 var deferred = this.$q.defer(); 225 var deferred = this.$q.defer();
214 var newDatasources = angular.copy(datasources); 226 var newDatasources = angular.copy(datasources);
215 var datasorceResolveTasks = []; 227 var datasorceResolveTasks = [];
216 - var aliasCtrl = this; 228 +
217 newDatasources.forEach(function (datasource) { 229 newDatasources.forEach(function (datasource) {
218 var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource); 230 var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource);
219 datasorceResolveTasks.push(resolveDatasourceTask); 231 datasorceResolveTasks.push(resolveDatasourceTask);
@@ -64,6 +64,39 @@ export default class Subscription { @@ -64,6 +64,39 @@ export default class Subscription {
64 deferred.resolve(subscription); 64 deferred.resolve(subscription);
65 } 65 }
66 ); 66 );
  67 + } else if (this.type === this.ctx.types.widgetType.alarm.value) {
  68 + this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
  69 + this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
  70 + this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
  71 + this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
  72 + this.alarmSource = options.alarmSource;
  73 + this.alarmSourceListener = null;
  74 + this.alarms = [];
  75 +
  76 + this.originalTimewindow = null;
  77 + this.timeWindow = {
  78 + stDiff: this.ctx.stDiff
  79 + }
  80 + this.useDashboardTimewindow = options.useDashboardTimewindow;
  81 +
  82 + if (this.useDashboardTimewindow) {
  83 + this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
  84 + } else {
  85 + this.timeWindowConfig = angular.copy(options.timeWindowConfig);
  86 + }
  87 +
  88 + this.subscriptionTimewindow = null;
  89 +
  90 + this.loadingData = false;
  91 + this.displayLegend = false;
  92 + this.initAlarmSubscription().then(
  93 + function success() {
  94 + deferred.resolve(subscription);
  95 + },
  96 + function fail() {
  97 + deferred.reject();
  98 + }
  99 + );
67 } else { 100 } else {
68 this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; 101 this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
69 this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; 102 this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
@@ -132,6 +165,44 @@ export default class Subscription { @@ -132,6 +165,44 @@ export default class Subscription {
132 return deferred.promise; 165 return deferred.promise;
133 } 166 }
134 167
  168 + initAlarmSubscription() {
  169 + var deferred = this.ctx.$q.defer();
  170 + if (!this.ctx.aliasController) {
  171 + this.configureAlarmsData();
  172 + deferred.resolve();
  173 + } else {
  174 + var subscription = this;
  175 + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).then(
  176 + function success(alarmSource) {
  177 + subscription.alarmSource = alarmSource;
  178 + subscription.configureAlarmsData();
  179 + deferred.resolve();
  180 + },
  181 + function fail() {
  182 + deferred.reject();
  183 + }
  184 + );
  185 + }
  186 + return deferred.promise;
  187 + }
  188 +
  189 + configureAlarmsData() {
  190 + var subscription = this;
  191 + var registration;
  192 + if (this.useDashboardTimewindow) {
  193 + registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
  194 + if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
  195 + subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
  196 + subscription.unsubscribe();
  197 + subscription.subscribe();
  198 + }
  199 + });
  200 + this.registrations.push(registration);
  201 + } else {
  202 + this.startWatchingTimewindow();
  203 + }
  204 + }
  205 +
135 initDataSubscription() { 206 initDataSubscription() {
136 var deferred = this.ctx.$q.defer(); 207 var deferred = this.ctx.$q.defer();
137 if (!this.ctx.aliasController) { 208 if (!this.ctx.aliasController) {
@@ -393,6 +464,8 @@ export default class Subscription { @@ -393,6 +464,8 @@ export default class Subscription {
393 onAliasesChanged(aliasIds) { 464 onAliasesChanged(aliasIds) {
394 if (this.type === this.ctx.types.widgetType.rpc.value) { 465 if (this.type === this.ctx.types.widgetType.rpc.value) {
395 return this.checkRpcTarget(aliasIds); 466 return this.checkRpcTarget(aliasIds);
  467 + } else if (this.type === this.ctx.types.widgetType.alarm.value) {
  468 + return this.checkAlarmSource(aliasIds);
396 } else { 469 } else {
397 return this.checkSubscriptions(aliasIds); 470 return this.checkSubscriptions(aliasIds);
398 } 471 }
@@ -516,6 +589,15 @@ export default class Subscription { @@ -516,6 +589,15 @@ export default class Subscription {
516 } 589 }
517 } 590 }
518 591
  592 + alarmsUpdated(alarms, apply) {
  593 + this.notifyDataLoaded();
  594 + this.alarms = alarms;
  595 + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
  596 + this.updateTimewindow();
  597 + }
  598 + this.onDataUpdated(apply);
  599 + }
  600 +
519 updateLegend(dataIndex, data, apply) { 601 updateLegend(dataIndex, data, apply) {
520 var dataKey = this.legendData.keys[dataIndex].dataKey; 602 var dataKey = this.legendData.keys[dataIndex].dataKey;
521 var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; 603 var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;
@@ -540,62 +622,104 @@ export default class Subscription { @@ -540,62 +622,104 @@ export default class Subscription {
540 if (this.type === this.ctx.types.widgetType.rpc.value) { 622 if (this.type === this.ctx.types.widgetType.rpc.value) {
541 return; 623 return;
542 } 624 }
  625 + if (this.type === this.ctx.types.widgetType.alarm.value) {
  626 + this.alarmsSubscribe();
  627 + } else {
  628 + this.notifyDataLoading();
  629 + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
  630 + this.updateRealtimeSubscription();
  631 + if (this.subscriptionTimewindow.fixedWindow) {
  632 + this.onDataUpdated();
  633 + }
  634 + }
  635 + var index = 0;
  636 + for (var i = 0; i < this.datasources.length; i++) {
  637 + var datasource = this.datasources[i];
  638 + if (angular.isFunction(datasource))
  639 + continue;
  640 +
  641 + var subscription = this;
  642 +
  643 + var listener = {
  644 + subscriptionType: this.type,
  645 + subscriptionTimewindow: this.subscriptionTimewindow,
  646 + datasource: datasource,
  647 + entityType: datasource.entityType,
  648 + entityId: datasource.entityId,
  649 + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
  650 + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
  651 + },
  652 + updateRealtimeSubscription: function () {
  653 + this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
  654 + return this.subscriptionTimewindow;
  655 + },
  656 + setRealtimeSubscription: function (subscriptionTimewindow) {
  657 + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
  658 + },
  659 + datasourceIndex: index
  660 + };
  661 +
  662 + for (var a = 0; a < datasource.dataKeys.length; a++) {
  663 + this.data[index + a].data = [];
  664 + }
  665 +
  666 + index += datasource.dataKeys.length;
  667 +
  668 + this.datasourceListeners.push(listener);
  669 + this.ctx.datasourceService.subscribeToDatasource(listener);
  670 + if (datasource.unresolvedStateEntity) {
  671 + this.notifyDataLoaded();
  672 + this.onDataUpdated();
  673 + }
  674 +
  675 + }
  676 + }
  677 + }
  678 +
  679 + alarmsSubscribe() {
543 this.notifyDataLoading(); 680 this.notifyDataLoading();
544 - if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { 681 + if (this.timeWindowConfig) {
545 this.updateRealtimeSubscription(); 682 this.updateRealtimeSubscription();
546 if (this.subscriptionTimewindow.fixedWindow) { 683 if (this.subscriptionTimewindow.fixedWindow) {
547 this.onDataUpdated(); 684 this.onDataUpdated();
548 } 685 }
549 } 686 }
550 - var index = 0;  
551 - for (var i = 0; i < this.datasources.length; i++) {  
552 - var datasource = this.datasources[i];  
553 - if (angular.isFunction(datasource))  
554 - continue;  
555 -  
556 - var subscription = this;  
557 -  
558 - var listener = {  
559 - subscriptionType: this.type,  
560 - subscriptionTimewindow: this.subscriptionTimewindow,  
561 - datasource: datasource,  
562 - entityType: datasource.entityType,  
563 - entityId: datasource.entityId,  
564 - dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {  
565 - subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);  
566 - },  
567 - updateRealtimeSubscription: function () {  
568 - this.subscriptionTimewindow = subscription.updateRealtimeSubscription();  
569 - return this.subscriptionTimewindow;  
570 - },  
571 - setRealtimeSubscription: function (subscriptionTimewindow) {  
572 - subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));  
573 - },  
574 - datasourceIndex: index  
575 - };  
576 -  
577 - for (var a = 0; a < datasource.dataKeys.length; a++) {  
578 - this.data[index + a].data = []; 687 + var subscription = this;
  688 + this.alarmSourceListener = {
  689 + subscriptionTimewindow: this.subscriptionTimewindow,
  690 + alarmSource: this.alarmSource,
  691 + alarmsUpdated: function(alarms, apply) {
  692 + subscription.alarmsUpdated(alarms, apply);
579 } 693 }
  694 + }
  695 + this.alarms = [];
580 696
581 - index += datasource.dataKeys.length; 697 + this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener);
582 698
583 - this.datasourceListeners.push(listener);  
584 - this.ctx.datasourceService.subscribeToDatasource(listener);  
585 - if (datasource.unresolvedStateEntity) {  
586 - this.notifyDataLoaded();  
587 - this.onDataUpdated();  
588 - } 699 + if (this.alarmSource.unresolvedStateEntity) {
  700 + this.notifyDataLoaded();
  701 + this.onDataUpdated();
589 } 702 }
590 } 703 }
591 704
592 unsubscribe() { 705 unsubscribe() {
593 if (this.type !== this.ctx.types.widgetType.rpc.value) { 706 if (this.type !== this.ctx.types.widgetType.rpc.value) {
594 - for (var i = 0; i < this.datasourceListeners.length; i++) {  
595 - var listener = this.datasourceListeners[i];  
596 - this.ctx.datasourceService.unsubscribeFromDatasource(listener); 707 + if (this.type == this.ctx.types.widgetType.alarm.value) {
  708 + this.alarmsUnsubscribe();
  709 + } else {
  710 + for (var i = 0; i < this.datasourceListeners.length; i++) {
  711 + var listener = this.datasourceListeners[i];
  712 + this.ctx.datasourceService.unsubscribeFromDatasource(listener);
  713 + }
  714 + this.datasourceListeners = [];
597 } 715 }
598 - this.datasourceListeners = []; 716 + }
  717 + }
  718 +
  719 + alarmsUnsubscribe() {
  720 + if (this.alarmSourceListener) {
  721 + this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener);
  722 + this.alarmSourceListener = null;
599 } 723 }
600 } 724 }
601 725
@@ -607,6 +731,14 @@ export default class Subscription { @@ -607,6 +731,14 @@ export default class Subscription {
607 } 731 }
608 } 732 }
609 733
  734 + checkAlarmSource(aliasIds) {
  735 + if (this.alarmSource && this.alarmSource.entityAliasId) {
  736 + return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1;
  737 + } else {
  738 + return false;
  739 + }
  740 + }
  741 +
610 checkSubscriptions(aliasIds) { 742 checkSubscriptions(aliasIds) {
611 var subscriptionsChanged = false; 743 var subscriptionsChanged = false;
612 for (var i = 0; i < this.datasourceListeners.length; i++) { 744 for (var i = 0; i < this.datasourceListeners.length; i++) {
@@ -19,6 +19,7 @@ import tinycolor from 'tinycolor2'; @@ -19,6 +19,7 @@ import tinycolor from 'tinycolor2';
19 19
20 import thingsboardLedLight from '../components/led-light.directive'; 20 import thingsboardLedLight from '../components/led-light.directive';
21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; 21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
  22 +import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
22 23
23 import TbFlot from '../widget/lib/flot-widget'; 24 import TbFlot from '../widget/lib/flot-widget';
24 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; 25 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@@ -33,7 +34,7 @@ import thingsboardTypes from '../common/types.constant'; @@ -33,7 +34,7 @@ import thingsboardTypes from '../common/types.constant';
33 import thingsboardUtils from '../common/utils.service'; 34 import thingsboardUtils from '../common/utils.service';
34 35
35 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, 36 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
36 - thingsboardTypes, thingsboardUtils]) 37 + thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils])
37 .factory('widgetService', WidgetService) 38 .factory('widgetService', WidgetService)
38 .name; 39 .name;
39 40
@@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', []) @@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', [])
59 name: "aggregation.none" 59 name: "aggregation.none"
60 } 60 }
61 }, 61 },
  62 + alarmFields: {
  63 + createdTime: {
  64 + value: "createdTime",
  65 + name: "alarm.created-time",
  66 + time: true
  67 + },
  68 + startTime: {
  69 + value: "startTs",
  70 + name: "alarm.start-time",
  71 + time: true
  72 + },
  73 + endTime: {
  74 + value: "endTs",
  75 + name: "alarm.end-time",
  76 + time: true
  77 + },
  78 + ackTime: {
  79 + value: "ackTs",
  80 + name: "alarm.ack-time",
  81 + time: true
  82 + },
  83 + clearTime: {
  84 + value: "clearTs",
  85 + name: "alarm.clear-time",
  86 + time: true
  87 + },
  88 + originator: {
  89 + value: "originatorName",
  90 + name: "alarm.originator"
  91 + },
  92 + originatorType: {
  93 + value: "originator.entityType",
  94 + name: "alarm.originator-type"
  95 + },
  96 + type: {
  97 + value: "type",
  98 + name: "alarm.type"
  99 + },
  100 + severity: {
  101 + value: "severity",
  102 + name: "alarm.severity"
  103 + },
  104 + status: {
  105 + value: "status",
  106 + name: "alarm.status"
  107 + }
  108 + },
62 alarmStatus: { 109 alarmStatus: {
63 activeUnack: "ACTIVE_UNACK", 110 activeUnack: "ACTIVE_UNACK",
64 activeAck: "ACTIVE_ACK", 111 activeAck: "ACTIVE_ACK",
@@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', []) @@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', [])
75 alarmSeverity: { 122 alarmSeverity: {
76 "CRITICAL": { 123 "CRITICAL": {
77 name: "alarm.severity-critical", 124 name: "alarm.severity-critical",
78 - class: "tb-critical" 125 + class: "tb-critical",
  126 + color: "red"
79 }, 127 },
80 "MAJOR": { 128 "MAJOR": {
81 name: "alarm.severity-major", 129 name: "alarm.severity-major",
82 - class: "tb-major" 130 + class: "tb-major",
  131 + color: "orange"
83 }, 132 },
84 "MINOR": { 133 "MINOR": {
85 name: "alarm.severity-minor", 134 name: "alarm.severity-minor",
86 - class: "tb-minor" 135 + class: "tb-minor",
  136 + color: "#ffca3d"
87 }, 137 },
88 "WARNING": { 138 "WARNING": {
89 name: "alarm.severity-warning", 139 name: "alarm.severity-warning",
90 - class: "tb-warning" 140 + class: "tb-warning",
  141 + color: "#abab00"
91 }, 142 },
92 "INDETERMINATE": { 143 "INDETERMINATE": {
93 name: "alarm.severity-indeterminate", 144 name: "alarm.severity-indeterminate",
94 - class: "tb-indeterminate" 145 + class: "tb-indeterminate",
  146 + color: "green"
95 } 147 }
96 }, 148 },
97 aliasFilterType: { 149 aliasFilterType: {
@@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', []) @@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', [])
153 dataKeyType: { 205 dataKeyType: {
154 timeseries: "timeseries", 206 timeseries: "timeseries",
155 attribute: "attribute", 207 attribute: "attribute",
156 - function: "function" 208 + function: "function",
  209 + alarm: "alarm"
157 }, 210 },
158 componentType: { 211 componentType: {
159 filter: "FILTER", 212 filter: "FILTER",
@@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', []) @@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', [])
319 alias: "basic_gpio_control" 372 alias: "basic_gpio_control"
320 } 373 }
321 }, 374 },
  375 + alarm: {
  376 + value: "alarm",
  377 + name: "widget.alarm",
  378 + template: {
  379 + bundleAlias: "alarm_widgets",
  380 + alias: "alarms_table"
  381 + }
  382 + },
322 static: { 383 static: {
323 value: "static", 384 value: "static",
324 name: "widget.static", 385 name: "widget.static",
@@ -21,8 +21,10 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) @@ -21,8 +21,10 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
21 .factory('utils', Utils) 21 .factory('utils', Utils)
22 .name; 22 .name;
23 23
  24 +const varsRegex = /\$\{([^\}]*)\}/g;
  25 +
24 /*@ngInject*/ 26 /*@ngInject*/
25 -function Utils($mdColorPalette, $rootScope, $window, types) { 27 +function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
26 28
27 var predefinedFunctions = {}, 29 var predefinedFunctions = {},
28 predefinedFunctionsList = [], 30 predefinedFunctionsList = [],
@@ -93,9 +95,32 @@ function Utils($mdColorPalette, $rootScope, $window, types) { @@ -93,9 +95,32 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
93 dataKeys: [angular.copy(defaultDataKey)] 95 dataKeys: [angular.copy(defaultDataKey)]
94 }; 96 };
95 97
  98 + var defaultAlarmFields = [
  99 + 'createdTime',
  100 + 'originator',
  101 + 'type',
  102 + 'severity',
  103 + 'status'
  104 + ];
  105 +
  106 + var defaultAlarmDataKeys = [];
  107 + for (var i=0;i<defaultAlarmFields.length;i++) {
  108 + var name = defaultAlarmFields[i];
  109 + var dataKey = {
  110 + name: name,
  111 + type: types.dataKeyType.alarm,
  112 + label: $translate.instant(types.alarmFields[name].name)+'',
  113 + color: getMaterialColor(i),
  114 + settings: {},
  115 + _hash: Math.random()
  116 + };
  117 + defaultAlarmDataKeys.push(dataKey);
  118 + }
  119 +
96 var service = { 120 var service = {
97 getDefaultDatasource: getDefaultDatasource, 121 getDefaultDatasource: getDefaultDatasource,
98 getDefaultDatasourceJson: getDefaultDatasourceJson, 122 getDefaultDatasourceJson: getDefaultDatasourceJson,
  123 + getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
99 getMaterialColor: getMaterialColor, 124 getMaterialColor: getMaterialColor,
100 getPredefinedFunctionBody: getPredefinedFunctionBody, 125 getPredefinedFunctionBody: getPredefinedFunctionBody,
101 getPredefinedFunctionsList: getPredefinedFunctionsList, 126 getPredefinedFunctionsList: getPredefinedFunctionsList,
@@ -109,7 +134,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) { @@ -109,7 +134,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
109 cleanCopy: cleanCopy, 134 cleanCopy: cleanCopy,
110 isLocalUrl: isLocalUrl, 135 isLocalUrl: isLocalUrl,
111 validateDatasources: validateDatasources, 136 validateDatasources: validateDatasources,
112 - createKey: createKey 137 + createKey: createKey,
  138 + createLabelFromDatasource: createLabelFromDatasource
113 } 139 }
114 140
115 return service; 141 return service;
@@ -212,6 +238,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) { @@ -212,6 +238,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
212 return angular.toJson(getDefaultDatasource(dataKeySchema)); 238 return angular.toJson(getDefaultDatasource(dataKeySchema));
213 } 239 }
214 240
  241 + function getDefaultAlarmDataKeys() {
  242 + return angular.copy(defaultAlarmDataKeys);
  243 + }
  244 +
215 function isDescriptorSchemaNotEmpty(descriptor) { 245 function isDescriptorSchemaNotEmpty(descriptor) {
216 if (descriptor && descriptor.schema && descriptor.schema.properties) { 246 if (descriptor && descriptor.schema && descriptor.schema.properties) {
217 for(var prop in descriptor.schema.properties) { 247 for(var prop in descriptor.schema.properties) {
@@ -357,4 +387,24 @@ function Utils($mdColorPalette, $rootScope, $window, types) { @@ -357,4 +387,24 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
357 return dataKey; 387 return dataKey;
358 } 388 }
359 389
  390 + function createLabelFromDatasource(datasource, pattern) {
  391 + var label = angular.copy(pattern);
  392 + var match = varsRegex.exec(pattern);
  393 + while (match !== null) {
  394 + var variable = match[0];
  395 + var variableName = match[1];
  396 + if (variableName === 'dsName') {
  397 + label = label.split(variable).join(datasource.name);
  398 + } else if (variableName === 'entityName') {
  399 + label = label.split(variable).join(datasource.entityName);
  400 + } else if (variableName === 'deviceName') {
  401 + label = label.split(variable).join(datasource.entityName);
  402 + } else if (variableName === 'aliasName') {
  403 + label = label.split(variable).join(datasource.aliasName);
  404 + }
  405 + match = varsRegex.exec(pattern);
  406 + }
  407 + return label;
  408 + }
  409 +
360 } 410 }
@@ -181,6 +181,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -181,6 +181,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
181 vm.dropWidgetShadow = dropWidgetShadow; 181 vm.dropWidgetShadow = dropWidgetShadow;
182 vm.enableWidgetFullscreen = enableWidgetFullscreen; 182 vm.enableWidgetFullscreen = enableWidgetFullscreen;
183 vm.hasTimewindow = hasTimewindow; 183 vm.hasTimewindow = hasTimewindow;
  184 + vm.hasAggregation = hasAggregation;
184 vm.editWidget = editWidget; 185 vm.editWidget = editWidget;
185 vm.exportWidget = exportWidget; 186 vm.exportWidget = exportWidget;
186 vm.removeWidget = removeWidget; 187 vm.removeWidget = removeWidget;
@@ -771,7 +772,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -771,7 +772,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
771 } 772 }
772 773
773 function hasTimewindow(widget) { 774 function hasTimewindow(widget) {
774 - if (widget.type === types.widgetType.timeseries.value) { 775 + if (widget.type === types.widgetType.timeseries.value || widget.type === types.widgetType.alarm.value) {
775 return angular.isDefined(widget.config.useDashboardTimewindow) ? 776 return angular.isDefined(widget.config.useDashboardTimewindow) ?
776 !widget.config.useDashboardTimewindow : false; 777 !widget.config.useDashboardTimewindow : false;
777 } else { 778 } else {
@@ -779,6 +780,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -779,6 +780,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
779 } 780 }
780 } 781 }
781 782
  783 + function hasAggregation(widget) {
  784 + return widget.type === types.widgetType.timeseries.value;
  785 + }
  786 +
782 function adoptMaxRows() { 787 function adoptMaxRows() {
783 if (vm.widgets) { 788 if (vm.widgets) {
784 var maxRows = vm.gridsterOpts.maxRows; 789 var maxRows = vm.gridsterOpts.maxRows;
@@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
47 padding: vm.widgetPadding(widget)}"> 47 padding: vm.widgetPadding(widget)}">
48 <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> 48 <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
49 <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span> 49 <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
50 - <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> 50 + <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
51 </div> 51 </div>
52 <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()"> 52 <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()">
53 <md-button id="expand-button" 53 <md-button id="expand-button"
@@ -45,12 +45,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -45,12 +45,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
45 scope.ngModelCtrl = ngModelCtrl; 45 scope.ngModelCtrl = ngModelCtrl;
46 scope.types = types; 46 scope.types = types;
47 47
  48 + scope.alarmFields = [];
  49 + for (var alarmField in types.alarmFields) {
  50 + scope.alarmFields.push(alarmField);
  51 + }
  52 +
48 scope.selectedTimeseriesDataKey = null; 53 scope.selectedTimeseriesDataKey = null;
49 scope.timeseriesDataKeySearchText = null; 54 scope.timeseriesDataKeySearchText = null;
50 55
51 scope.selectedAttributeDataKey = null; 56 scope.selectedAttributeDataKey = null;
52 scope.attributeDataKeySearchText = null; 57 scope.attributeDataKeySearchText = null;
53 58
  59 + scope.selectedAlarmDataKey = null;
  60 + scope.alarmDataKeySearchText = null;
  61 +
54 scope.updateValidity = function () { 62 scope.updateValidity = function () {
55 if (ngModelCtrl.$viewValue) { 63 if (ngModelCtrl.$viewValue) {
56 var value = ngModelCtrl.$viewValue; 64 var value = ngModelCtrl.$viewValue;
@@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
81 }); 89 });
82 90
83 scope.$watch('timeseriesDataKeys', function () { 91 scope.$watch('timeseriesDataKeys', function () {
84 - if (ngModelCtrl.$viewValue) {  
85 - var dataKeys = [];  
86 - dataKeys = dataKeys.concat(scope.timeseriesDataKeys);  
87 - dataKeys = dataKeys.concat(scope.attributeDataKeys);  
88 - ngModelCtrl.$viewValue.dataKeys = dataKeys;  
89 - scope.updateValidity();  
90 - } 92 + updateDataKeys();
91 }, true); 93 }, true);
92 94
93 scope.$watch('attributeDataKeys', function () { 95 scope.$watch('attributeDataKeys', function () {
  96 + updateDataKeys();
  97 + }, true);
  98 +
  99 + scope.$watch('alarmDataKeys', function () {
  100 + updateDataKeys();
  101 + }, true);
  102 +
  103 + function updateDataKeys() {
94 if (ngModelCtrl.$viewValue) { 104 if (ngModelCtrl.$viewValue) {
95 var dataKeys = []; 105 var dataKeys = [];
96 dataKeys = dataKeys.concat(scope.timeseriesDataKeys); 106 dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
97 dataKeys = dataKeys.concat(scope.attributeDataKeys); 107 dataKeys = dataKeys.concat(scope.attributeDataKeys);
  108 + dataKeys = dataKeys.concat(scope.alarmDataKeys);
98 ngModelCtrl.$viewValue.dataKeys = dataKeys; 109 ngModelCtrl.$viewValue.dataKeys = dataKeys;
99 scope.updateValidity(); 110 scope.updateValidity();
100 } 111 }
101 - }, true); 112 + }
102 113
103 ngModelCtrl.$render = function () { 114 ngModelCtrl.$render = function () {
104 if (ngModelCtrl.$viewValue) { 115 if (ngModelCtrl.$viewValue) {
@@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
111 } 122 }
112 var timeseriesDataKeys = []; 123 var timeseriesDataKeys = [];
113 var attributeDataKeys = []; 124 var attributeDataKeys = [];
  125 + var alarmDataKeys = [];
114 for (var d in ngModelCtrl.$viewValue.dataKeys) { 126 for (var d in ngModelCtrl.$viewValue.dataKeys) {
115 var dataKey = ngModelCtrl.$viewValue.dataKeys[d]; 127 var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
116 if (dataKey.type === types.dataKeyType.timeseries) { 128 if (dataKey.type === types.dataKeyType.timeseries) {
117 timeseriesDataKeys.push(dataKey); 129 timeseriesDataKeys.push(dataKey);
118 } else if (dataKey.type === types.dataKeyType.attribute) { 130 } else if (dataKey.type === types.dataKeyType.attribute) {
119 attributeDataKeys.push(dataKey); 131 attributeDataKeys.push(dataKey);
  132 + } else if (dataKey.type === types.dataKeyType.alarm) {
  133 + alarmDataKeys.push(dataKey);
120 } 134 }
121 } 135 }
122 scope.timeseriesDataKeys = timeseriesDataKeys; 136 scope.timeseriesDataKeys = timeseriesDataKeys;
123 scope.attributeDataKeys = attributeDataKeys; 137 scope.attributeDataKeys = attributeDataKeys;
  138 + scope.alarmDataKeys = alarmDataKeys;
124 } 139 }
125 }; 140 };
126 141
@@ -135,6 +150,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -135,6 +150,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
135 if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') { 150 if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
136 scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : ''; 151 scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
137 } 152 }
  153 + if (!scope.alarmDataKeySearchText || scope.alarmDataKeySearchText === '') {
  154 + scope.alarmDataKeySearchText = scope.alarmDataKeySearchText === '' ? null : '';
  155 + }
138 }; 156 };
139 157
140 scope.transformTimeseriesDataKeyChip = function (chip) { 158 scope.transformTimeseriesDataKeyChip = function (chip) {
@@ -145,6 +163,10 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -145,6 +163,10 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
145 return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute}); 163 return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
146 }; 164 };
147 165
  166 + scope.transformAlarmDataKeyChip = function (chip) {
  167 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm});
  168 + };
  169 +
148 scope.showColorPicker = function (event, dataKey) { 170 scope.showColorPicker = function (event, dataKey) {
149 $mdColorPicker.show({ 171 $mdColorPicker.show({
150 value: dataKey.color, 172 value: dataKey.color,
@@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
196 scope.timeseriesDataKeys[index] = dataKey; 218 scope.timeseriesDataKeys[index] = dataKey;
197 } else if (dataKey.type === types.dataKeyType.attribute) { 219 } else if (dataKey.type === types.dataKeyType.attribute) {
198 scope.attributeDataKeys[index] = dataKey; 220 scope.attributeDataKeys[index] = dataKey;
  221 + } else if (dataKey.type === types.dataKeyType.alarm) {
  222 + scope.alarmDataKeys[index] = dataKey;
199 } 223 }
200 ngModelCtrl.$setDirty(); 224 ngModelCtrl.$setDirty();
201 }, function () { 225 }, function () {
@@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc @@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
203 }; 227 };
204 228
205 scope.dataKeysSearch = function (searchText, type) { 229 scope.dataKeysSearch = function (searchText, type) {
206 - if (scope.entityAlias) {  
207 - var deferred = $q.defer();  
208 - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})  
209 - .then(function (dataKeys) {  
210 - deferred.resolve(dataKeys);  
211 - }, function (e) {  
212 - deferred.reject(e);  
213 - });  
214 - return deferred.promise; 230 + if (scope.widgetType == types.widgetType.alarm.value) {
  231 + var dataKeys = searchText ? scope.alarmFields.filter(
  232 + scope.createFilterForDataKey(searchText)) : scope.alarmFields;
  233 + return dataKeys;
215 } else { 234 } else {
216 - return $q.when([]); 235 + if (scope.entityAlias) {
  236 + var deferred = $q.defer();
  237 + scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})
  238 + .then(function (dataKeys) {
  239 + deferred.resolve(dataKeys);
  240 + }, function (e) {
  241 + deferred.reject(e);
  242 + });
  243 + return deferred.promise;
  244 + } else {
  245 + return $q.when([]);
  246 + }
217 } 247 }
218 }; 248 };
219 249
  250 + scope.createFilterForDataKey = function (query) {
  251 + var lowercaseQuery = angular.lowercase(query);
  252 + return function filterFn(dataKey) {
  253 + return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0);
  254 + };
  255 + };
  256 +
220 scope.createKey = function (event, chipsId) { 257 scope.createKey = function (event, chipsId) {
221 var chipsChild = $(chipsId, element)[0].firstElementChild; 258 var chipsChild = $(chipsId, element)[0].firstElementChild;
222 var el = angular.element(chipsChild); 259 var el = angular.element(chipsChild);
@@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
24 </tb-entity-alias-select> 24 </tb-entity-alias-select>
25 <section flex layout='column'> 25 <section flex layout='column'>
26 <section flex layout='column' layout-align="center" style="padding-left: 4px;"> 26 <section flex layout='column' layout-align="center" style="padding-left: 4px;">
27 - <md-chips flex 27 + <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
28 id="timeseries_datakey_chips" 28 id="timeseries_datakey_chips"
29 ng-required="true" 29 ng-required="true"
30 ng-model="timeseriesDataKeys" md-autocomplete-snap 30 ng-model="timeseriesDataKeys" md-autocomplete-snap
@@ -128,10 +128,60 @@ @@ -128,10 +128,60 @@
128 </div> 128 </div>
129 </md-chip-template> 129 </md-chip-template>
130 </md-chips> 130 </md-chips>
  131 + <md-chips flex ng-if="widgetType == types.widgetType.alarm.value"
  132 + id="alarm_datakey_chips"
  133 + ng-required="true"
  134 + ng-model="alarmDataKeys" md-autocomplete-snap
  135 + md-transform-chip="transformAlarmDataKeyChip($chip)"
  136 + md-require-match="true">
  137 + <md-autocomplete
  138 + md-no-cache="true"
  139 + id="alarm_datakey"
  140 + md-selected-item="selectedAlarmDataKey"
  141 + md-search-text="alarmDataKeySearchText"
  142 + md-items="item in dataKeysSearch(alarmDataKeySearchText, types.dataKeyType.alarm)"
  143 + md-item-text="item.name"
  144 + md-min-length="0"
  145 + placeholder="{{'datakey.alarm' | translate }}"
  146 + md-menu-class="tb-alarm-datakey-autocomplete">
  147 + <span md-highlight-text="alarmDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  148 + <md-not-found>
  149 + <div class="tb-not-found">
  150 + <div class="tb-no-entries" ng-if="!textIsNotEmpty(alarmDataKeySearchText)">
  151 + <span translate>entity.no-keys-found</span>
  152 + </div>
  153 + <div ng-if="textIsNotEmpty(alarmDataKeySearchText)">
  154 + <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
  155 + </div>
  156 + </div>
  157 + </md-not-found>
  158 + </md-autocomplete>
  159 + <md-chip-template>
  160 + <div layout="row" layout-align="start center" class="tb-attribute-chip">
  161 + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
  162 + <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
  163 + </div>
  164 + <div layout="row" flex>
  165 + <div class="tb-chip-label">
  166 + {{$chip.label}}
  167 + </div>
  168 + <div class="tb-chip-separator">: </div>
  169 + <div class="tb-chip-label">
  170 + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
  171 + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
  172 + </div>
  173 + </div>
  174 + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
  175 + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
  176 + </md-button>
  177 + </div>
  178 + </md-chip-template>
  179 + </md-chips>
131 </section> 180 </section>
132 <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> 181 <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
133 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div> 182 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
134 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div> 183 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
  184 + <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
135 </div> 185 </div>
136 </section> 186 </section>
137 </section> 187 </section>
@@ -43,18 +43,27 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -43,18 +43,27 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
43 element.html(template); 43 element.html(template);
44 44
45 scope.ngModelCtrl = ngModelCtrl; 45 scope.ngModelCtrl = ngModelCtrl;
  46 + scope.types = types;
  47 +
46 scope.functionTypes = utils.getPredefinedFunctionsList(); 48 scope.functionTypes = utils.getPredefinedFunctionsList();
  49 + scope.alarmFields = [];
  50 + for (var alarmField in types.alarmFields) {
  51 + scope.alarmFields.push(alarmField);
  52 + }
47 53
48 scope.selectedDataKey = null; 54 scope.selectedDataKey = null;
49 scope.dataKeySearchText = null; 55 scope.dataKeySearchText = null;
50 56
  57 + scope.selectedAlarmDataKey = null;
  58 + scope.alarmDataKeySearchText = null;
  59 +
51 scope.updateValidity = function () { 60 scope.updateValidity = function () {
52 if (ngModelCtrl.$viewValue) { 61 if (ngModelCtrl.$viewValue) {
53 var value = ngModelCtrl.$viewValue; 62 var value = ngModelCtrl.$viewValue;
54 var dataValid = angular.isDefined(value) && value != null; 63 var dataValid = angular.isDefined(value) && value != null;
55 ngModelCtrl.$setValidity('deviceData', dataValid); 64 ngModelCtrl.$setValidity('deviceData', dataValid);
56 if (dataValid) { 65 if (dataValid) {
57 - ngModelCtrl.$setValidity('funcTypes', 66 + ngModelCtrl.$setValidity('datasourceKeys',
58 angular.isDefined(value.dataKeys) && 67 angular.isDefined(value.dataKeys) &&
59 value.dataKeys != null && 68 value.dataKeys != null &&
60 value.dataKeys.length > 0); 69 value.dataKeys.length > 0);
@@ -63,13 +72,22 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -63,13 +72,22 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
63 }; 72 };
64 73
65 scope.$watch('funcDataKeys', function () { 74 scope.$watch('funcDataKeys', function () {
  75 + updateDataKeys();
  76 + }, true);
  77 +
  78 + scope.$watch('alarmDataKeys', function () {
  79 + updateDataKeys();
  80 + }, true);
  81 +
  82 + function updateDataKeys() {
66 if (ngModelCtrl.$viewValue) { 83 if (ngModelCtrl.$viewValue) {
67 var dataKeys = []; 84 var dataKeys = [];
68 dataKeys = dataKeys.concat(scope.funcDataKeys); 85 dataKeys = dataKeys.concat(scope.funcDataKeys);
  86 + dataKeys = dataKeys.concat(scope.alarmDataKeys);
69 ngModelCtrl.$viewValue.dataKeys = dataKeys; 87 ngModelCtrl.$viewValue.dataKeys = dataKeys;
70 scope.updateValidity(); 88 scope.updateValidity();
71 } 89 }
72 - }, true); 90 + }
73 91
74 scope.$watch('datasourceName', function () { 92 scope.$watch('datasourceName', function () {
75 if (ngModelCtrl.$viewValue) { 93 if (ngModelCtrl.$viewValue) {
@@ -81,18 +99,31 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -81,18 +99,31 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
81 ngModelCtrl.$render = function () { 99 ngModelCtrl.$render = function () {
82 if (ngModelCtrl.$viewValue) { 100 if (ngModelCtrl.$viewValue) {
83 var funcDataKeys = []; 101 var funcDataKeys = [];
  102 + var alarmDataKeys = [];
84 if (ngModelCtrl.$viewValue.dataKeys) { 103 if (ngModelCtrl.$viewValue.dataKeys) {
85 - funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys); 104 + for (var d=0;d<ngModelCtrl.$viewValue.dataKeys.length;d++) {
  105 + var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
  106 + if (dataKey.type === types.dataKeyType.function) {
  107 + funcDataKeys.push(dataKey);
  108 + } else if (dataKey.type === types.dataKeyType.alarm) {
  109 + alarmDataKeys.push(dataKey);
  110 + }
  111 + }
86 } 112 }
87 scope.funcDataKeys = funcDataKeys; 113 scope.funcDataKeys = funcDataKeys;
  114 + scope.alarmDataKeys = alarmDataKeys;
88 scope.datasourceName = ngModelCtrl.$viewValue.name; 115 scope.datasourceName = ngModelCtrl.$viewValue.name;
89 } 116 }
90 }; 117 };
91 118
92 - scope.transformDataKeyChip = function (chip) { 119 + scope.transformFuncDataKeyChip = function (chip) {
93 return scope.generateDataKey({chip: chip, type: types.dataKeyType.function}); 120 return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
94 }; 121 };
95 122
  123 + scope.transformAlarmDataKeyChip = function (chip) {
  124 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm});
  125 + };
  126 +
96 scope.showColorPicker = function (event, dataKey) { 127 scope.showColorPicker = function (event, dataKey) {
97 $mdColorPicker.show({ 128 $mdColorPicker.show({
98 value: dataKey.color, 129 value: dataKey.color,
@@ -129,7 +160,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -129,7 +160,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
129 dataKey: angular.copy(dataKey), 160 dataKey: angular.copy(dataKey),
130 dataKeySettingsSchema: scope.datakeySettingsSchema, 161 dataKeySettingsSchema: scope.datakeySettingsSchema,
131 entityAlias: null, 162 entityAlias: null,
132 - entityAliases: null 163 + aliasController: null
133 }, 164 },
134 parent: angular.element($document[0].body), 165 parent: angular.element($document[0].body),
135 fullscreen: true, 166 fullscreen: true,
@@ -140,7 +171,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -140,7 +171,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
140 w.triggerHandler('resize'); 171 w.triggerHandler('resize');
141 } 172 }
142 }).then(function (dataKey) { 173 }).then(function (dataKey) {
143 - scope.funcDataKeys[index] = dataKey; 174 + if (dataKey.type === types.dataKeyType.function) {
  175 + scope.funcDataKeys[index] = dataKey;
  176 + } else if (dataKey.type === types.dataKeyType.alarm) {
  177 + scope.alarmDataKeys[index] = dataKey;
  178 + }
144 ngModelCtrl.$setDirty(); 179 ngModelCtrl.$setDirty();
145 }, function () { 180 }, function () {
146 }); 181 });
@@ -151,8 +186,9 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -151,8 +186,9 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
151 } 186 }
152 187
153 scope.dataKeysSearch = function (dataKeySearchText) { 188 scope.dataKeysSearch = function (dataKeySearchText) {
154 - var dataKeys = dataKeySearchText ? scope.functionTypes.filter(  
155 - scope.createFilterForDataKey(dataKeySearchText)) : scope.functionTypes; 189 + var targetKeys = scope.widgetType == types.widgetType.alarm.value ? scope.alarmFields : scope.functionTypes;
  190 + var dataKeys = dataKeySearchText ? targetKeys.filter(
  191 + scope.createFilterForDataKey(dataKeySearchText)) : targetKeys;
156 return dataKeys; 192 return dataKeys;
157 }; 193 };
158 194
@@ -180,6 +216,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -180,6 +216,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
180 restrict: "E", 216 restrict: "E",
181 require: "^ngModel", 217 require: "^ngModel",
182 scope: { 218 scope: {
  219 + widgetType: '=',
183 generateDataKey: '&', 220 generateDataKey: '&',
184 datakeySettingsSchema: '=' 221 datakeySettingsSchema: '='
185 }, 222 },
@@ -17,18 +17,19 @@ @@ -17,18 +17,19 @@
17 --> 17 -->
18 <section class="tb-datasource-func" flex layout='column' 18 <section class="tb-datasource-func" flex layout='column'
19 layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center"> 19 layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
20 - <md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;"> 20 + <md-input-container ng-if="widgetType != types.widgetType.alarm.value"
  21 + class="tb-datasource-name" md-no-float style="min-width: 200px;">
21 <input name="datasourceName" 22 <input name="datasourceName"
22 placeholder="{{ 'datasource.name' | translate }}" 23 placeholder="{{ 'datasource.name' | translate }}"
23 ng-model="datasourceName" 24 ng-model="datasourceName"
24 aria-label="{{ 'datasource.name' | translate }}"> 25 aria-label="{{ 'datasource.name' | translate }}">
25 </md-input-container> 26 </md-input-container>
26 <section flex layout='column' style="padding-left: 4px;"> 27 <section flex layout='column' style="padding-left: 4px;">
27 - <md-chips flex 28 + <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
28 id="function_datakey_chips" 29 id="function_datakey_chips"
29 ng-required="true" 30 ng-required="true"
30 ng-model="funcDataKeys" md-autocomplete-snap 31 ng-model="funcDataKeys" md-autocomplete-snap
31 - md-transform-chip="transformDataKeyChip($chip)" 32 + md-transform-chip="transformFuncDataKeyChip($chip)"
32 md-require-match="false"> 33 md-require-match="false">
33 <md-autocomplete 34 <md-autocomplete
34 md-no-cache="false" 35 md-no-cache="false"
@@ -75,8 +76,58 @@ @@ -75,8 +76,58 @@
75 </div> 76 </div>
76 </md-chip-template> 77 </md-chip-template>
77 </md-chips> 78 </md-chips>
  79 + <md-chips flex ng-if="widgetType == types.widgetType.alarm.value"
  80 + id="alarm_datakey_chips"
  81 + ng-required="true"
  82 + ng-model="alarmDataKeys" md-autocomplete-snap
  83 + md-transform-chip="transformAlarmDataKeyChip($chip)"
  84 + md-require-match="true">
  85 + <md-autocomplete
  86 + md-no-cache="true"
  87 + id="alarm_datakey"
  88 + md-selected-item="selectedAlarmDataKey"
  89 + md-search-text="alarmDataKeySearchText"
  90 + md-items="item in dataKeysSearch(alarmDataKeySearchText, types.dataKeyType.alarm)"
  91 + md-item-text="item.name"
  92 + md-min-length="0"
  93 + placeholder="{{'datakey.alarm' | translate }}"
  94 + md-menu-class="tb-alarm-datakey-autocomplete">
  95 + <span md-highlight-text="alarmDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  96 + <md-not-found>
  97 + <div class="tb-not-found">
  98 + <div class="tb-no-entries" ng-if="!textIsNotEmpty(alarmDataKeySearchText)">
  99 + <span translate>entity.no-keys-found</span>
  100 + </div>
  101 + <div ng-if="textIsNotEmpty(alarmDataKeySearchText)">
  102 + <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
  103 + </div>
  104 + </div>
  105 + </md-not-found>
  106 + </md-autocomplete>
  107 + <md-chip-template>
  108 + <div layout="row" layout-align="start center" class="tb-attribute-chip">
  109 + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
  110 + <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
  111 + </div>
  112 + <div layout="row" flex>
  113 + <div class="tb-chip-label">
  114 + {{$chip.label}}
  115 + </div>
  116 + <div class="tb-chip-separator">: </div>
  117 + <div class="tb-chip-label">
  118 + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
  119 + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
  120 + </div>
  121 + </div>
  122 + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
  123 + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
  124 + </md-button>
  125 + </div>
  126 + </md-chip-template>
  127 + </md-chips>
78 <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> 128 <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
79 - <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div> 129 + <div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div>
  130 + <div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
80 </div> 131 </div>
81 </section> 132 </section>
82 </section> 133 </section>
@@ -30,7 +30,7 @@ export default angular.module('thingsboard.directives.datasource', [thingsboardT @@ -30,7 +30,7 @@ export default angular.module('thingsboard.directives.datasource', [thingsboardT
30 .name; 30 .name;
31 31
32 /*@ngInject*/ 32 /*@ngInject*/
33 -function Datasource($compile, $templateCache, types) { 33 +function Datasource($compile, $templateCache, utils, types) {
34 34
35 var linker = function (scope, element, attrs, ngModelCtrl) { 35 var linker = function (scope, element, attrs, ngModelCtrl) {
36 36
@@ -53,8 +53,12 @@ function Datasource($compile, $templateCache, types) { @@ -53,8 +53,12 @@ function Datasource($compile, $templateCache, types) {
53 } 53 }
54 54
55 scope.$watch('model.type', function (newType, prevType) { 55 scope.$watch('model.type', function (newType, prevType) {
56 - if (newType != prevType) {  
57 - scope.model.dataKeys = []; 56 + if (newType && prevType && newType != prevType) {
  57 + if (scope.widgetType == types.widgetType.alarm.value) {
  58 + scope.model.dataKeys = utils.getDefaultAlarmDataKeys();
  59 + } else {
  60 + scope.model.dataKeys = [];
  61 + }
58 } 62 }
59 }); 63 });
60 64
@@ -63,9 +67,10 @@ function Datasource($compile, $templateCache, types) { @@ -63,9 +67,10 @@ function Datasource($compile, $templateCache, types) {
63 }, true); 67 }, true);
64 68
65 ngModelCtrl.$render = function () { 69 ngModelCtrl.$render = function () {
66 - scope.model = {};  
67 if (ngModelCtrl.$viewValue) { 70 if (ngModelCtrl.$viewValue) {
68 scope.model = ngModelCtrl.$viewValue; 71 scope.model = ngModelCtrl.$viewValue;
  72 + } else {
  73 + scope.model = {};
69 } 74 }
70 }; 75 };
71 76
@@ -29,6 +29,7 @@ @@ -29,6 +29,7 @@
29 ng-model="model" 29 ng-model="model"
30 datakey-settings-schema="datakeySettingsSchema" 30 datakey-settings-schema="datakeySettingsSchema"
31 ng-required="model.type === types.datasourceType.function" 31 ng-required="model.type === types.datasourceType.function"
  32 + widget-type="widgetType"
32 generate-data-key="generateDataKey({chip: chip, type: type})"> 33 generate-data-key="generateDataKey({chip: chip, type: type})">
33 </tb-datasource-func> 34 </tb-datasource-func>
34 <tb-datasource-entity flex 35 <tb-datasource-entity flex
@@ -64,7 +64,7 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -64,7 +64,7 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
64 64
65 scope.historyOnly = angular.isDefined(attrs.historyOnly); 65 scope.historyOnly = angular.isDefined(attrs.historyOnly);
66 66
67 - scope.aggregation = angular.isDefined(attrs.aggregation); 67 + scope.aggregation = scope.$eval(attrs.aggregation);
68 68
69 scope.isToolbar = angular.isDefined(attrs.isToolbar); 69 scope.isToolbar = angular.isDefined(attrs.isToolbar);
70 70
@@ -43,7 +43,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar @@ -43,7 +43,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar
43 .name; 43 .name;
44 44
45 /*@ngInject*/ 45 /*@ngInject*/
46 -function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, utils) { 46 +function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout, types, utils) {
47 47
48 var linker = function (scope, element, attrs, ngModelCtrl) { 48 var linker = function (scope, element, attrs, ngModelCtrl) {
49 49
@@ -87,6 +87,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -87,6 +87,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
87 value: null 87 value: null
88 } 88 }
89 89
  90 + scope.alarmSource = {
  91 + value: null
  92 + }
  93 +
90 ngModelCtrl.$render = function () { 94 ngModelCtrl.$render = function () {
91 if (ngModelCtrl.$viewValue) { 95 if (ngModelCtrl.$viewValue) {
92 var config = ngModelCtrl.$viewValue.config; 96 var config = ngModelCtrl.$viewValue.config;
@@ -113,7 +117,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -113,7 +117,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
113 scope.showLegend = angular.isDefined(config.showLegend) ? 117 scope.showLegend = angular.isDefined(config.showLegend) ?
114 config.showLegend : scope.widgetType === types.widgetType.timeseries.value; 118 config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
115 scope.legendConfig = config.legendConfig; 119 scope.legendConfig = config.legendConfig;
116 - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value 120 + if (scope.widgetType !== types.widgetType.rpc.value &&
  121 + scope.widgetType !== types.widgetType.alarm.value &&
  122 + scope.widgetType !== types.widgetType.static.value
117 && scope.isDataEnabled) { 123 && scope.isDataEnabled) {
118 if (scope.datasources) { 124 if (scope.datasources) {
119 scope.datasources.splice(0, scope.datasources.length); 125 scope.datasources.splice(0, scope.datasources.length);
@@ -137,6 +143,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -137,6 +143,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
137 } else { 143 } else {
138 scope.targetDeviceAlias.value = null; 144 scope.targetDeviceAlias.value = null;
139 } 145 }
  146 + } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
  147 + if (config.alarmSource) {
  148 + scope.alarmSource.value = config.alarmSource;
  149 + } else {
  150 + scope.alarmSource.value = null;
  151 + }
140 } 152 }
141 153
142 scope.settings = config.settings; 154 scope.settings = config.settings;
@@ -175,6 +187,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -175,6 +187,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
175 if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { 187 if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
176 valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; 188 valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0;
177 ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); 189 ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
  190 + } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
  191 + valid = config && config.alarmSource;
  192 + ngModelCtrl.$setValidity('alarmSource', valid);
178 } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { 193 } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
179 valid = config && config.datasources && config.datasources.length > 0; 194 valid = config && config.datasources && config.datasources.length > 0;
180 ngModelCtrl.$setValidity('datasources', valid); 195 ngModelCtrl.$setValidity('datasources', valid);
@@ -253,7 +268,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -253,7 +268,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
253 }, true); 268 }, true);
254 269
255 scope.$watch('datasources', function () { 270 scope.$watch('datasources', function () {
256 - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value 271 + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config
  272 + && scope.widgetType !== types.widgetType.rpc.value
  273 + && scope.widgetType !== types.widgetType.alarm.value
257 && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { 274 && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
258 var value = ngModelCtrl.$viewValue; 275 var value = ngModelCtrl.$viewValue;
259 var config = value.config; 276 var config = value.config;
@@ -286,6 +303,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -286,6 +303,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
286 } 303 }
287 }); 304 });
288 305
  306 + scope.$watch('alarmSource.value', function () {
  307 + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
  308 + var value = ngModelCtrl.$viewValue;
  309 + var config = value.config;
  310 + if (scope.alarmSource.value) {
  311 + config.alarmSource = scope.alarmSource.value;
  312 + } else {
  313 + config.alarmSource = null;
  314 + }
  315 + ngModelCtrl.$setViewValue(value);
  316 + scope.updateValidity();
  317 + }
  318 + });
  319 +
289 scope.addDatasource = function () { 320 scope.addDatasource = function () {
290 var newDatasource; 321 var newDatasource;
291 if (scope.functionsOnly) { 322 if (scope.functionsOnly) {
@@ -320,10 +351,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -320,10 +351,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
320 return chip; 351 return chip;
321 } 352 }
322 353
  354 + var label = chip;
  355 + if (type === types.dataKeyType.alarm) {
  356 + var alarmField = types.alarmFields[chip];
  357 + if (alarmField) {
  358 + label = $translate.instant(alarmField.name)+'';
  359 + }
  360 + }
  361 + label = scope.genNextLabel(label);
  362 +
323 var result = { 363 var result = {
324 name: chip, 364 name: chip,
325 type: type, 365 type: type,
326 - label: scope.genNextLabel(chip), 366 + label: label,
327 color: scope.genNextColor(), 367 color: scope.genNextColor(),
328 settings: {}, 368 settings: {},
329 _hash: Math.random() 369 _hash: Math.random()
@@ -351,15 +391,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -351,15 +391,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
351 var matches = false; 391 var matches = false;
352 do { 392 do {
353 matches = false; 393 matches = false;
354 - if (value.config.datasources) {  
355 - for (var d=0;d<value.config.datasources.length;d++) {  
356 - var datasource = value.config.datasources[d];  
357 - for (var k=0;k<datasource.dataKeys.length;k++) {  
358 - var dataKey = datasource.dataKeys[k];  
359 - if (dataKey.label === label) {  
360 - i++;  
361 - label = name + ' ' + i;  
362 - matches = true; 394 + var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
  395 + if (datasources) {
  396 + for (var d=0;d<datasources.length;d++) {
  397 + var datasource = datasources[d];
  398 + if (datasource && datasource.dataKeys) {
  399 + for (var k = 0; k < datasource.dataKeys.length; k++) {
  400 + var dataKey = datasource.dataKeys[k];
  401 + if (dataKey.label === label) {
  402 + i++;
  403 + label = name + ' ' + i;
  404 + matches = true;
  405 + }
363 } 406 }
364 } 407 }
365 } 408 }
@@ -371,10 +414,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -371,10 +414,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
371 scope.genNextColor = function () { 414 scope.genNextColor = function () {
372 var i = 0; 415 var i = 0;
373 var value = ngModelCtrl.$viewValue; 416 var value = ngModelCtrl.$viewValue;
374 - if (value.config.datasources) {  
375 - for (var d=0;d<value.config.datasources.length;d++) {  
376 - var datasource = value.config.datasources[d];  
377 - i += datasource.dataKeys.length; 417 + var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
  418 + if (datasources) {
  419 + for (var d=0;d<datasources.length;d++) {
  420 + var datasource = datasources[d];
  421 + if (datasource && datasource.dataKeys) {
  422 + i += datasource.dataKeys.length;
  423 + }
378 } 424 }
379 } 425 }
380 return utils.getMaterialColor(i); 426 return utils.getMaterialColor(i);
@@ -20,18 +20,22 @@ @@ -20,18 +20,22 @@
20 <md-tab label="{{ 'widget-config.data' | translate }}" 20 <md-tab label="{{ 'widget-config.data' | translate }}"
21 ng-show="widgetType !== types.widgetType.static.value"> 21 ng-show="widgetType !== types.widgetType.static.value">
22 <md-content class="md-padding" layout="column"> 22 <md-content class="md-padding" layout="column">
23 - <div ng-show="widgetType === types.widgetType.timeseries.value" layout='column' layout-align="center" 23 + <div ng-show="widgetType === types.widgetType.timeseries.value || widgetType === types.widgetType.alarm.value" layout='column' layout-align="center"
24 layout-gt-sm='row' layout-align-gt-sm="start center"> 24 layout-gt-sm='row' layout-align-gt-sm="start center">
25 <md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}" 25 <md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}"
26 ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }} 26 ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }}
27 </md-checkbox> 27 </md-checkbox>
28 <section flex layout="row" layout-align="start center" style="margin-bottom: 16px;"> 28 <section flex layout="row" layout-align="start center" style="margin-bottom: 16px;">
29 <span ng-class="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span> 29 <span ng-class="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
30 - <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation flex ng-model="timewindow"></tb-timewindow> 30 + <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation="{{ widgetType === types.widgetType.timeseries.value }}"
  31 + flex ng-model="timewindow"></tb-timewindow>
31 </section> 32 </section>
32 </div> 33 </div>
33 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" 34 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
34 - ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled"> 35 + ng-show="widgetType !== types.widgetType.rpc.value
  36 + && widgetType !== types.widgetType.alarm.value
  37 + && widgetType !== types.widgetType.static.value
  38 + && isDataEnabled">
35 <v-pane id="datasources-pane" expanded="true"> 39 <v-pane id="datasources-pane" expanded="true">
36 <v-pane-header> 40 <v-pane-header>
37 {{ 'widget-config.datasources' | translate }} 41 {{ 'widget-config.datasources' | translate }}
@@ -112,6 +116,24 @@ @@ -112,6 +116,24 @@
112 </v-pane-content> 116 </v-pane-content>
113 </v-pane> 117 </v-pane>
114 </v-accordion> 118 </v-accordion>
  119 + <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
  120 + ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
  121 + <v-pane id="alarm-source-pane" expanded="true">
  122 + <v-pane-header>
  123 + {{ 'widget-config.alarm-source' | translate }}
  124 + </v-pane-header>
  125 + <v-pane-content style="padding: 0 5px;">
  126 + <tb-datasource flex
  127 + ng-model="alarmSource.value"
  128 + widget-type="widgetType"
  129 + functions-only="functionsOnly"
  130 + alias-controller="aliasController"
  131 + datakey-settings-schema="datakeySettingsSchema"
  132 + generate-data-key="generateDataKey(chip,type)"
  133 + on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"></tb-datasource>
  134 + </v-pane-content>
  135 + </v-pane>
  136 + </v-accordion>
115 </md-content> 137 </md-content>
116 </md-tab> 138 </md-tab>
117 <md-tab label="{{ 'widget-config.settings' | translate }}"> 139 <md-tab label="{{ 'widget-config.settings' | translate }}">
@@ -21,7 +21,7 @@ import Subscription from '../api/subscription'; @@ -21,7 +21,7 @@ import Subscription from '../api/subscription';
21 21
22 /*@ngInject*/ 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, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
24 - datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, 24 + datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
25 dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { 25 dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
26 26
27 var vm = this; 27 var vm = this;
@@ -44,7 +44,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -44,7 +44,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
44 44
45 var widgetContext = { 45 var widgetContext = {
46 inited: false, 46 inited: false,
47 - $scope: $scope,  
48 $container: null, 47 $container: null,
49 $containerParent: null, 48 $containerParent: null,
50 width: 0, 49 width: 0,
@@ -113,6 +112,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -113,6 +112,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
113 timeService: timeService, 112 timeService: timeService,
114 deviceService: deviceService, 113 deviceService: deviceService,
115 datasourceService: datasourceService, 114 datasourceService: datasourceService,
  115 + alarmService: alarmService,
116 utils: utils, 116 utils: utils,
117 widgetUtils: widgetContext.utils, 117 widgetUtils: widgetContext.utils,
118 dashboardTimewindowApi: dashboardTimewindowApi, 118 dashboardTimewindowApi: dashboardTimewindowApi,
@@ -285,9 +285,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -285,9 +285,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
285 var deferred = $q.defer(); 285 var deferred = $q.defer();
286 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { 286 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
287 options = { 287 options = {
288 - type: widget.type,  
289 - datasources: angular.copy(widget.config.datasources)  
290 - }; 288 + type: widget.type
  289 + }
  290 + if (widget.type == types.widgetType.alarm.value) {
  291 + options.alarmSource = angular.copy(widget.config.alarmSource);
  292 + } else {
  293 + options.datasources = angular.copy(widget.config.datasources)
  294 + }
  295 +
291 defaultComponentsOptions(options); 296 defaultComponentsOptions(options);
292 297
293 createSubscription(options).then( 298 createSubscription(options).then(
@@ -320,7 +325,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -320,7 +325,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
320 $scope.executingRpcRequest = subscription.executingRpcRequest; 325 $scope.executingRpcRequest = subscription.executingRpcRequest;
321 }, 326 },
322 onRpcSuccess: function(subscription) { 327 onRpcSuccess: function(subscription) {
323 - $scope.executingRpcRequest = subscription.executingRpcRequest; 328 + $scope.executingRpcRequest = subscription.executingRpcRequest;
324 $scope.rpcErrorText = subscription.rpcErrorText; 329 $scope.rpcErrorText = subscription.rpcErrorText;
325 $scope.rpcRejection = subscription.rpcRejection; 330 $scope.rpcRejection = subscription.rpcRejection;
326 }, 331 },
@@ -436,7 +441,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -436,7 +441,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
436 widgetContext.$container = $('#container', containerElement); 441 widgetContext.$container = $('#container', containerElement);
437 widgetContext.$containerParent = $(containerElement); 442 widgetContext.$containerParent = $(containerElement);
438 443
439 - $compile($element.contents())($scope); 444 + if (widgetSizeDetected) {
  445 + widgetContext.$container.css('height', widgetContext.height + 'px');
  446 + widgetContext.$container.css('width', widgetContext.width + 'px');
  447 + }
  448 +
  449 + widgetContext.$scope = $scope.$new();
  450 +
  451 + $compile($element.contents())(widgetContext.$scope);
440 452
441 addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef 453 addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
442 } 454 }
@@ -444,6 +456,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -444,6 +456,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
444 function destroyWidgetElement() { 456 function destroyWidgetElement() {
445 removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef 457 removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
446 $element.html(''); 458 $element.html('');
  459 + if (widgetContext.$scope) {
  460 + widgetContext.$scope.$destroy();
  461 + }
447 widgetContext.$container = null; 462 widgetContext.$container = null;
448 widgetContext.$containerParent = null; 463 widgetContext.$containerParent = null;
449 } 464 }
@@ -76,6 +76,10 @@ export default function AddWidgetController($scope, widgetService, entityService @@ -76,6 +76,10 @@ export default function AddWidgetController($scope, widgetService, entityService
76 link = 'widgetsConfigRpc'; 76 link = 'widgetsConfigRpc';
77 break; 77 break;
78 } 78 }
  79 + case types.widgetType.alarm.value: {
  80 + link = 'widgetsConfigAlarm';
  81 + break;
  82 + }
79 case types.widgetType.static.value: { 83 case types.widgetType.static.value: {
80 link = 'widgetsConfigStatic'; 84 link = 'widgetsConfigStatic';
81 break; 85 break;
@@ -47,6 +47,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget @@ -47,6 +47,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
47 vm.latestWidgetTypes = []; 47 vm.latestWidgetTypes = [];
48 vm.timeseriesWidgetTypes = []; 48 vm.timeseriesWidgetTypes = [];
49 vm.rpcWidgetTypes = []; 49 vm.rpcWidgetTypes = [];
  50 + vm.alarmWidgetTypes = [];
50 vm.staticWidgetTypes = []; 51 vm.staticWidgetTypes = [];
51 vm.widgetEditMode = $state.$current.data.widgetEditMode; 52 vm.widgetEditMode = $state.$current.data.widgetEditMode;
52 vm.iframeMode = $rootScope.iframeMode; 53 vm.iframeMode = $rootScope.iframeMode;
@@ -263,6 +264,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget @@ -263,6 +264,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
263 vm.latestWidgetTypes = []; 264 vm.latestWidgetTypes = [];
264 vm.timeseriesWidgetTypes = []; 265 vm.timeseriesWidgetTypes = [];
265 vm.rpcWidgetTypes = []; 266 vm.rpcWidgetTypes = [];
  267 + vm.alarmWidgetTypes = [];
266 vm.staticWidgetTypes = []; 268 vm.staticWidgetTypes = [];
267 if (vm.widgetsBundle) { 269 if (vm.widgetsBundle) {
268 var bundleAlias = vm.widgetsBundle.alias; 270 var bundleAlias = vm.widgetsBundle.alias;
@@ -308,6 +310,8 @@ export default function DashboardController(types, utils, dashboardUtils, widget @@ -308,6 +310,8 @@ export default function DashboardController(types, utils, dashboardUtils, widget
308 vm.latestWidgetTypes.push(widget); 310 vm.latestWidgetTypes.push(widget);
309 } else if (widgetTypeInfo.type === types.widgetType.rpc.value) { 311 } else if (widgetTypeInfo.type === types.widgetType.rpc.value) {
310 vm.rpcWidgetTypes.push(widget); 312 vm.rpcWidgetTypes.push(widget);
  313 + } else if (widgetTypeInfo.type === types.widgetType.alarm.value) {
  314 + vm.alarmWidgetTypes.push(widget);
311 } else if (widgetTypeInfo.type === types.widgetType.static.value) { 315 } else if (widgetTypeInfo.type === types.widgetType.static.value) {
312 vm.staticWidgetTypes.push(widget); 316 vm.staticWidgetTypes.push(widget);
313 } 317 }
@@ -358,21 +362,6 @@ export default function DashboardController(types, utils, dashboardUtils, widget @@ -358,21 +362,6 @@ export default function DashboardController(types, utils, dashboardUtils, widget
358 vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; 362 vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
359 vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils, 363 vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils,
360 types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases); 364 types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases);
361 -  
362 - /* entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)  
363 - .then(  
364 - function(resolution) {  
365 - if (resolution.error && !isTenantAdmin()) {  
366 - vm.configurationError = true;  
367 - showAliasesResolutionError(resolution.error);  
368 - } else {  
369 - vm.dashboardConfiguration = vm.dashboard.configuration;  
370 - vm.dashboardCtx.dashboard = vm.dashboard;  
371 - vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;  
372 - vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;  
373 - }  
374 - }  
375 - );*/  
376 }, function fail() { 365 }, function fail() {
377 vm.configurationError = true; 366 vm.configurationError = true;
378 }); 367 });
@@ -744,6 +733,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget @@ -744,6 +733,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget
744 link = 'widgetsConfigRpc'; 733 link = 'widgetsConfigRpc';
745 break; 734 break;
746 } 735 }
  736 + case types.widgetType.alarm.value: {
  737 + link = 'widgetsConfigAlarm';
  738 + break;
  739 + }
747 case types.widgetType.static.value: { 740 case types.widgetType.static.value: {
748 link = 'widgetsConfigStatic'; 741 link = 'widgetsConfigStatic';
749 break; 742 break;
@@ -851,6 +844,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget @@ -851,6 +844,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
851 vm.timeseriesWidgetTypes = []; 844 vm.timeseriesWidgetTypes = [];
852 vm.latestWidgetTypes = []; 845 vm.latestWidgetTypes = [];
853 vm.rpcWidgetTypes = []; 846 vm.rpcWidgetTypes = [];
  847 + vm.alarmWidgetTypes = [];
854 vm.staticWidgetTypes = []; 848 vm.staticWidgetTypes = [];
855 } 849 }
856 850
@@ -52,7 +52,7 @@ @@ -52,7 +52,7 @@
52 <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()" 52 <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
53 is-toolbar 53 is-toolbar
54 direction="left" 54 direction="left"
55 - tooltip-direction="bottom" aggregation 55 + tooltip-direction="bottom" aggregation="true"
56 ng-model="vm.dashboardCtx.dashboardTimewindow"> 56 ng-model="vm.dashboardCtx.dashboardTimewindow">
57 </tb-timewindow> 57 </tb-timewindow>
58 <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()" 58 <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
@@ -179,6 +179,7 @@ @@ -179,6 +179,7 @@
179 <tb-edit-widget 179 <tb-edit-widget
180 dashboard="vm.dashboard" 180 dashboard="vm.dashboard"
181 alias-controller="vm.dashboardCtx.aliasController" 181 alias-controller="vm.dashboardCtx.aliasController"
  182 + widget-edit-mode="vm.widgetEditMode"
182 widget="vm.editingWidget" 183 widget="vm.editingWidget"
183 widget-layout="vm.editingWidgetLayout" 184 widget-layout="vm.editingWidgetLayout"
184 the-form="vm.widgetForm"> 185 the-form="vm.widgetForm">
@@ -205,7 +206,8 @@ @@ -205,7 +206,8 @@
205 </header-pane> 206 </header-pane>
206 <div ng-if="vm.isAddingWidget"> 207 <div ng-if="vm.isAddingWidget">
207 <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 || 208 <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||
208 - vm.rpcWidgetTypes.length > 0 || vm.staticWidgetTypes.length > 0" 209 + vm.rpcWidgetTypes.length > 0 || vm.alarmWidgetTypes.length > 0 ||
  210 + vm.staticWidgetTypes.length > 0"
209 flex 211 flex
210 class="tb-absolute-fill" md-border-bottom> 212 class="tb-absolute-fill" md-border-bottom>
211 <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> 213 <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
@@ -238,6 +240,16 @@ @@ -238,6 +240,16 @@
238 on-widget-clicked="vm.addWidgetFromType(event, widget)"> 240 on-widget-clicked="vm.addWidgetFromType(event, widget)">
239 </tb-dashboard> 241 </tb-dashboard>
240 </md-tab> 242 </md-tab>
  243 + <md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
  244 + <tb-dashboard
  245 + widgets="vm.alarmWidgetTypes"
  246 + is-edit="false"
  247 + is-mobile="true"
  248 + is-edit-action-enabled="false"
  249 + is-remove-action-enabled="false"
  250 + on-widget-clicked="vm.addWidgetFromType(event, widget)">
  251 + </tb-dashboard>
  252 + </md-tab>
241 <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}"> 253 <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
242 <tb-dashboard 254 <tb-dashboard
243 widgets="vm.staticWidgetTypes" 255 widgets="vm.staticWidgetTypes"
@@ -250,7 +262,8 @@ @@ -250,7 +262,8 @@
250 </md-tab> 262 </md-tab>
251 </md-tabs> 263 </md-tabs>
252 <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 && 264 <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&
253 - vm.rpcWidgetTypes.length === 0 && vm.staticWidgetTypes.length === 0 && vm.widgetsBundle" 265 + vm.rpcWidgetTypes.length === 0 && vm.alarmWidgetTypes.length === 0 &&
  266 + vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
254 layout-align="center center" 267 layout-align="center center"
255 style="text-transform: uppercase; display: flex;" 268 style="text-transform: uppercase; display: flex;"
256 class="md-headline tb-absolute-fill">widgets-bundle.empty</span> 269 class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
@@ -131,6 +131,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid @@ -131,6 +131,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
131 scope: { 131 scope: {
132 dashboard: '=', 132 dashboard: '=',
133 aliasController: '=', 133 aliasController: '=',
  134 + widgetEditMode: '=',
134 widget: '=', 135 widget: '=',
135 widgetLayout: '=', 136 widgetLayout: '=',
136 theForm: '=' 137 theForm: '='
@@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
22 widget-settings-schema="settingsSchema" 22 widget-settings-schema="settingsSchema"
23 datakey-settings-schema="dataKeySettingsSchema" 23 datakey-settings-schema="dataKeySettingsSchema"
24 alias-controller="aliasController" 24 alias-controller="aliasController"
25 - functions-only="functionsOnly" 25 + functions-only="widgetEditMode"
26 fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" 26 fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
27 on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" 27 on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
28 the-form="theForm"></tb-widget-config> 28 the-form="theForm"></tb-widget-config>
@@ -87,6 +87,7 @@ export default angular.module('thingsboard.help', []) @@ -87,6 +87,7 @@ export default angular.module('thingsboard.help', [])
87 widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries", 87 widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries",
88 widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest", 88 widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest",
89 widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", 89 widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc",
  90 + widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm",
90 widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static", 91 widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static",
91 }, 92 },
92 getPluginLink: function(plugin) { 93 getPluginLink: function(plugin) {
@@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', []) @@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', [])
131 "type": "Type", 131 "type": "Type",
132 "severity": "Severity", 132 "severity": "Severity",
133 "originator": "Originator", 133 "originator": "Originator",
  134 + "originator-type": "Originator type",
134 "details": "Details", 135 "details": "Details",
135 "status": "Status", 136 "status": "Status",
136 "alarm-details": "Alarm details", 137 "alarm-details": "Alarm details",
@@ -144,7 +145,10 @@ export default angular.module('thingsboard.locale', []) @@ -144,7 +145,10 @@ export default angular.module('thingsboard.locale', [])
144 "severity-warning": "Warning", 145 "severity-warning": "Warning",
145 "severity-indeterminate": "Indeterminate", 146 "severity-indeterminate": "Indeterminate",
146 "acknowledge": "Acknowledge", 147 "acknowledge": "Acknowledge",
147 - "clear": "Clear" 148 + "clear": "Clear",
  149 + "search": "Search alarms",
  150 + "selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected",
  151 + "no-data": "No data to display"
148 }, 152 },
149 "alias": { 153 "alias": {
150 "add": "Add alias", 154 "add": "Add alias",
@@ -486,8 +490,10 @@ export default angular.module('thingsboard.locale', []) @@ -486,8 +490,10 @@ export default angular.module('thingsboard.locale', [])
486 "configuration": "Data key configuration", 490 "configuration": "Data key configuration",
487 "timeseries": "Timeseries", 491 "timeseries": "Timeseries",
488 "attributes": "Attributes", 492 "attributes": "Attributes",
  493 + "alarm": "Alarm fields",
489 "timeseries-required": "Entity timeseries are required.", 494 "timeseries-required": "Entity timeseries are required.",
490 "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", 495 "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
  496 + "alarm-fields-required": "Alarm fields are required.",
491 "function-types": "Function types", 497 "function-types": "Function types",
492 "function-types-required": "Function types are required." 498 "function-types-required": "Function types are required."
493 }, 499 },
@@ -1046,6 +1052,7 @@ export default angular.module('thingsboard.locale', []) @@ -1046,6 +1052,7 @@ export default angular.module('thingsboard.locale', [])
1046 "timeseries": "Time series", 1052 "timeseries": "Time series",
1047 "latest-values": "Latest values", 1053 "latest-values": "Latest values",
1048 "rpc": "Control widget", 1054 "rpc": "Control widget",
  1055 + "alarm": "Alarm widget",
1049 "static": "Static widget", 1056 "static": "Static widget",
1050 "select-widget-type": "Select widget type", 1057 "select-widget-type": "Select widget type",
1051 "missing-widget-title-error": "Widget title must be specified!", 1058 "missing-widget-title-error": "Widget title must be specified!",
@@ -1133,7 +1140,8 @@ export default angular.module('thingsboard.locale', []) @@ -1133,7 +1140,8 @@ export default angular.module('thingsboard.locale', [])
1133 "datasource-parameters": "Parameters", 1140 "datasource-parameters": "Parameters",
1134 "remove-datasource": "Remove datasource", 1141 "remove-datasource": "Remove datasource",
1135 "add-datasource": "Add datasource", 1142 "add-datasource": "Add datasource",
1136 - "target-device": "Target device" 1143 + "target-device": "Target device",
  1144 + "alarm-source": "Alarm source"
1137 }, 1145 },
1138 "widget-type": { 1146 "widget-type": {
1139 "import": "Import widget type", 1147 "import": "Import widget type",
  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 './alarms-table-widget.scss';
  18 +
  19 +/* eslint-disable import/no-unresolved, import/default */
  20 +
  21 +import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html';
  22 +
  23 +/* eslint-enable import/no-unresolved, import/default */
  24 +
  25 +import tinycolor from 'tinycolor2';
  26 +import cssjs from '../../../vendor/css.js/css';
  27 +
  28 +export default angular.module('thingsboard.widgets.alarmsTableWidget', [])
  29 + .directive('tbAlarmsTableWidget', AlarmsTableWidget)
  30 + .name;
  31 +
  32 +/*@ngInject*/
  33 +function AlarmsTableWidget() {
  34 + return {
  35 + restrict: "E",
  36 + scope: true,
  37 + bindToController: {
  38 + tableId: '=',
  39 + config: '=',
  40 + subscription: '='
  41 + },
  42 + controller: AlarmsTableWidgetController,
  43 + controllerAs: 'vm',
  44 + templateUrl: alarmsTableWidgetTemplate
  45 + };
  46 +}
  47 +
  48 +/*@ngInject*/
  49 +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUtil, $translate, utils, types) {
  50 + var vm = this;
  51 +
  52 + vm.stylesInfo = {};
  53 + vm.contentsInfo = {};
  54 +
  55 + vm.showData = true;
  56 + vm.hasData = false;
  57 +
  58 + vm.alarms = [];
  59 + vm.alarmsCount = 0;
  60 + vm.selectedAlarms = []
  61 +
  62 + vm.alarmSource = null;
  63 + vm.allAlarms = null;
  64 +
  65 + vm.currentAlarm = null;
  66 +
  67 + vm.query = {
  68 + order: '-'+types.alarmFields.createdTime.value,
  69 + limit: 10,
  70 + page: 1,
  71 + search: null
  72 + };
  73 +
  74 + vm.alarmsTitle = $translate.instant('alarm.alarms');
  75 +
  76 + vm.enterFilterMode = enterFilterMode;
  77 + vm.exitFilterMode = exitFilterMode;
  78 + vm.onReorder = onReorder;
  79 + vm.onPaginate = onPaginate;
  80 +
  81 + vm.cellStyle = cellStyle;
  82 + vm.cellContent = cellContent;
  83 +
  84 + $scope.$watch('vm.config', function() {
  85 + if (vm.config) {
  86 + vm.settings = vm.config.settings;
  87 + vm.widgetConfig = vm.config.widgetConfig;
  88 + initializeConfig();
  89 + }
  90 + });
  91 +
  92 + $scope.$watch("vm.query.search", function(newVal, prevVal) {
  93 + if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
  94 + updateAlarms();
  95 + }
  96 + });
  97 +
  98 + $scope.$watch('vm.subscription', function() {
  99 + if (vm.subscription) {
  100 + vm.alarmSource = vm.subscription.alarmSource;
  101 + updateAlarmSource();
  102 + }
  103 + });
  104 +
  105 + $scope.$on('alarms-table-data-updated', function(event, tableId) {
  106 + if (vm.tableId == tableId) {
  107 + if (vm.subscription) {
  108 + vm.allAlarms = vm.subscription.alarms;
  109 + updateAlarms(true);
  110 + $scope.$digest();
  111 + }
  112 + }
  113 + });
  114 +
  115 + $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
  116 + vm.isGtXs = isGtXs;
  117 + });
  118 +
  119 + $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
  120 + vm.isGtMd = isGtMd;
  121 + if (vm.isGtMd) {
  122 + vm.limitOptions = [5, 10, 15];
  123 + } else {
  124 + vm.limitOptions = null;
  125 + }
  126 + });
  127 +
  128 + function initializeConfig() {
  129 +
  130 + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
  131 + vm.alarmsTitle = vm.settings.alarmsTitle;
  132 + }
  133 + //TODO:
  134 +
  135 + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
  136 + var defaultColor = tinycolor(origColor);
  137 + var mdDark = defaultColor.setAlpha(0.87).toRgbString();
  138 + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
  139 + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
  140 + //var mdDarkIcon = mdDarkSecondary;
  141 + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
  142 +
  143 + var cssString = 'table.md-table th.md-column {\n'+
  144 + 'color: ' + mdDarkSecondary + ';\n'+
  145 + '}\n'+
  146 + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+
  147 + 'color: ' + mdDarkDisabled + ';\n'+
  148 + '}\n'+
  149 + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
  150 + 'color: ' + mdDark + ';\n'+
  151 + '}\n'+
  152 + 'table.md-table td.md-cell {\n'+
  153 + 'color: ' + mdDark + ';\n'+
  154 + 'border-top: 1px '+mdDarkDivider+' solid;\n'+
  155 + '}\n'+
  156 + 'table.md-table td.md-cell.md-placeholder {\n'+
  157 + 'color: ' + mdDarkDisabled + ';\n'+
  158 + '}\n'+
  159 + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
  160 + 'color: ' + mdDarkSecondary + ';\n'+
  161 + '}\n'+
  162 + '.md-table-pagination {\n'+
  163 + 'color: ' + mdDarkSecondary + ';\n'+
  164 + 'border-top: 1px '+mdDarkDivider+' solid;\n'+
  165 + '}\n'+
  166 + '.md-table-pagination .buttons md-icon {\n'+
  167 + 'color: ' + mdDarkSecondary + ';\n'+
  168 + '}\n'+
  169 + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
  170 + 'color: ' + mdDarkSecondary + ';\n'+
  171 + '}';
  172 +
  173 + var cssParser = new cssjs();
  174 + cssParser.testMode = false;
  175 + var namespace = 'ts-table-' + hashCode(cssString);
  176 + cssParser.cssPreviewNamespace = namespace;
  177 + cssParser.createStyleElement(namespace, cssString);
  178 + $element.addClass(namespace);
  179 +
  180 + function hashCode(str) {
  181 + var hash = 0;
  182 + var i, char;
  183 + if (str.length === 0) return hash;
  184 + for (i = 0; i < str.length; i++) {
  185 + char = str.charCodeAt(i);
  186 + hash = ((hash << 5) - hash) + char;
  187 + hash = hash & hash;
  188 + }
  189 + return hash;
  190 + }
  191 + }
  192 +
  193 + function enterFilterMode () {
  194 + vm.query.search = '';
  195 + }
  196 +
  197 + function exitFilterMode () {
  198 + vm.query.search = null;
  199 + updateAlarms();
  200 + }
  201 +
  202 + function onReorder () {
  203 + updateAlarms();
  204 + }
  205 +
  206 + function onPaginate () {
  207 + updateAlarms();
  208 + }
  209 +
  210 + function updateAlarms(preserveSelections) {
  211 + if (!preserveSelections) {
  212 + vm.selectedAlarms = [];
  213 + }
  214 + var result = $filter('orderBy')(vm.allAlarms, vm.query.order);
  215 + if (vm.query.search != null) {
  216 + result = $filter('filter')(result, {$: vm.query.search});
  217 + }
  218 + vm.alarmsCount = result.length;
  219 + var startIndex = vm.query.limit * (vm.query.page - 1);
  220 + vm.alarms = result.slice(startIndex, startIndex + vm.query.limit);
  221 + if (preserveSelections) {
  222 + var newSelectedAlarms = [];
  223 + if (vm.selectedAlarms && vm.selectedAlarms.length) {
  224 + var i = vm.selectedAlarms.length;
  225 + while (i--) {
  226 + var selectedAlarm = vm.selectedAlarms[i];
  227 + if (selectedAlarm.id) {
  228 + result = $filter('filter')(vm.alarms, {id: {id: selectedAlarm.id.id} });
  229 + if (result && result.length) {
  230 + newSelectedAlarms.push(result[0]);
  231 + }
  232 + }
  233 + }
  234 + }
  235 + vm.selectedAlarms = newSelectedAlarms;
  236 + }
  237 + }
  238 +
  239 + function cellStyle(alarm, key) {
  240 + var style = {};
  241 + if (alarm && key) {
  242 + var styleInfo = vm.stylesInfo[key.label];
  243 + var value = getAlarmValue(alarm, key);
  244 + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
  245 + try {
  246 + style = styleInfo.cellStyleFunction(value);
  247 + } catch (e) {
  248 + style = {};
  249 + }
  250 + } else {
  251 + style = defaultStyle(key, value);
  252 + }
  253 + }
  254 + return style;
  255 + }
  256 +
  257 + function cellContent(alarm, key) {
  258 + var strContent = '';
  259 + if (alarm && key) {
  260 + var contentInfo = vm.contentsInfo[key.label];
  261 + var value = getAlarmValue(alarm, key);
  262 + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
  263 + if (angular.isDefined(value)) {
  264 + strContent = '' + value;
  265 + }
  266 + var content = strContent;
  267 + try {
  268 + content = contentInfo.cellContentFunction(value, alarm, $filter);
  269 + } catch (e) {
  270 + content = strContent;
  271 + }
  272 + } else {
  273 + content = defaultContent(key, value);
  274 + }
  275 + return content;
  276 + } else {
  277 + return strContent;
  278 + }
  279 + }
  280 +
  281 + function defaultContent(key, value) {
  282 + if (angular.isDefined(value)) {
  283 + var alarmField = types.alarmFields[key.name];
  284 + if (alarmField) {
  285 + if (alarmField.time) {
  286 + return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss');
  287 + } else if (alarmField.value == types.alarmFields.severity.value) {
  288 + return $translate.instant(types.alarmSeverity[value].name);
  289 + } else if (alarmField.value == types.alarmFields.status.value) {
  290 + return $translate.instant('alarm.display-status.'+value);
  291 + } else if (alarmField.value == types.alarmFields.originatorType.value) {
  292 + return $translate.instant(types.entityTypeTranslations[value].type);
  293 + }
  294 + else {
  295 + return value;
  296 + }
  297 + } else {
  298 + return '';
  299 + }
  300 + } else {
  301 + return '';
  302 + }
  303 + }
  304 + function defaultStyle(key, value) {
  305 + if (angular.isDefined(value)) {
  306 + var alarmField = types.alarmFields[key.name];
  307 + if (alarmField) {
  308 + if (alarmField.value == types.alarmFields.severity.value) {
  309 + return {
  310 + fontWeight: 'bold',
  311 + color: types.alarmSeverity[value].color
  312 + };
  313 + } else {
  314 + return {};
  315 + }
  316 + }
  317 + } else {
  318 + return {};
  319 + }
  320 + }
  321 +
  322 + const getDescendantProp = (obj, path) => (
  323 + path.split('.').reduce((acc, part) => acc && acc[part], obj)
  324 + );
  325 +
  326 + function getAlarmValue(alarm, key) {
  327 + var alarmField = types.alarmFields[key.name];
  328 + if (alarmField) {
  329 + return getDescendantProp(alarm, alarmField.value);
  330 + } else {
  331 + return getDescendantProp(alarm, key.name);
  332 + }
  333 + }
  334 +
  335 + function updateAlarmSource() {
  336 +
  337 + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
  338 + vm.alarmsTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.settings.alarmsTitle);
  339 + }
  340 +
  341 + vm.stylesInfo = {};
  342 + vm.contentsInfo = {};
  343 +
  344 + for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
  345 + var dataKey = vm.alarmSource.dataKeys[d];
  346 + var keySettings = dataKey.settings;
  347 +
  348 + var cellStyleFunction = null;
  349 + var useCellStyleFunction = false;
  350 +
  351 + if (keySettings.useCellStyleFunction === true) {
  352 + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
  353 + try {
  354 + cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
  355 + useCellStyleFunction = true;
  356 + } catch (e) {
  357 + cellStyleFunction = null;
  358 + useCellStyleFunction = false;
  359 + }
  360 + }
  361 + }
  362 +
  363 + vm.stylesInfo[dataKey.label] = {
  364 + useCellStyleFunction: useCellStyleFunction,
  365 + cellStyleFunction: cellStyleFunction
  366 + };
  367 +
  368 + var cellContentFunction = null;
  369 + var useCellContentFunction = false;
  370 +
  371 + if (keySettings.useCellContentFunction === true) {
  372 + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
  373 + try {
  374 + cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction);
  375 + useCellContentFunction = true;
  376 + } catch (e) {
  377 + cellContentFunction = null;
  378 + useCellContentFunction = false;
  379 + }
  380 + }
  381 + }
  382 +
  383 + vm.contentsInfo[dataKey.label] = {
  384 + useCellContentFunction: useCellContentFunction,
  385 + cellContentFunction: cellContentFunction
  386 + };
  387 + }
  388 + }
  389 +
  390 +}
  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-alarms-table {
  18 + margin-top: 15px;
  19 + &.tb-data-table {
  20 + table.md-table {
  21 + tbody {
  22 + tr {
  23 + td {
  24 + &.ag-action-cell {
  25 + min-width: 40px;
  26 + max-width: 40px;
  27 + width: 40px;
  28 + }
  29 + }
  30 + }
  31 + }
  32 + }
  33 + }
  34 +}
  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-absolute-fill tb-alarms-table tb-data-table" layout="column">
  19 + <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
  20 + <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedAlarms.length
  21 + && vm.query.search === null">
  22 + <div class="md-toolbar-tools">
  23 + <span>{{ vm.alarmsTitle }}</span>
  24 + <span flex></span>
  25 + <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
  26 + <md-icon>search</md-icon>
  27 + <md-tooltip md-direction="top">
  28 + {{ 'action.search' | translate }}
  29 + </md-tooltip>
  30 + </md-button>
  31 + </div>
  32 + </md-toolbar>
  33 + <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedAlarms.length &&
  34 + vm.query.search != null">
  35 + <div class="md-toolbar-tools">
  36 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  37 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  38 + <md-tooltip md-direction="top">
  39 + {{'alarm.search' | translate}}
  40 + </md-tooltip>
  41 + </md-button>
  42 + <md-input-container flex>
  43 + <label>&nbsp;</label>
  44 + <input ng-model="vm.query.search" placeholder="{{'alarm.search' | translate}}"/>
  45 + </md-input-container>
  46 + <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
  47 + <md-icon aria-label="Close" class="material-icons">close</md-icon>
  48 + <md-tooltip md-direction="top">
  49 + {{ 'action.close' | translate }}
  50 + </md-tooltip>
  51 + </md-button>
  52 + </div>
  53 + </md-toolbar>
  54 + <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedAlarms.length">
  55 + <div class="md-toolbar-tools">
  56 + <span translate="alarm.selected-alarms"
  57 + translate-values="{count: vm.selectedAlarms.length}"
  58 + translate-interpolation="messageformat"></span>
  59 + <span flex></span>
  60 + <md-button class="md-icon-button" ng-click="vm.ackAlarms($event)">
  61 + <md-icon>done</md-icon>
  62 + <md-tooltip md-direction="top">
  63 + {{ 'alarm.acknowledge' | translate }}
  64 + </md-tooltip>
  65 + </md-button>
  66 + <md-button class="md-icon-button" ng-click="vm.clearAlarms($event)">
  67 + <md-icon>clear</md-icon>
  68 + <md-tooltip md-direction="top">
  69 + {{ 'alarm.clear' | translate }}
  70 + </md-tooltip>
  71 + </md-button>
  72 + </div>
  73 + </md-toolbar>
  74 + <md-table-container flex>
  75 + <table md-table md-row-select multiple="" ng-model="vm.selectedAlarms">
  76 + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
  77 + <tr md-row>
  78 + <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.label }}</span></th>
  79 + <th md-column><span>&nbsp</span></th>
  80 + </tr>
  81 + </thead>
  82 + <tbody md-body>
  83 + <tr ng-show="vm.alarms.length" md-row md-select="alarm"
  84 + md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms"
  85 + ng-click="vm.onRowClick($event, alarm)" ng-class="{'tb-current': vm.isCurrent(alarm)}">
  86 + <td md-cell ng-repeat="key in vm.alarmSource.dataKeys"
  87 + ng-style="vm.cellStyle(alarm, key)"
  88 + ng-bind-html="vm.cellContent(alarm, key)">
  89 + </td>
  90 + <td md-cell class="tb-action-cell">
  91 + <md-button class="md-icon-button" aria-label="{{ 'alarm.details' | translate }}"
  92 + ng-click="vm.openAlarmDetails($event, alarm)">
  93 + <md-icon aria-label="{{ 'alarm.details' | translate }}" class="material-icons">more_horiz</md-icon>
  94 + <md-tooltip md-direction="top">
  95 + {{ 'alarm.details' | translate }}
  96 + </md-tooltip>
  97 + </md-button>
  98 + </td>
  99 + </tr>
  100 + </tbody>
  101 + </table>
  102 + <md-divider></md-divider>
  103 + <span ng-show="!vm.alarms.length"
  104 + layout-align="center center"
  105 + class="no-data-found" translate>alarm.no-alarms-prompt</span>
  106 + </md-table-container>
  107 + <md-table-pagination md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions"
  108 + md-page="vm.query.page" md-total="{{vm.alarmsCount}}"
  109 + md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd">
  110 + </md-table-pagination>
  111 + </div>
  112 + <span ng-show="!vm.showData"
  113 + layout-align="center center"
  114 + style="text-transform: uppercase; display: flex;"
  115 + class="tb-absolute-fill" translate>alarm.no-data</span>
  116 +</div>
@@ -54,6 +54,13 @@ @@ -54,6 +54,13 @@
54 <span translate>{{vm.types.widgetType.rpc.name}}</span> 54 <span translate>{{vm.types.widgetType.rpc.name}}</span>
55 </md-button> 55 </md-button>
56 <md-button class="tb-card-button md-raised md-primary" layout="column" 56 <md-button class="tb-card-button md-raised md-primary" layout="column"
  57 + ng-click="vm.typeSelected(vm.types.widgetType.alarm.value)">
  58 + <md-icon class="material-icons tb-md-96"
  59 + aria-label="{{ vm.types.widgetType.alarm.name | translate }}">error
  60 + </md-icon>
  61 + <span translate>{{vm.types.widgetType.alarm.name}}</span>
  62 + </md-button>
  63 + <md-button class="tb-card-button md-raised md-primary" layout="column"
57 ng-click="vm.typeSelected(vm.types.widgetType.static.value)"> 64 ng-click="vm.typeSelected(vm.types.widgetType.static.value)">
58 <md-icon class="material-icons tb-md-96" 65 <md-icon class="material-icons tb-md-96"
59 aria-label="{{ vm.types.widgetType.static.name | translate }}">font_download 66 aria-label="{{ vm.types.widgetType.static.name | translate }}">font_download
@@ -329,10 +329,14 @@ export default function WidgetEditorController(widgetService, userService, types @@ -329,10 +329,14 @@ export default function WidgetEditorController(widgetService, userService, types
329 $scope.$watch('vm.widget.type', function (newVal, oldVal) { 329 $scope.$watch('vm.widget.type', function (newVal, oldVal) {
330 if (!angular.equals(newVal, oldVal)) { 330 if (!angular.equals(newVal, oldVal)) {
331 var config = angular.fromJson(vm.widget.defaultConfig); 331 var config = angular.fromJson(vm.widget.defaultConfig);
332 - if (vm.widget.type !== types.widgetType.rpc.value) { 332 + if (vm.widget.type !== types.widgetType.rpc.value
  333 + && vm.widget.type !== types.widgetType.alarm.value) {
333 if (config.targetDeviceAliases) { 334 if (config.targetDeviceAliases) {
334 delete config.targetDeviceAliases; 335 delete config.targetDeviceAliases;
335 } 336 }
  337 + if (config.alarmSource) {
  338 + delete config.alarmSource;
  339 + }
336 if (!config.datasources) { 340 if (!config.datasources) {
337 config.datasources = []; 341 config.datasources = [];
338 } 342 }
@@ -346,22 +350,38 @@ export default function WidgetEditorController(widgetService, userService, types @@ -346,22 +350,38 @@ export default function WidgetEditorController(widgetService, userService, types
346 for (var i = 0; i < config.datasources.length; i++) { 350 for (var i = 0; i < config.datasources.length; i++) {
347 var datasource = config.datasources[i]; 351 var datasource = config.datasources[i];
348 datasource.type = vm.widget.type; 352 datasource.type = vm.widget.type;
349 - if (vm.widget.type !== types.widgetType.timeseries.value && datasource.intervalSec) {  
350 - delete datasource.intervalSec;  
351 - } else if (vm.widget.type === types.widgetType.timeseries.value && !datasource.intervalSec) {  
352 - datasource.intervalSec = 60;  
353 - }  
354 } 353 }
355 - } else { 354 + } else if (vm.widget.type == types.widgetType.rpc.value) {
356 if (config.datasources) { 355 if (config.datasources) {
357 delete config.datasources; 356 delete config.datasources;
358 } 357 }
  358 + if (config.alarmSource) {
  359 + delete config.alarmSource;
  360 + }
359 if (config.timewindow) { 361 if (config.timewindow) {
360 delete config.timewindow; 362 delete config.timewindow;
361 } 363 }
362 if (!config.targetDeviceAliases) { 364 if (!config.targetDeviceAliases) {
363 config.targetDeviceAliases = []; 365 config.targetDeviceAliases = [];
364 } 366 }
  367 + } else { // alarm
  368 + if (config.datasources) {
  369 + delete config.datasources;
  370 + }
  371 + if (config.targetDeviceAliases) {
  372 + delete config.targetDeviceAliases;
  373 + }
  374 + if (!config.alarmSource) {
  375 + config.alarmSource = {};
  376 + config.alarmSource.type = vm.widget.type
  377 + }
  378 + if (!config.timewindow) {
  379 + config.timewindow = {
  380 + realtime: {
  381 + timewindowMs: 24 * 60 * 60 * 1000
  382 + }
  383 + };
  384 + }
365 } 385 }
366 vm.widget.defaultConfig = angular.toJson(config); 386 vm.widget.defaultConfig = angular.toJson(config);
367 } 387 }