Commit 531f42450f94c1e70d34e4c30fca51248fdbc6df
1 parent
0726fbb5
TB-63: Alarms widget initial implementation.
Showing
30 changed files
with
1370 additions
and
169 deletions
... | ... | @@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', []) |
18 | 18 | .name; |
19 | 19 | |
20 | 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 | 44 | var service = { |
23 | 45 | getAlarm: getAlarm, |
24 | 46 | getAlarmInfo: getAlarmInfo, |
... | ... | @@ -27,7 +49,9 @@ function AlarmService($http, $q, $interval, $filter) { |
27 | 49 | clearAlarm: clearAlarm, |
28 | 50 | getAlarms: getAlarms, |
29 | 51 | pollAlarms: pollAlarms, |
30 | - cancelPollAlarms: cancelPollAlarms | |
52 | + cancelPollAlarms: cancelPollAlarms, | |
53 | + subscribeForAlarms: subscribeForAlarms, | |
54 | + unsubscribeFromAlarms: unsubscribeFromAlarms | |
31 | 55 | } |
32 | 56 | |
33 | 57 | return service; |
... | ... | @@ -171,12 +195,21 @@ function AlarmService($http, $q, $interval, $filter) { |
171 | 195 | pageLink = { |
172 | 196 | limit: alarmsQuery.limit |
173 | 197 | }; |
174 | - } else { | |
198 | + } else if (alarmsQuery.interval) { | |
175 | 199 | pageLink = { |
176 | 200 | limit: 100, |
177 | 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 | 213 | fetchAlarms(alarmsQuery, pageLink, deferred); |
181 | 214 | return deferred.promise; |
182 | 215 | } |
... | ... | @@ -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 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | |
17 | -const varsRegex = /\$\{([^\}]*)\}/g; | |
18 | - | |
19 | 17 | export default class AliasController { |
20 | 18 | |
21 | 19 | constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) { |
... | ... | @@ -113,14 +111,14 @@ export default class AliasController { |
113 | 111 | } |
114 | 112 | } |
115 | 113 | |
116 | - resolveDatasource(datasource) { | |
114 | + resolveDatasource(datasource, isSingle) { | |
117 | 115 | var deferred = this.$q.defer(); |
118 | 116 | if (datasource.type === this.types.datasourceType.entity) { |
119 | 117 | if (datasource.entityAliasId) { |
120 | 118 | this.getAliasInfo(datasource.entityAliasId).then( |
121 | 119 | function success(aliasInfo) { |
122 | 120 | datasource.aliasName = aliasInfo.alias; |
123 | - if (aliasInfo.resolveMultiple) { | |
121 | + if (aliasInfo.resolveMultiple && !isSingle) { | |
124 | 122 | var newDatasource; |
125 | 123 | var resolvedEntities = aliasInfo.resolvedEntities; |
126 | 124 | if (resolvedEntities && resolvedEntities.length) { |
... | ... | @@ -178,30 +176,44 @@ export default class AliasController { |
178 | 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 | 208 | resolveDatasources(datasources) { |
182 | 209 | |
210 | + var aliasCtrl = this; | |
211 | + | |
183 | 212 | function updateDataKeyLabel(dataKey, datasource) { |
184 | 213 | if (!dataKey.pattern) { |
185 | 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 | 219 | function updateDatasourceKeyLabels(datasource) { |
... | ... | @@ -213,7 +225,7 @@ export default class AliasController { |
213 | 225 | var deferred = this.$q.defer(); |
214 | 226 | var newDatasources = angular.copy(datasources); |
215 | 227 | var datasorceResolveTasks = []; |
216 | - var aliasCtrl = this; | |
228 | + | |
217 | 229 | newDatasources.forEach(function (datasource) { |
218 | 230 | var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource); |
219 | 231 | datasorceResolveTasks.push(resolveDatasourceTask); | ... | ... |
... | ... | @@ -64,6 +64,39 @@ export default class Subscription { |
64 | 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 | 100 | } else { |
68 | 101 | this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; |
69 | 102 | this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; |
... | ... | @@ -132,6 +165,44 @@ export default class Subscription { |
132 | 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 | 206 | initDataSubscription() { |
136 | 207 | var deferred = this.ctx.$q.defer(); |
137 | 208 | if (!this.ctx.aliasController) { |
... | ... | @@ -393,6 +464,8 @@ export default class Subscription { |
393 | 464 | onAliasesChanged(aliasIds) { |
394 | 465 | if (this.type === this.ctx.types.widgetType.rpc.value) { |
395 | 466 | return this.checkRpcTarget(aliasIds); |
467 | + } else if (this.type === this.ctx.types.widgetType.alarm.value) { | |
468 | + return this.checkAlarmSource(aliasIds); | |
396 | 469 | } else { |
397 | 470 | return this.checkSubscriptions(aliasIds); |
398 | 471 | } |
... | ... | @@ -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 | 601 | updateLegend(dataIndex, data, apply) { |
520 | 602 | var dataKey = this.legendData.keys[dataIndex].dataKey; |
521 | 603 | var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; |
... | ... | @@ -540,62 +622,104 @@ export default class Subscription { |
540 | 622 | if (this.type === this.ctx.types.widgetType.rpc.value) { |
541 | 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 | 680 | this.notifyDataLoading(); |
544 | - if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { | |
681 | + if (this.timeWindowConfig) { | |
545 | 682 | this.updateRealtimeSubscription(); |
546 | 683 | if (this.subscriptionTimewindow.fixedWindow) { |
547 | 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 | 705 | unsubscribe() { |
593 | 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 | 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 | 742 | checkSubscriptions(aliasIds) { |
611 | 743 | var subscriptionsChanged = false; |
612 | 744 | for (var i = 0; i < this.datasourceListeners.length; i++) { | ... | ... |
... | ... | @@ -19,6 +19,7 @@ import tinycolor from 'tinycolor2'; |
19 | 19 | |
20 | 20 | import thingsboardLedLight from '../components/led-light.directive'; |
21 | 21 | import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; |
22 | +import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; | |
22 | 23 | |
23 | 24 | import TbFlot from '../widget/lib/flot-widget'; |
24 | 25 | import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; |
... | ... | @@ -33,7 +34,7 @@ import thingsboardTypes from '../common/types.constant'; |
33 | 34 | import thingsboardUtils from '../common/utils.service'; |
34 | 35 | |
35 | 36 | export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, |
36 | - thingsboardTypes, thingsboardUtils]) | |
37 | + thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils]) | |
37 | 38 | .factory('widgetService', WidgetService) |
38 | 39 | .name; |
39 | 40 | ... | ... |
... | ... | @@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', []) |
59 | 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 | 109 | alarmStatus: { |
63 | 110 | activeUnack: "ACTIVE_UNACK", |
64 | 111 | activeAck: "ACTIVE_ACK", |
... | ... | @@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', []) |
75 | 122 | alarmSeverity: { |
76 | 123 | "CRITICAL": { |
77 | 124 | name: "alarm.severity-critical", |
78 | - class: "tb-critical" | |
125 | + class: "tb-critical", | |
126 | + color: "red" | |
79 | 127 | }, |
80 | 128 | "MAJOR": { |
81 | 129 | name: "alarm.severity-major", |
82 | - class: "tb-major" | |
130 | + class: "tb-major", | |
131 | + color: "orange" | |
83 | 132 | }, |
84 | 133 | "MINOR": { |
85 | 134 | name: "alarm.severity-minor", |
86 | - class: "tb-minor" | |
135 | + class: "tb-minor", | |
136 | + color: "#ffca3d" | |
87 | 137 | }, |
88 | 138 | "WARNING": { |
89 | 139 | name: "alarm.severity-warning", |
90 | - class: "tb-warning" | |
140 | + class: "tb-warning", | |
141 | + color: "#abab00" | |
91 | 142 | }, |
92 | 143 | "INDETERMINATE": { |
93 | 144 | name: "alarm.severity-indeterminate", |
94 | - class: "tb-indeterminate" | |
145 | + class: "tb-indeterminate", | |
146 | + color: "green" | |
95 | 147 | } |
96 | 148 | }, |
97 | 149 | aliasFilterType: { |
... | ... | @@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', []) |
153 | 205 | dataKeyType: { |
154 | 206 | timeseries: "timeseries", |
155 | 207 | attribute: "attribute", |
156 | - function: "function" | |
208 | + function: "function", | |
209 | + alarm: "alarm" | |
157 | 210 | }, |
158 | 211 | componentType: { |
159 | 212 | filter: "FILTER", |
... | ... | @@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', []) |
319 | 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 | 383 | static: { |
323 | 384 | value: "static", |
324 | 385 | name: "widget.static", | ... | ... |
... | ... | @@ -21,8 +21,10 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) |
21 | 21 | .factory('utils', Utils) |
22 | 22 | .name; |
23 | 23 | |
24 | +const varsRegex = /\$\{([^\}]*)\}/g; | |
25 | + | |
24 | 26 | /*@ngInject*/ |
25 | -function Utils($mdColorPalette, $rootScope, $window, types) { | |
27 | +function Utils($mdColorPalette, $rootScope, $window, $translate, types) { | |
26 | 28 | |
27 | 29 | var predefinedFunctions = {}, |
28 | 30 | predefinedFunctionsList = [], |
... | ... | @@ -93,9 +95,32 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
93 | 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 | 120 | var service = { |
97 | 121 | getDefaultDatasource: getDefaultDatasource, |
98 | 122 | getDefaultDatasourceJson: getDefaultDatasourceJson, |
123 | + getDefaultAlarmDataKeys: getDefaultAlarmDataKeys, | |
99 | 124 | getMaterialColor: getMaterialColor, |
100 | 125 | getPredefinedFunctionBody: getPredefinedFunctionBody, |
101 | 126 | getPredefinedFunctionsList: getPredefinedFunctionsList, |
... | ... | @@ -109,7 +134,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
109 | 134 | cleanCopy: cleanCopy, |
110 | 135 | isLocalUrl: isLocalUrl, |
111 | 136 | validateDatasources: validateDatasources, |
112 | - createKey: createKey | |
137 | + createKey: createKey, | |
138 | + createLabelFromDatasource: createLabelFromDatasource | |
113 | 139 | } |
114 | 140 | |
115 | 141 | return service; |
... | ... | @@ -212,6 +238,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
212 | 238 | return angular.toJson(getDefaultDatasource(dataKeySchema)); |
213 | 239 | } |
214 | 240 | |
241 | + function getDefaultAlarmDataKeys() { | |
242 | + return angular.copy(defaultAlarmDataKeys); | |
243 | + } | |
244 | + | |
215 | 245 | function isDescriptorSchemaNotEmpty(descriptor) { |
216 | 246 | if (descriptor && descriptor.schema && descriptor.schema.properties) { |
217 | 247 | for(var prop in descriptor.schema.properties) { |
... | ... | @@ -357,4 +387,24 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
357 | 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 | 181 | vm.dropWidgetShadow = dropWidgetShadow; |
182 | 182 | vm.enableWidgetFullscreen = enableWidgetFullscreen; |
183 | 183 | vm.hasTimewindow = hasTimewindow; |
184 | + vm.hasAggregation = hasAggregation; | |
184 | 185 | vm.editWidget = editWidget; |
185 | 186 | vm.exportWidget = exportWidget; |
186 | 187 | vm.removeWidget = removeWidget; |
... | ... | @@ -771,7 +772,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
771 | 772 | } |
772 | 773 | |
773 | 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 | 776 | return angular.isDefined(widget.config.useDashboardTimewindow) ? |
776 | 777 | !widget.config.useDashboardTimewindow : false; |
777 | 778 | } else { |
... | ... | @@ -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 | 787 | function adoptMaxRows() { |
783 | 788 | if (vm.widgets) { |
784 | 789 | var maxRows = vm.gridsterOpts.maxRows; | ... | ... |
... | ... | @@ -47,7 +47,7 @@ |
47 | 47 | padding: vm.widgetPadding(widget)}"> |
48 | 48 | <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> |
49 | 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 | 51 | </div> |
52 | 52 | <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()"> |
53 | 53 | <md-button id="expand-button" | ... | ... |
... | ... | @@ -45,12 +45,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc |
45 | 45 | scope.ngModelCtrl = ngModelCtrl; |
46 | 46 | scope.types = types; |
47 | 47 | |
48 | + scope.alarmFields = []; | |
49 | + for (var alarmField in types.alarmFields) { | |
50 | + scope.alarmFields.push(alarmField); | |
51 | + } | |
52 | + | |
48 | 53 | scope.selectedTimeseriesDataKey = null; |
49 | 54 | scope.timeseriesDataKeySearchText = null; |
50 | 55 | |
51 | 56 | scope.selectedAttributeDataKey = null; |
52 | 57 | scope.attributeDataKeySearchText = null; |
53 | 58 | |
59 | + scope.selectedAlarmDataKey = null; | |
60 | + scope.alarmDataKeySearchText = null; | |
61 | + | |
54 | 62 | scope.updateValidity = function () { |
55 | 63 | if (ngModelCtrl.$viewValue) { |
56 | 64 | var value = ngModelCtrl.$viewValue; |
... | ... | @@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc |
81 | 89 | }); |
82 | 90 | |
83 | 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 | 93 | }, true); |
92 | 94 | |
93 | 95 | scope.$watch('attributeDataKeys', function () { |
96 | + updateDataKeys(); | |
97 | + }, true); | |
98 | + | |
99 | + scope.$watch('alarmDataKeys', function () { | |
100 | + updateDataKeys(); | |
101 | + }, true); | |
102 | + | |
103 | + function updateDataKeys() { | |
94 | 104 | if (ngModelCtrl.$viewValue) { |
95 | 105 | var dataKeys = []; |
96 | 106 | dataKeys = dataKeys.concat(scope.timeseriesDataKeys); |
97 | 107 | dataKeys = dataKeys.concat(scope.attributeDataKeys); |
108 | + dataKeys = dataKeys.concat(scope.alarmDataKeys); | |
98 | 109 | ngModelCtrl.$viewValue.dataKeys = dataKeys; |
99 | 110 | scope.updateValidity(); |
100 | 111 | } |
101 | - }, true); | |
112 | + } | |
102 | 113 | |
103 | 114 | ngModelCtrl.$render = function () { |
104 | 115 | if (ngModelCtrl.$viewValue) { |
... | ... | @@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc |
111 | 122 | } |
112 | 123 | var timeseriesDataKeys = []; |
113 | 124 | var attributeDataKeys = []; |
125 | + var alarmDataKeys = []; | |
114 | 126 | for (var d in ngModelCtrl.$viewValue.dataKeys) { |
115 | 127 | var dataKey = ngModelCtrl.$viewValue.dataKeys[d]; |
116 | 128 | if (dataKey.type === types.dataKeyType.timeseries) { |
117 | 129 | timeseriesDataKeys.push(dataKey); |
118 | 130 | } else if (dataKey.type === types.dataKeyType.attribute) { |
119 | 131 | attributeDataKeys.push(dataKey); |
132 | + } else if (dataKey.type === types.dataKeyType.alarm) { | |
133 | + alarmDataKeys.push(dataKey); | |
120 | 134 | } |
121 | 135 | } |
122 | 136 | scope.timeseriesDataKeys = timeseriesDataKeys; |
123 | 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 | 150 | if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') { |
136 | 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 | 158 | scope.transformTimeseriesDataKeyChip = function (chip) { |
... | ... | @@ -145,6 +163,10 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc |
145 | 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 | 170 | scope.showColorPicker = function (event, dataKey) { |
149 | 171 | $mdColorPicker.show({ |
150 | 172 | value: dataKey.color, |
... | ... | @@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc |
196 | 218 | scope.timeseriesDataKeys[index] = dataKey; |
197 | 219 | } else if (dataKey.type === types.dataKeyType.attribute) { |
198 | 220 | scope.attributeDataKeys[index] = dataKey; |
221 | + } else if (dataKey.type === types.dataKeyType.alarm) { | |
222 | + scope.alarmDataKeys[index] = dataKey; | |
199 | 223 | } |
200 | 224 | ngModelCtrl.$setDirty(); |
201 | 225 | }, function () { |
... | ... | @@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc |
203 | 227 | }; |
204 | 228 | |
205 | 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 | 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 | 257 | scope.createKey = function (event, chipsId) { |
221 | 258 | var chipsChild = $(chipsId, element)[0].firstElementChild; |
222 | 259 | var el = angular.element(chipsChild); | ... | ... |
... | ... | @@ -24,7 +24,7 @@ |
24 | 24 | </tb-entity-alias-select> |
25 | 25 | <section flex layout='column'> |
26 | 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 | 28 | id="timeseries_datakey_chips" |
29 | 29 | ng-required="true" |
30 | 30 | ng-model="timeseriesDataKeys" md-autocomplete-snap |
... | ... | @@ -128,10 +128,60 @@ |
128 | 128 | </div> |
129 | 129 | </md-chip-template> |
130 | 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:'...'}}" }'>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 | 180 | </section> |
132 | 181 | <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> |
133 | 182 | <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div> |
134 | 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 | 185 | </div> |
136 | 186 | </section> |
137 | 187 | </section> | ... | ... |
... | ... | @@ -43,18 +43,27 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
43 | 43 | element.html(template); |
44 | 44 | |
45 | 45 | scope.ngModelCtrl = ngModelCtrl; |
46 | + scope.types = types; | |
47 | + | |
46 | 48 | scope.functionTypes = utils.getPredefinedFunctionsList(); |
49 | + scope.alarmFields = []; | |
50 | + for (var alarmField in types.alarmFields) { | |
51 | + scope.alarmFields.push(alarmField); | |
52 | + } | |
47 | 53 | |
48 | 54 | scope.selectedDataKey = null; |
49 | 55 | scope.dataKeySearchText = null; |
50 | 56 | |
57 | + scope.selectedAlarmDataKey = null; | |
58 | + scope.alarmDataKeySearchText = null; | |
59 | + | |
51 | 60 | scope.updateValidity = function () { |
52 | 61 | if (ngModelCtrl.$viewValue) { |
53 | 62 | var value = ngModelCtrl.$viewValue; |
54 | 63 | var dataValid = angular.isDefined(value) && value != null; |
55 | 64 | ngModelCtrl.$setValidity('deviceData', dataValid); |
56 | 65 | if (dataValid) { |
57 | - ngModelCtrl.$setValidity('funcTypes', | |
66 | + ngModelCtrl.$setValidity('datasourceKeys', | |
58 | 67 | angular.isDefined(value.dataKeys) && |
59 | 68 | value.dataKeys != null && |
60 | 69 | value.dataKeys.length > 0); |
... | ... | @@ -63,13 +72,22 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
63 | 72 | }; |
64 | 73 | |
65 | 74 | scope.$watch('funcDataKeys', function () { |
75 | + updateDataKeys(); | |
76 | + }, true); | |
77 | + | |
78 | + scope.$watch('alarmDataKeys', function () { | |
79 | + updateDataKeys(); | |
80 | + }, true); | |
81 | + | |
82 | + function updateDataKeys() { | |
66 | 83 | if (ngModelCtrl.$viewValue) { |
67 | 84 | var dataKeys = []; |
68 | 85 | dataKeys = dataKeys.concat(scope.funcDataKeys); |
86 | + dataKeys = dataKeys.concat(scope.alarmDataKeys); | |
69 | 87 | ngModelCtrl.$viewValue.dataKeys = dataKeys; |
70 | 88 | scope.updateValidity(); |
71 | 89 | } |
72 | - }, true); | |
90 | + } | |
73 | 91 | |
74 | 92 | scope.$watch('datasourceName', function () { |
75 | 93 | if (ngModelCtrl.$viewValue) { |
... | ... | @@ -81,18 +99,31 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
81 | 99 | ngModelCtrl.$render = function () { |
82 | 100 | if (ngModelCtrl.$viewValue) { |
83 | 101 | var funcDataKeys = []; |
102 | + var alarmDataKeys = []; | |
84 | 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 | 113 | scope.funcDataKeys = funcDataKeys; |
114 | + scope.alarmDataKeys = alarmDataKeys; | |
88 | 115 | scope.datasourceName = ngModelCtrl.$viewValue.name; |
89 | 116 | } |
90 | 117 | }; |
91 | 118 | |
92 | - scope.transformDataKeyChip = function (chip) { | |
119 | + scope.transformFuncDataKeyChip = function (chip) { | |
93 | 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 | 127 | scope.showColorPicker = function (event, dataKey) { |
97 | 128 | $mdColorPicker.show({ |
98 | 129 | value: dataKey.color, |
... | ... | @@ -129,7 +160,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
129 | 160 | dataKey: angular.copy(dataKey), |
130 | 161 | dataKeySettingsSchema: scope.datakeySettingsSchema, |
131 | 162 | entityAlias: null, |
132 | - entityAliases: null | |
163 | + aliasController: null | |
133 | 164 | }, |
134 | 165 | parent: angular.element($document[0].body), |
135 | 166 | fullscreen: true, |
... | ... | @@ -140,7 +171,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
140 | 171 | w.triggerHandler('resize'); |
141 | 172 | } |
142 | 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 | 179 | ngModelCtrl.$setDirty(); |
145 | 180 | }, function () { |
146 | 181 | }); |
... | ... | @@ -151,8 +186,9 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
151 | 186 | } |
152 | 187 | |
153 | 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 | 192 | return dataKeys; |
157 | 193 | }; |
158 | 194 | |
... | ... | @@ -180,6 +216,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, |
180 | 216 | restrict: "E", |
181 | 217 | require: "^ngModel", |
182 | 218 | scope: { |
219 | + widgetType: '=', | |
183 | 220 | generateDataKey: '&', |
184 | 221 | datakeySettingsSchema: '=' |
185 | 222 | }, | ... | ... |
... | ... | @@ -17,18 +17,19 @@ |
17 | 17 | --> |
18 | 18 | <section class="tb-datasource-func" flex layout='column' |
19 | 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 | 22 | <input name="datasourceName" |
22 | 23 | placeholder="{{ 'datasource.name' | translate }}" |
23 | 24 | ng-model="datasourceName" |
24 | 25 | aria-label="{{ 'datasource.name' | translate }}"> |
25 | 26 | </md-input-container> |
26 | 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 | 29 | id="function_datakey_chips" |
29 | 30 | ng-required="true" |
30 | 31 | ng-model="funcDataKeys" md-autocomplete-snap |
31 | - md-transform-chip="transformDataKeyChip($chip)" | |
32 | + md-transform-chip="transformFuncDataKeyChip($chip)" | |
32 | 33 | md-require-match="false"> |
33 | 34 | <md-autocomplete |
34 | 35 | md-no-cache="false" |
... | ... | @@ -75,8 +76,58 @@ |
75 | 76 | </div> |
76 | 77 | </md-chip-template> |
77 | 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:'...'}}" }'>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 | 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 | 131 | </div> |
81 | 132 | </section> |
82 | 133 | </section> | ... | ... |
... | ... | @@ -30,7 +30,7 @@ export default angular.module('thingsboard.directives.datasource', [thingsboardT |
30 | 30 | .name; |
31 | 31 | |
32 | 32 | /*@ngInject*/ |
33 | -function Datasource($compile, $templateCache, types) { | |
33 | +function Datasource($compile, $templateCache, utils, types) { | |
34 | 34 | |
35 | 35 | var linker = function (scope, element, attrs, ngModelCtrl) { |
36 | 36 | |
... | ... | @@ -53,8 +53,12 @@ function Datasource($compile, $templateCache, types) { |
53 | 53 | } |
54 | 54 | |
55 | 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 | 67 | }, true); |
64 | 68 | |
65 | 69 | ngModelCtrl.$render = function () { |
66 | - scope.model = {}; | |
67 | 70 | if (ngModelCtrl.$viewValue) { |
68 | 71 | scope.model = ngModelCtrl.$viewValue; |
72 | + } else { | |
73 | + scope.model = {}; | |
69 | 74 | } |
70 | 75 | }; |
71 | 76 | ... | ... |
... | ... | @@ -29,6 +29,7 @@ |
29 | 29 | ng-model="model" |
30 | 30 | datakey-settings-schema="datakeySettingsSchema" |
31 | 31 | ng-required="model.type === types.datasourceType.function" |
32 | + widget-type="widgetType" | |
32 | 33 | generate-data-key="generateDataKey({chip: chip, type: type})"> |
33 | 34 | </tb-datasource-func> |
34 | 35 | <tb-datasource-entity flex | ... | ... |
... | ... | @@ -64,7 +64,7 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM |
64 | 64 | |
65 | 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 | 69 | scope.isToolbar = angular.isDefined(attrs.isToolbar); |
70 | 70 | ... | ... |
... | ... | @@ -43,7 +43,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar |
43 | 43 | .name; |
44 | 44 | |
45 | 45 | /*@ngInject*/ |
46 | -function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, utils) { | |
46 | +function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout, types, utils) { | |
47 | 47 | |
48 | 48 | var linker = function (scope, element, attrs, ngModelCtrl) { |
49 | 49 | |
... | ... | @@ -87,6 +87,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
87 | 87 | value: null |
88 | 88 | } |
89 | 89 | |
90 | + scope.alarmSource = { | |
91 | + value: null | |
92 | + } | |
93 | + | |
90 | 94 | ngModelCtrl.$render = function () { |
91 | 95 | if (ngModelCtrl.$viewValue) { |
92 | 96 | var config = ngModelCtrl.$viewValue.config; |
... | ... | @@ -113,7 +117,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
113 | 117 | scope.showLegend = angular.isDefined(config.showLegend) ? |
114 | 118 | config.showLegend : scope.widgetType === types.widgetType.timeseries.value; |
115 | 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 | 123 | && scope.isDataEnabled) { |
118 | 124 | if (scope.datasources) { |
119 | 125 | scope.datasources.splice(0, scope.datasources.length); |
... | ... | @@ -137,6 +143,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
137 | 143 | } else { |
138 | 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 | 154 | scope.settings = config.settings; |
... | ... | @@ -175,6 +187,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
175 | 187 | if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { |
176 | 188 | valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; |
177 | 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 | 193 | } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { |
179 | 194 | valid = config && config.datasources && config.datasources.length > 0; |
180 | 195 | ngModelCtrl.$setValidity('datasources', valid); |
... | ... | @@ -253,7 +268,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
253 | 268 | }, true); |
254 | 269 | |
255 | 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 | 274 | && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { |
258 | 275 | var value = ngModelCtrl.$viewValue; |
259 | 276 | var config = value.config; |
... | ... | @@ -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 | 320 | scope.addDatasource = function () { |
290 | 321 | var newDatasource; |
291 | 322 | if (scope.functionsOnly) { |
... | ... | @@ -320,10 +351,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
320 | 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 | 363 | var result = { |
324 | 364 | name: chip, |
325 | 365 | type: type, |
326 | - label: scope.genNextLabel(chip), | |
366 | + label: label, | |
327 | 367 | color: scope.genNextColor(), |
328 | 368 | settings: {}, |
329 | 369 | _hash: Math.random() |
... | ... | @@ -351,15 +391,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
351 | 391 | var matches = false; |
352 | 392 | do { |
353 | 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 | 414 | scope.genNextColor = function () { |
372 | 415 | var i = 0; |
373 | 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 | 426 | return utils.getMaterialColor(i); | ... | ... |
... | ... | @@ -20,18 +20,22 @@ |
20 | 20 | <md-tab label="{{ 'widget-config.data' | translate }}" |
21 | 21 | ng-show="widgetType !== types.widgetType.static.value"> |
22 | 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 | 24 | layout-gt-sm='row' layout-align-gt-sm="start center"> |
25 | 25 | <md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}" |
26 | 26 | ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }} |
27 | 27 | </md-checkbox> |
28 | 28 | <section flex layout="row" layout-align="start center" style="margin-bottom: 16px;"> |
29 | 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 | 32 | </section> |
32 | 33 | </div> |
33 | 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 | 39 | <v-pane id="datasources-pane" expanded="true"> |
36 | 40 | <v-pane-header> |
37 | 41 | {{ 'widget-config.datasources' | translate }} |
... | ... | @@ -112,6 +116,24 @@ |
112 | 116 | </v-pane-content> |
113 | 117 | </v-pane> |
114 | 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 | 137 | </md-content> |
116 | 138 | </md-tab> |
117 | 139 | <md-tab label="{{ 'widget-config.settings' | translate }}"> | ... | ... |
... | ... | @@ -21,7 +21,7 @@ import Subscription from '../api/subscription'; |
21 | 21 | |
22 | 22 | /*@ngInject*/ |
23 | 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 | 25 | dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { |
26 | 26 | |
27 | 27 | var vm = this; |
... | ... | @@ -44,7 +44,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
44 | 44 | |
45 | 45 | var widgetContext = { |
46 | 46 | inited: false, |
47 | - $scope: $scope, | |
48 | 47 | $container: null, |
49 | 48 | $containerParent: null, |
50 | 49 | width: 0, |
... | ... | @@ -113,6 +112,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
113 | 112 | timeService: timeService, |
114 | 113 | deviceService: deviceService, |
115 | 114 | datasourceService: datasourceService, |
115 | + alarmService: alarmService, | |
116 | 116 | utils: utils, |
117 | 117 | widgetUtils: widgetContext.utils, |
118 | 118 | dashboardTimewindowApi: dashboardTimewindowApi, |
... | ... | @@ -285,9 +285,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
285 | 285 | var deferred = $q.defer(); |
286 | 286 | if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { |
287 | 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 | 296 | defaultComponentsOptions(options); |
292 | 297 | |
293 | 298 | createSubscription(options).then( |
... | ... | @@ -320,7 +325,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
320 | 325 | $scope.executingRpcRequest = subscription.executingRpcRequest; |
321 | 326 | }, |
322 | 327 | onRpcSuccess: function(subscription) { |
323 | - $scope.executingRpcRequest = subscription.executingRpcRequest; | |
328 | + $scope.executingRpcRequest = subscription.executingRpcRequest; | |
324 | 329 | $scope.rpcErrorText = subscription.rpcErrorText; |
325 | 330 | $scope.rpcRejection = subscription.rpcRejection; |
326 | 331 | }, |
... | ... | @@ -436,7 +441,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
436 | 441 | widgetContext.$container = $('#container', containerElement); |
437 | 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 | 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 | 456 | function destroyWidgetElement() { |
445 | 457 | removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef |
446 | 458 | $element.html(''); |
459 | + if (widgetContext.$scope) { | |
460 | + widgetContext.$scope.$destroy(); | |
461 | + } | |
447 | 462 | widgetContext.$container = null; |
448 | 463 | widgetContext.$containerParent = null; |
449 | 464 | } | ... | ... |
... | ... | @@ -76,6 +76,10 @@ export default function AddWidgetController($scope, widgetService, entityService |
76 | 76 | link = 'widgetsConfigRpc'; |
77 | 77 | break; |
78 | 78 | } |
79 | + case types.widgetType.alarm.value: { | |
80 | + link = 'widgetsConfigAlarm'; | |
81 | + break; | |
82 | + } | |
79 | 83 | case types.widgetType.static.value: { |
80 | 84 | link = 'widgetsConfigStatic'; |
81 | 85 | break; | ... | ... |
... | ... | @@ -47,6 +47,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget |
47 | 47 | vm.latestWidgetTypes = []; |
48 | 48 | vm.timeseriesWidgetTypes = []; |
49 | 49 | vm.rpcWidgetTypes = []; |
50 | + vm.alarmWidgetTypes = []; | |
50 | 51 | vm.staticWidgetTypes = []; |
51 | 52 | vm.widgetEditMode = $state.$current.data.widgetEditMode; |
52 | 53 | vm.iframeMode = $rootScope.iframeMode; |
... | ... | @@ -263,6 +264,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget |
263 | 264 | vm.latestWidgetTypes = []; |
264 | 265 | vm.timeseriesWidgetTypes = []; |
265 | 266 | vm.rpcWidgetTypes = []; |
267 | + vm.alarmWidgetTypes = []; | |
266 | 268 | vm.staticWidgetTypes = []; |
267 | 269 | if (vm.widgetsBundle) { |
268 | 270 | var bundleAlias = vm.widgetsBundle.alias; |
... | ... | @@ -308,6 +310,8 @@ export default function DashboardController(types, utils, dashboardUtils, widget |
308 | 310 | vm.latestWidgetTypes.push(widget); |
309 | 311 | } else if (widgetTypeInfo.type === types.widgetType.rpc.value) { |
310 | 312 | vm.rpcWidgetTypes.push(widget); |
313 | + } else if (widgetTypeInfo.type === types.widgetType.alarm.value) { | |
314 | + vm.alarmWidgetTypes.push(widget); | |
311 | 315 | } else if (widgetTypeInfo.type === types.widgetType.static.value) { |
312 | 316 | vm.staticWidgetTypes.push(widget); |
313 | 317 | } |
... | ... | @@ -358,21 +362,6 @@ export default function DashboardController(types, utils, dashboardUtils, widget |
358 | 362 | vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; |
359 | 363 | vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils, |
360 | 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 | 365 | }, function fail() { |
377 | 366 | vm.configurationError = true; |
378 | 367 | }); |
... | ... | @@ -744,6 +733,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget |
744 | 733 | link = 'widgetsConfigRpc'; |
745 | 734 | break; |
746 | 735 | } |
736 | + case types.widgetType.alarm.value: { | |
737 | + link = 'widgetsConfigAlarm'; | |
738 | + break; | |
739 | + } | |
747 | 740 | case types.widgetType.static.value: { |
748 | 741 | link = 'widgetsConfigStatic'; |
749 | 742 | break; |
... | ... | @@ -851,6 +844,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget |
851 | 844 | vm.timeseriesWidgetTypes = []; |
852 | 845 | vm.latestWidgetTypes = []; |
853 | 846 | vm.rpcWidgetTypes = []; |
847 | + vm.alarmWidgetTypes = []; | |
854 | 848 | vm.staticWidgetTypes = []; |
855 | 849 | } |
856 | 850 | ... | ... |
... | ... | @@ -52,7 +52,7 @@ |
52 | 52 | <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()" |
53 | 53 | is-toolbar |
54 | 54 | direction="left" |
55 | - tooltip-direction="bottom" aggregation | |
55 | + tooltip-direction="bottom" aggregation="true" | |
56 | 56 | ng-model="vm.dashboardCtx.dashboardTimewindow"> |
57 | 57 | </tb-timewindow> |
58 | 58 | <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()" |
... | ... | @@ -179,6 +179,7 @@ |
179 | 179 | <tb-edit-widget |
180 | 180 | dashboard="vm.dashboard" |
181 | 181 | alias-controller="vm.dashboardCtx.aliasController" |
182 | + widget-edit-mode="vm.widgetEditMode" | |
182 | 183 | widget="vm.editingWidget" |
183 | 184 | widget-layout="vm.editingWidgetLayout" |
184 | 185 | the-form="vm.widgetForm"> |
... | ... | @@ -205,7 +206,8 @@ |
205 | 206 | </header-pane> |
206 | 207 | <div ng-if="vm.isAddingWidget"> |
207 | 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 | 211 | flex |
210 | 212 | class="tb-absolute-fill" md-border-bottom> |
211 | 213 | <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> |
... | ... | @@ -238,6 +240,16 @@ |
238 | 240 | on-widget-clicked="vm.addWidgetFromType(event, widget)"> |
239 | 241 | </tb-dashboard> |
240 | 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 | 253 | <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}"> |
242 | 254 | <tb-dashboard |
243 | 255 | widgets="vm.staticWidgetTypes" |
... | ... | @@ -250,7 +262,8 @@ |
250 | 262 | </md-tab> |
251 | 263 | </md-tabs> |
252 | 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 | 267 | layout-align="center center" |
255 | 268 | style="text-transform: uppercase; display: flex;" |
256 | 269 | class="md-headline tb-absolute-fill">widgets-bundle.empty</span> | ... | ... |
... | ... | @@ -22,7 +22,7 @@ |
22 | 22 | widget-settings-schema="settingsSchema" |
23 | 23 | datakey-settings-schema="dataKeySettingsSchema" |
24 | 24 | alias-controller="aliasController" |
25 | - functions-only="functionsOnly" | |
25 | + functions-only="widgetEditMode" | |
26 | 26 | fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" |
27 | 27 | on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" |
28 | 28 | the-form="theForm"></tb-widget-config> | ... | ... |
... | ... | @@ -87,6 +87,7 @@ export default angular.module('thingsboard.help', []) |
87 | 87 | widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries", |
88 | 88 | widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest", |
89 | 89 | widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", |
90 | + widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm", | |
90 | 91 | widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static", |
91 | 92 | }, |
92 | 93 | getPluginLink: function(plugin) { | ... | ... |
... | ... | @@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', []) |
131 | 131 | "type": "Type", |
132 | 132 | "severity": "Severity", |
133 | 133 | "originator": "Originator", |
134 | + "originator-type": "Originator type", | |
134 | 135 | "details": "Details", |
135 | 136 | "status": "Status", |
136 | 137 | "alarm-details": "Alarm details", |
... | ... | @@ -144,7 +145,10 @@ export default angular.module('thingsboard.locale', []) |
144 | 145 | "severity-warning": "Warning", |
145 | 146 | "severity-indeterminate": "Indeterminate", |
146 | 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 | 153 | "alias": { |
150 | 154 | "add": "Add alias", |
... | ... | @@ -486,8 +490,10 @@ export default angular.module('thingsboard.locale', []) |
486 | 490 | "configuration": "Data key configuration", |
487 | 491 | "timeseries": "Timeseries", |
488 | 492 | "attributes": "Attributes", |
493 | + "alarm": "Alarm fields", | |
489 | 494 | "timeseries-required": "Entity timeseries are required.", |
490 | 495 | "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", |
496 | + "alarm-fields-required": "Alarm fields are required.", | |
491 | 497 | "function-types": "Function types", |
492 | 498 | "function-types-required": "Function types are required." |
493 | 499 | }, |
... | ... | @@ -1046,6 +1052,7 @@ export default angular.module('thingsboard.locale', []) |
1046 | 1052 | "timeseries": "Time series", |
1047 | 1053 | "latest-values": "Latest values", |
1048 | 1054 | "rpc": "Control widget", |
1055 | + "alarm": "Alarm widget", | |
1049 | 1056 | "static": "Static widget", |
1050 | 1057 | "select-widget-type": "Select widget type", |
1051 | 1058 | "missing-widget-title-error": "Widget title must be specified!", |
... | ... | @@ -1133,7 +1140,8 @@ export default angular.module('thingsboard.locale', []) |
1133 | 1140 | "datasource-parameters": "Parameters", |
1134 | 1141 | "remove-datasource": "Remove datasource", |
1135 | 1142 | "add-datasource": "Add datasource", |
1136 | - "target-device": "Target device" | |
1143 | + "target-device": "Target device", | |
1144 | + "alarm-source": "Alarm source" | |
1137 | 1145 | }, |
1138 | 1146 | "widget-type": { |
1139 | 1147 | "import": "Import widget type", | ... | ... |
ui/src/app/widget/lib/alarms-table-widget.js
0 → 100644
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 | +} | |
\ No newline at end of file | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2017 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +.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> </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> </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 | 54 | <span translate>{{vm.types.widgetType.rpc.name}}</span> |
55 | 55 | </md-button> |
56 | 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 | 64 | ng-click="vm.typeSelected(vm.types.widgetType.static.value)"> |
58 | 65 | <md-icon class="material-icons tb-md-96" |
59 | 66 | aria-label="{{ vm.types.widgetType.static.name | translate }}">font_download | ... | ... |
... | ... | @@ -329,10 +329,14 @@ export default function WidgetEditorController(widgetService, userService, types |
329 | 329 | $scope.$watch('vm.widget.type', function (newVal, oldVal) { |
330 | 330 | if (!angular.equals(newVal, oldVal)) { |
331 | 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 | 334 | if (config.targetDeviceAliases) { |
334 | 335 | delete config.targetDeviceAliases; |
335 | 336 | } |
337 | + if (config.alarmSource) { | |
338 | + delete config.alarmSource; | |
339 | + } | |
336 | 340 | if (!config.datasources) { |
337 | 341 | config.datasources = []; |
338 | 342 | } |
... | ... | @@ -346,22 +350,38 @@ export default function WidgetEditorController(widgetService, userService, types |
346 | 350 | for (var i = 0; i < config.datasources.length; i++) { |
347 | 351 | var datasource = config.datasources[i]; |
348 | 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 | 355 | if (config.datasources) { |
357 | 356 | delete config.datasources; |
358 | 357 | } |
358 | + if (config.alarmSource) { | |
359 | + delete config.alarmSource; | |
360 | + } | |
359 | 361 | if (config.timewindow) { |
360 | 362 | delete config.timewindow; |
361 | 363 | } |
362 | 364 | if (!config.targetDeviceAliases) { |
363 | 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 | 386 | vm.widget.defaultConfig = angular.toJson(config); |
367 | 387 | } | ... | ... |