Commit eda21c94efabb9f206273e5a4f114db898c4458b
1 parent
531f4245
TB-63: Alarms widget implementation
Showing
22 changed files
with
388 additions
and
73 deletions
... | ... | @@ -23,11 +23,14 @@ import './alarm-details-dialog.scss'; |
23 | 23 | const js_beautify = beautify.js; |
24 | 24 | |
25 | 25 | /*@ngInject*/ |
26 | -export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, alarmService, alarmId, showingCallback) { | |
26 | +export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, | |
27 | + alarmService, alarmId, allowAcknowledgment, allowClear, showingCallback) { | |
27 | 28 | |
28 | 29 | var vm = this; |
29 | 30 | |
30 | 31 | vm.alarmId = alarmId; |
32 | + vm.allowAcknowledgment = allowAcknowledgment; | |
33 | + vm.allowClear = allowClear; | |
31 | 34 | vm.types = types; |
32 | 35 | vm.alarm = null; |
33 | 36 | ... | ... |
... | ... | @@ -84,16 +84,16 @@ |
84 | 84 | </div> |
85 | 85 | </md-dialog-content> |
86 | 86 | <md-dialog-actions layout="row"> |
87 | - <md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeUnack || | |
88 | - vm.alarm.status==vm.types.alarmStatus.clearedUnack" | |
87 | + <md-button ng-if="vm.allowAcknowledgment && (vm.alarm.status==vm.types.alarmStatus.activeUnack || | |
88 | + vm.alarm.status==vm.types.alarmStatus.clearedUnack)" | |
89 | 89 | class="md-raised md-primary" |
90 | 90 | ng-disabled="loading" |
91 | 91 | ng-click="vm.acknowledge()" |
92 | 92 | style="margin-right:20px;">{{ 'alarm.acknowledge' | |
93 | 93 | translate }} |
94 | 94 | </md-button> |
95 | - <md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeAck || | |
96 | - vm.alarm.status==vm.types.alarmStatus.activeUnack" | |
95 | + <md-button ng-if="vm.allowClear && (vm.alarm.status==vm.types.alarmStatus.activeAck || | |
96 | + vm.alarm.status==vm.types.alarmStatus.activeUnack)" | |
97 | 97 | class="md-raised md-primary" |
98 | 98 | ng-disabled="loading" |
99 | 99 | ng-click="vm.clear()">{{ 'alarm.clear' | | ... | ... |
... | ... | @@ -40,7 +40,12 @@ export default function AlarmRowDirective($compile, $templateCache, types, $mdDi |
40 | 40 | controller: 'AlarmDetailsDialogController', |
41 | 41 | controllerAs: 'vm', |
42 | 42 | templateUrl: alarmDetailsDialogTemplate, |
43 | - locals: {alarmId: scope.alarm.id.id, showingCallback: onShowingCallback}, | |
43 | + locals: { | |
44 | + alarmId: scope.alarm.id.id, | |
45 | + allowAcknowledgment: true, | |
46 | + allowClear: true, | |
47 | + showingCallback: onShowingCallback | |
48 | + }, | |
44 | 49 | parent: angular.element($document[0].body), |
45 | 50 | targetEvent: $event, |
46 | 51 | fullscreen: true, | ... | ... |
... | ... | @@ -252,12 +252,12 @@ function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) { |
252 | 252 | $timeout(function() { |
253 | 253 | alarmSourceListener.alarmsUpdated([simulatedAlarm], false); |
254 | 254 | }); |
255 | - } else { | |
256 | - var pollingInterval = 5000; //TODO: | |
255 | + } else if (alarmSource.entityType && alarmSource.entityId) { | |
256 | + var pollingInterval = alarmSourceListener.alarmsPollingInterval; | |
257 | 257 | alarmSourceListener.alarmsQuery = { |
258 | 258 | entityType: alarmSource.entityType, |
259 | 259 | entityId: alarmSource.entityId, |
260 | - alarmSearchStatus: null, //TODO: | |
260 | + alarmSearchStatus: alarmSourceListener.alarmSearchStatus, | |
261 | 261 | alarmStatus: null |
262 | 262 | } |
263 | 263 | var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'}); | ... | ... |
... | ... | @@ -70,6 +70,12 @@ export default class Subscription { |
70 | 70 | this.callbacks.dataLoading = this.callbacks.dataLoading || function(){}; |
71 | 71 | this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){}; |
72 | 72 | this.alarmSource = options.alarmSource; |
73 | + | |
74 | + this.alarmSearchStatus = angular.isDefined(options.alarmSearchStatus) ? | |
75 | + options.alarmSearchStatus : this.ctx.types.alarmSearchStatus.any; | |
76 | + this.alarmsPollingInterval = angular.isDefined(options.alarmsPollingInterval) ? | |
77 | + options.alarmsPollingInterval : 5000; | |
78 | + | |
73 | 79 | this.alarmSourceListener = null; |
74 | 80 | this.alarms = []; |
75 | 81 | |
... | ... | @@ -193,8 +199,7 @@ export default class Subscription { |
193 | 199 | registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { |
194 | 200 | if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { |
195 | 201 | subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); |
196 | - subscription.unsubscribe(); | |
197 | - subscription.subscribe(); | |
202 | + subscription.update(); | |
198 | 203 | } |
199 | 204 | }); |
200 | 205 | this.registrations.push(registration); |
... | ... | @@ -281,8 +286,7 @@ export default class Subscription { |
281 | 286 | registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { |
282 | 287 | if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { |
283 | 288 | subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); |
284 | - subscription.unsubscribe(); | |
285 | - subscription.subscribe(); | |
289 | + subscription.update(); | |
286 | 290 | } |
287 | 291 | }); |
288 | 292 | this.registrations.push(registration); |
... | ... | @@ -298,8 +302,7 @@ export default class Subscription { |
298 | 302 | return subscription.timeWindowConfig; |
299 | 303 | }, function (newTimewindow, prevTimewindow) { |
300 | 304 | if (!angular.equals(newTimewindow, prevTimewindow)) { |
301 | - subscription.unsubscribe(); | |
302 | - subscription.subscribe(); | |
305 | + subscription.update(); | |
303 | 306 | } |
304 | 307 | }, true); |
305 | 308 | this.registrations.push(this.timeWindowWatchRegistration); |
... | ... | @@ -502,8 +505,7 @@ export default class Subscription { |
502 | 505 | this.timeWindowConfig = angular.copy(this.originalTimewindow); |
503 | 506 | this.originalTimewindow = null; |
504 | 507 | this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); |
505 | - this.unsubscribe(); | |
506 | - this.subscribe(); | |
508 | + this.update(); | |
507 | 509 | this.startWatchingTimewindow(); |
508 | 510 | } |
509 | 511 | } |
... | ... | @@ -519,8 +521,7 @@ export default class Subscription { |
519 | 521 | } |
520 | 522 | this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); |
521 | 523 | this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); |
522 | - this.unsubscribe(); | |
523 | - this.subscribe(); | |
524 | + this.update(); | |
524 | 525 | this.startWatchingTimewindow(); |
525 | 526 | } |
526 | 527 | } |
... | ... | @@ -618,6 +619,11 @@ export default class Subscription { |
618 | 619 | this.callbacks.legendDataUpdated(this, apply !== false); |
619 | 620 | } |
620 | 621 | |
622 | + update() { | |
623 | + this.unsubscribe(); | |
624 | + this.subscribe(); | |
625 | + } | |
626 | + | |
621 | 627 | subscribe() { |
622 | 628 | if (this.type === this.ctx.types.widgetType.rpc.value) { |
623 | 629 | return; |
... | ... | @@ -688,6 +694,8 @@ export default class Subscription { |
688 | 694 | this.alarmSourceListener = { |
689 | 695 | subscriptionTimewindow: this.subscriptionTimewindow, |
690 | 696 | alarmSource: this.alarmSource, |
697 | + alarmSearchStatus: this.alarmSearchStatus, | |
698 | + alarmsPollingInterval: this.alarmsPollingInterval, | |
691 | 699 | alarmsUpdated: function(alarms, apply) { |
692 | 700 | subscription.alarmsUpdated(alarms, apply); |
693 | 701 | } | ... | ... |
... | ... | @@ -394,7 +394,8 @@ export default angular.module('thingsboard.types', []) |
394 | 394 | cards: "cards" |
395 | 395 | }, |
396 | 396 | translate: { |
397 | - dashboardStatePrefix: "dashboardState.state." | |
397 | + dashboardStatePrefix: "dashboardState.state.", | |
398 | + keyLabelPrefix: "key.label." | |
398 | 399 | } |
399 | 400 | } |
400 | 401 | ).name; | ... | ... |
... | ... | @@ -63,6 +63,12 @@ function DatakeyConfig($compile, $templateCache, $q, types) { |
63 | 63 | element.html(template); |
64 | 64 | |
65 | 65 | scope.types = types; |
66 | + | |
67 | + scope.alarmFields = []; | |
68 | + for (var alarmField in types.alarmFields) { | |
69 | + scope.alarmFields.push(alarmField); | |
70 | + } | |
71 | + | |
66 | 72 | scope.selectedKey = null; |
67 | 73 | scope.keySearchText = null; |
68 | 74 | scope.usePostProcessing = false; |
... | ... | @@ -112,21 +118,39 @@ function DatakeyConfig($compile, $templateCache, $q, types) { |
112 | 118 | }, true); |
113 | 119 | |
114 | 120 | scope.keysSearch = function (searchText) { |
115 | - if (scope.entityAlias) { | |
116 | - var deferred = $q.defer(); | |
117 | - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: scope.model.type}) | |
118 | - .then(function (keys) { | |
119 | - keys.push(searchText); | |
120 | - deferred.resolve(keys); | |
121 | - }, function (e) { | |
122 | - deferred.reject(e); | |
123 | - }); | |
124 | - return deferred.promise; | |
121 | + if (scope.model.type === types.dataKeyType.alarm) { | |
122 | + var dataKeys = searchText ? scope.alarmFields.filter( | |
123 | + scope.createFilterForDataKey(searchText)) : scope.alarmFields; | |
124 | + dataKeys.push(searchText); | |
125 | + return dataKeys; | |
125 | 126 | } else { |
126 | - return $q.when([]); | |
127 | + if (scope.entityAlias) { | |
128 | + var deferred = $q.defer(); | |
129 | + scope.fetchEntityKeys({ | |
130 | + entityAliasId: scope.entityAlias.id, | |
131 | + query: searchText, | |
132 | + type: scope.model.type | |
133 | + }) | |
134 | + .then(function (keys) { | |
135 | + keys.push(searchText); | |
136 | + deferred.resolve(keys); | |
137 | + }, function (e) { | |
138 | + deferred.reject(e); | |
139 | + }); | |
140 | + return deferred.promise; | |
141 | + } else { | |
142 | + return $q.when([]); | |
143 | + } | |
127 | 144 | } |
128 | 145 | }; |
129 | 146 | |
147 | + scope.createFilterForDataKey = function (query) { | |
148 | + var lowercaseQuery = angular.lowercase(query); | |
149 | + return function filterFn(dataKey) { | |
150 | + return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0); | |
151 | + }; | |
152 | + }; | |
153 | + | |
130 | 154 | $compile(element.contents())(scope); |
131 | 155 | } |
132 | 156 | ... | ... |
... | ... | @@ -16,7 +16,9 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 18 | <md-content class="md-padding" layout="column"> |
19 | - <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries || model.type === types.dataKeyType.attribute" | |
19 | + <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries || | |
20 | + model.type === types.dataKeyType.attribute || | |
21 | + model.type === types.dataKeyType.alarm" | |
20 | 22 | style="padding-bottom: 8px;" |
21 | 23 | ng-required="true" |
22 | 24 | md-no-cache="true" |
... | ... | @@ -27,8 +29,8 @@ |
27 | 29 | md-items="item in keysSearch(keySearchText)" |
28 | 30 | md-item-text="item" |
29 | 31 | md-min-length="0" |
30 | - placeholder="Key name" | |
31 | - md-floating-label="Key"> | |
32 | + placeholder="{{ 'entity.key-name' | translate }}" | |
33 | + md-floating-label="{{ 'entity.key' | translate }}"> | |
32 | 34 | <span md-highlight-text="keySearchText" md-highlight-flags="^i">{{item}}</span> |
33 | 35 | </md-autocomplete> |
34 | 36 | <div layout="row" layout-align="start center"> |
... | ... | @@ -48,7 +50,7 @@ |
48 | 50 | md-color-history="false"> |
49 | 51 | </div> |
50 | 52 | </div> |
51 | - <div layout="row" layout-align="start center"> | |
53 | + <div layout="row" layout-align="start center" ng-if="model.type !== types.dataKeyType.alarm"> | |
52 | 54 | <md-input-container flex> |
53 | 55 | <label translate>datakey.units</label> |
54 | 56 | <input name="units" ng-model="model.units"> | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | */ |
16 | 16 | @import '../../scss/constants'; |
17 | 17 | |
18 | -.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete { | |
18 | +.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete, .tb-alarm-datakey-autocomplete { | |
19 | 19 | .tb-not-found { |
20 | 20 | display: block; |
21 | 21 | line-height: 1.5; | ... | ... |
... | ... | @@ -133,7 +133,7 @@ |
133 | 133 | ng-required="true" |
134 | 134 | ng-model="alarmDataKeys" md-autocomplete-snap |
135 | 135 | md-transform-chip="transformAlarmDataKeyChip($chip)" |
136 | - md-require-match="true"> | |
136 | + md-require-match="false"> | |
137 | 137 | <md-autocomplete |
138 | 138 | md-no-cache="true" |
139 | 139 | id="alarm_datakey" |
... | ... | @@ -152,6 +152,9 @@ |
152 | 152 | </div> |
153 | 153 | <div ng-if="textIsNotEmpty(alarmDataKeySearchText)"> |
154 | 154 | <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span> |
155 | + <span> | |
156 | + <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a> | |
157 | + </span> | |
155 | 158 | </div> |
156 | 159 | </div> |
157 | 160 | </md-not-found> | ... | ... |
... | ... | @@ -48,9 +48,9 @@ |
48 | 48 | <span translate>device.no-keys-found</span> |
49 | 49 | </div> |
50 | 50 | <div ng-if="textIsNotEmpty(dataKeySearchText)"> |
51 | - <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span> | |
51 | + <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span> | |
52 | 52 | <span> |
53 | - <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a> | |
53 | + <a translate ng-click="createKey($event, '#function_datakey_chips')">entity.create-new-key</a> | |
54 | 54 | </span> |
55 | 55 | </div> |
56 | 56 | </div> |
... | ... | @@ -81,7 +81,7 @@ |
81 | 81 | ng-required="true" |
82 | 82 | ng-model="alarmDataKeys" md-autocomplete-snap |
83 | 83 | md-transform-chip="transformAlarmDataKeyChip($chip)" |
84 | - md-require-match="true"> | |
84 | + md-require-match="false"> | |
85 | 85 | <md-autocomplete |
86 | 86 | md-no-cache="true" |
87 | 87 | id="alarm_datakey" |
... | ... | @@ -100,6 +100,9 @@ |
100 | 100 | </div> |
101 | 101 | <div ng-if="textIsNotEmpty(alarmDataKeySearchText)"> |
102 | 102 | <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span> |
103 | + <span> | |
104 | + <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a> | |
105 | + </span> | |
103 | 106 | </div> |
104 | 107 | </div> |
105 | 108 | </md-not-found> | ... | ... |
... | ... | @@ -144,6 +144,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout |
144 | 144 | scope.targetDeviceAlias.value = null; |
145 | 145 | } |
146 | 146 | } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { |
147 | + scope.alarmSearchStatus = angular.isDefined(config.alarmSearchStatus) ? | |
148 | + config.alarmSearchStatus : types.alarmSearchStatus.any; | |
149 | + scope.alarmsPollingInterval = angular.isDefined(config.alarmsPollingInterval) ? | |
150 | + config.alarmsPollingInterval : 5; | |
147 | 151 | if (config.alarmSource) { |
148 | 152 | scope.alarmSource.value = config.alarmSource; |
149 | 153 | } else { |
... | ... | @@ -205,7 +209,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout |
205 | 209 | }; |
206 | 210 | |
207 | 211 | scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' + |
208 | - 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () { | |
212 | + 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' + | |
213 | + 'alarmSearchStatus + alarmsPollingInterval + showLegend', function () { | |
209 | 214 | if (ngModelCtrl.$viewValue) { |
210 | 215 | var value = ngModelCtrl.$viewValue; |
211 | 216 | if (value.config) { |
... | ... | @@ -225,6 +230,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout |
225 | 230 | config.units = scope.units; |
226 | 231 | config.decimals = scope.decimals; |
227 | 232 | config.useDashboardTimewindow = scope.useDashboardTimewindow; |
233 | + config.alarmSearchStatus = scope.alarmSearchStatus; | |
234 | + config.alarmsPollingInterval = scope.alarmsPollingInterval; | |
228 | 235 | config.showLegend = scope.showLegend; |
229 | 236 | } |
230 | 237 | if (value.layout) { | ... | ... |
... | ... | @@ -31,6 +31,30 @@ |
31 | 31 | flex ng-model="timewindow"></tb-timewindow> |
32 | 32 | </section> |
33 | 33 | </div> |
34 | + <div ng-show="widgetType === types.widgetType.alarm.value" layout='column' layout-align="center" | |
35 | + layout-gt-sm='row' layout-align-gt-sm="start center"> | |
36 | + <md-input-container class="md-block" flex> | |
37 | + <label translate>alarm.alarm-status</label> | |
38 | + <md-select ng-model="alarmSearchStatus" style="padding-bottom: 24px;"> | |
39 | + <md-option ng-repeat="searchStatus in types.alarmSearchStatus" ng-value="searchStatus"> | |
40 | + {{ ('alarm.search-status.' + searchStatus) | translate }} | |
41 | + </md-option> | |
42 | + </md-select> | |
43 | + </md-input-container> | |
44 | + <md-input-container flex class="md-block"> | |
45 | + <label translate>alarm.polling-interval</label> | |
46 | + <input ng-required="widgetType === types.widgetType.alarm.value" | |
47 | + type="number" | |
48 | + step="1" | |
49 | + min="1" | |
50 | + name="alarmsPollingInterval" | |
51 | + ng-model="alarmsPollingInterval"/> | |
52 | + <div ng-messages="theForm.alarmsPollingInterval.$error" multiple md-auto-hide="false"> | |
53 | + <div ng-message="required" translate>alarm.polling-interval-required</div> | |
54 | + <div ng-message="min" translate>alarm.min-polling-interval-message</div> | |
55 | + </div> | |
56 | + </md-input-container> | |
57 | + </div> | |
34 | 58 | <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" |
35 | 59 | ng-show="widgetType !== types.widgetType.rpc.value |
36 | 60 | && widgetType !== types.widgetType.alarm.value | ... | ... |
... | ... | @@ -289,6 +289,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
289 | 289 | } |
290 | 290 | if (widget.type == types.widgetType.alarm.value) { |
291 | 291 | options.alarmSource = angular.copy(widget.config.alarmSource); |
292 | + options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ? | |
293 | + widget.config.alarmSearchStatus : types.alarmSearchStatus.any; | |
294 | + options.alarmsPollingInterval = angular.isDefined(widget.config.alarmsPollingInterval) ? | |
295 | + widget.config.alarmsPollingInterval * 1000 : 5000; | |
292 | 296 | } else { |
293 | 297 | options.datasources = angular.copy(widget.config.datasources) |
294 | 298 | } | ... | ... |
... | ... | @@ -243,11 +243,9 @@ export default function EntityStateController($scope, $location, $state, $stateP |
243 | 243 | } |
244 | 244 | |
245 | 245 | function gotoState(stateId, update, openRightLayout) { |
246 | - if (vm.dashboardCtrl.dashboardCtx.state != stateId) { | |
247 | - vm.dashboardCtrl.openDashboardState(stateId, openRightLayout); | |
248 | - if (update) { | |
249 | - updateLocation(); | |
250 | - } | |
246 | + vm.dashboardCtrl.openDashboardState(stateId, openRightLayout); | |
247 | + if (update) { | |
248 | + updateLocation(); | |
251 | 249 | } |
252 | 250 | } |
253 | 251 | ... | ... |
... | ... | @@ -148,7 +148,14 @@ export default angular.module('thingsboard.locale', []) |
148 | 148 | "clear": "Clear", |
149 | 149 | "search": "Search alarms", |
150 | 150 | "selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected", |
151 | - "no-data": "No data to display" | |
151 | + "no-data": "No data to display", | |
152 | + "polling-interval": "Alarms polling interval (sec)", | |
153 | + "polling-interval-required": "Alarms polling interval is required.", | |
154 | + "min-polling-interval-message": "At least 1 sec polling interval is allowed.", | |
155 | + "aknowledge-alarms-title": "Acknowledge { count, select, 1 {1 alarm} other {# alarms} }", | |
156 | + "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, select, 1 {1 alarm} other {# alarms} }?", | |
157 | + "clear-alarms-title": "Clear { count, select, 1 {1 alarm} other {# alarms} }", | |
158 | + "clear-alarms-text": "Are you sure you want to clear { count, select, 1 {1 alarm} other {# alarms} }?" | |
152 | 159 | }, |
153 | 160 | "alias": { |
154 | 161 | "add": "Add alias", |
... | ... | @@ -643,6 +650,8 @@ export default angular.module('thingsboard.locale', []) |
643 | 650 | "no-aliases-found": "No aliases found.", |
644 | 651 | "no-alias-matching": "'{{alias}}' not found.", |
645 | 652 | "create-new-alias": "Create a new one!", |
653 | + "key": "Key", | |
654 | + "key-name": "Key name", | |
646 | 655 | "no-keys-found": "No keys found.", |
647 | 656 | "no-key-matching": "'{{key}}' not found.", |
648 | 657 | "create-new-key": "Create a new one!", | ... | ... |
... | ... | @@ -18,7 +18,8 @@ |
18 | 18 | export default function ThingsboardMissingTranslateHandler($log, types) { |
19 | 19 | |
20 | 20 | return function (translationId) { |
21 | - if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) { | |
21 | + if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix) && | |
22 | + !translationId.startsWith(types.translate.keyLabelPrefix)) { | |
22 | 23 | $log.warn('Translation for ' + translationId + ' doesn\'t exist'); |
23 | 24 | } |
24 | 25 | }; | ... | ... |
... | ... | @@ -19,6 +19,7 @@ import './alarms-table-widget.scss'; |
19 | 19 | /* eslint-disable import/no-unresolved, import/default */ |
20 | 20 | |
21 | 21 | import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html'; |
22 | +import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html'; | |
22 | 23 | |
23 | 24 | /* eslint-enable import/no-unresolved, import/default */ |
24 | 25 | |
... | ... | @@ -46,11 +47,12 @@ function AlarmsTableWidget() { |
46 | 47 | } |
47 | 48 | |
48 | 49 | /*@ngInject*/ |
49 | -function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUtil, $translate, utils, types) { | |
50 | +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, alarmService, utils, types) { | |
50 | 51 | var vm = this; |
51 | 52 | |
52 | 53 | vm.stylesInfo = {}; |
53 | 54 | vm.contentsInfo = {}; |
55 | + vm.columnWidth = {}; | |
54 | 56 | |
55 | 57 | vm.showData = true; |
56 | 58 | vm.hasData = false; |
... | ... | @@ -64,19 +66,32 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
64 | 66 | |
65 | 67 | vm.currentAlarm = null; |
66 | 68 | |
69 | + vm.alarmsTitle = $translate.instant('alarm.alarms'); | |
70 | + vm.enableSelection = true; | |
71 | + vm.enableSearch = true; | |
72 | + vm.displayDetails = true; | |
73 | + vm.allowAcknowledgment = true; | |
74 | + vm.allowClear = true; | |
75 | + vm.displayPagination = true; | |
76 | + vm.defaultPageSize = 10; | |
77 | + vm.defaultSortOrder = '-'+types.alarmFields.createdTime.value; | |
78 | + | |
67 | 79 | vm.query = { |
68 | - order: '-'+types.alarmFields.createdTime.value, | |
69 | - limit: 10, | |
80 | + order: vm.defaultSortOrder, | |
81 | + limit: vm.defaultPageSize, | |
70 | 82 | page: 1, |
71 | 83 | search: null |
72 | 84 | }; |
73 | 85 | |
74 | - vm.alarmsTitle = $translate.instant('alarm.alarms'); | |
75 | - | |
76 | 86 | vm.enterFilterMode = enterFilterMode; |
77 | 87 | vm.exitFilterMode = exitFilterMode; |
78 | 88 | vm.onReorder = onReorder; |
79 | 89 | vm.onPaginate = onPaginate; |
90 | + vm.onRowClick = onRowClick; | |
91 | + vm.isCurrent = isCurrent; | |
92 | + vm.openAlarmDetails = openAlarmDetails; | |
93 | + vm.ackAlarms = ackAlarms; | |
94 | + vm.clearAlarms = clearAlarms; | |
80 | 95 | |
81 | 96 | vm.cellStyle = cellStyle; |
82 | 97 | vm.cellContent = cellContent; |
... | ... | @@ -119,7 +134,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
119 | 134 | $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) { |
120 | 135 | vm.isGtMd = isGtMd; |
121 | 136 | if (vm.isGtMd) { |
122 | - vm.limitOptions = [5, 10, 15]; | |
137 | + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; | |
123 | 138 | } else { |
124 | 139 | vm.limitOptions = null; |
125 | 140 | } |
... | ... | @@ -130,7 +145,33 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
130 | 145 | if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { |
131 | 146 | vm.alarmsTitle = vm.settings.alarmsTitle; |
132 | 147 | } |
133 | - //TODO: | |
148 | + vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; | |
149 | + vm.enableSearch = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; | |
150 | + vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; | |
151 | + vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; | |
152 | + vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true; | |
153 | + if (!vm.allowAcknowledgment && !vm.allowClear) { | |
154 | + vm.enableSelection = false; | |
155 | + } | |
156 | + | |
157 | + vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; | |
158 | + | |
159 | + var pageSize = vm.settings.defaultPageSize; | |
160 | + if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) { | |
161 | + vm.defaultPageSize = pageSize; | |
162 | + } | |
163 | + | |
164 | + if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) { | |
165 | + vm.defaultSortOrder = vm.settings.defaultSortOrder; | |
166 | + } | |
167 | + | |
168 | + vm.query.order = vm.defaultSortOrder; | |
169 | + vm.query.limit = vm.defaultPageSize; | |
170 | + if (vm.isGtMd) { | |
171 | + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; | |
172 | + } else { | |
173 | + vm.limitOptions = null; | |
174 | + } | |
134 | 175 | |
135 | 176 | var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; |
136 | 177 | var defaultColor = tinycolor(origColor); |
... | ... | @@ -207,6 +248,115 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
207 | 248 | updateAlarms(); |
208 | 249 | } |
209 | 250 | |
251 | + function onRowClick($event, alarm) { | |
252 | + if (vm.currentAlarm != alarm) { | |
253 | + vm.currentAlarm = alarm; | |
254 | + } | |
255 | + } | |
256 | + | |
257 | + function isCurrent(alarm) { | |
258 | + return (vm.currentAlarm && alarm && vm.currentAlarm.id && alarm.id) && | |
259 | + (vm.currentAlarm.id.id === alarm.id.id); | |
260 | + } | |
261 | + | |
262 | + function openAlarmDetails($event, alarm) { | |
263 | + if (alarm && alarm.id) { | |
264 | + var onShowingCallback = { | |
265 | + onShowing: function(){} | |
266 | + } | |
267 | + $mdDialog.show({ | |
268 | + controller: 'AlarmDetailsDialogController', | |
269 | + controllerAs: 'vm', | |
270 | + templateUrl: alarmDetailsDialogTemplate, | |
271 | + locals: { | |
272 | + alarmId: alarm.id.id, | |
273 | + allowAcknowledgment: vm.allowAcknowledgment, | |
274 | + allowClear: vm.allowClear, | |
275 | + showingCallback: onShowingCallback | |
276 | + }, | |
277 | + parent: angular.element($document[0].body), | |
278 | + targetEvent: $event, | |
279 | + fullscreen: true, | |
280 | + skipHide: true, | |
281 | + onShowing: function(scope, element) { | |
282 | + onShowingCallback.onShowing(scope, element); | |
283 | + } | |
284 | + }).then(function (alarm) { | |
285 | + if (alarm) { | |
286 | + vm.subscription.update(); | |
287 | + } | |
288 | + }); | |
289 | + | |
290 | + } | |
291 | + } | |
292 | + | |
293 | + function ackAlarms($event) { | |
294 | + if ($event) { | |
295 | + $event.stopPropagation(); | |
296 | + } | |
297 | + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) { | |
298 | + var title = $translate.instant('alarm.aknowledge-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); | |
299 | + var content = $translate.instant('alarm.aknowledge-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); | |
300 | + var confirm = $mdDialog.confirm() | |
301 | + .targetEvent($event) | |
302 | + .title(title) | |
303 | + .htmlContent(content) | |
304 | + .ariaLabel(title) | |
305 | + .cancel($translate.instant('action.no')) | |
306 | + .ok($translate.instant('action.yes')); | |
307 | + $mdDialog.show(confirm).then(function () { | |
308 | + var tasks = []; | |
309 | + for (var i=0;i<vm.selectedAlarms.length;i++) { | |
310 | + var alarm = vm.selectedAlarms[i]; | |
311 | + if (alarm.id) { | |
312 | + tasks.push(alarmService.ackAlarm(alarm.id.id)); | |
313 | + } | |
314 | + } | |
315 | + if (tasks.length) { | |
316 | + $q.all(tasks).then(function () { | |
317 | + vm.selectedAlarms = []; | |
318 | + vm.subscription.update(); | |
319 | + }); | |
320 | + } | |
321 | + | |
322 | + }); | |
323 | + } | |
324 | + } | |
325 | + | |
326 | + function clearAlarms($event) { | |
327 | + if ($event) { | |
328 | + $event.stopPropagation(); | |
329 | + } | |
330 | + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) { | |
331 | + var title = $translate.instant('alarm.clear-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); | |
332 | + var content = $translate.instant('alarm.clear-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); | |
333 | + var confirm = $mdDialog.confirm() | |
334 | + .targetEvent($event) | |
335 | + .title(title) | |
336 | + .htmlContent(content) | |
337 | + .ariaLabel(title) | |
338 | + .cancel($translate.instant('action.no')) | |
339 | + .ok($translate.instant('action.yes')); | |
340 | + $mdDialog.show(confirm).then(function () { | |
341 | + var tasks = []; | |
342 | + for (var i=0;i<vm.selectedAlarms.length;i++) { | |
343 | + var alarm = vm.selectedAlarms[i]; | |
344 | + if (alarm.id) { | |
345 | + tasks.push(alarmService.clearAlarm(alarm.id.id)); | |
346 | + } | |
347 | + } | |
348 | + if (tasks.length) { | |
349 | + $q.all(tasks).then(function () { | |
350 | + vm.selectedAlarms = []; | |
351 | + vm.subscription.update(); | |
352 | + }); | |
353 | + } | |
354 | + | |
355 | + }); | |
356 | + } | |
357 | + } | |
358 | + | |
359 | + | |
210 | 360 | function updateAlarms(preserveSelections) { |
211 | 361 | if (!preserveSelections) { |
212 | 362 | vm.selectedAlarms = []; |
... | ... | @@ -216,8 +366,14 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
216 | 366 | result = $filter('filter')(result, {$: vm.query.search}); |
217 | 367 | } |
218 | 368 | vm.alarmsCount = result.length; |
219 | - var startIndex = vm.query.limit * (vm.query.page - 1); | |
220 | - vm.alarms = result.slice(startIndex, startIndex + vm.query.limit); | |
369 | + | |
370 | + if (vm.displayPagination) { | |
371 | + var startIndex = vm.query.limit * (vm.query.page - 1); | |
372 | + vm.alarms = result.slice(startIndex, startIndex + vm.query.limit); | |
373 | + } else { | |
374 | + vm.alarms = result; | |
375 | + } | |
376 | + | |
221 | 377 | if (preserveSelections) { |
222 | 378 | var newSelectedAlarms = []; |
223 | 379 | if (vm.selectedAlarms && vm.selectedAlarms.length) { |
... | ... | @@ -251,6 +407,10 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
251 | 407 | style = defaultStyle(key, value); |
252 | 408 | } |
253 | 409 | } |
410 | + if (!style.width) { | |
411 | + var columnWidth = vm.columnWidth[key.label]; | |
412 | + style.width = columnWidth; | |
413 | + } | |
254 | 414 | return style; |
255 | 415 | } |
256 | 416 | |
... | ... | @@ -295,7 +455,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
295 | 455 | return value; |
296 | 456 | } |
297 | 457 | } else { |
298 | - return ''; | |
458 | + return value; | |
299 | 459 | } |
300 | 460 | } else { |
301 | 461 | return ''; |
... | ... | @@ -313,6 +473,8 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
313 | 473 | } else { |
314 | 474 | return {}; |
315 | 475 | } |
476 | + } else { | |
477 | + return {}; | |
316 | 478 | } |
317 | 479 | } else { |
318 | 480 | return {}; |
... | ... | @@ -340,9 +502,19 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
340 | 502 | |
341 | 503 | vm.stylesInfo = {}; |
342 | 504 | vm.contentsInfo = {}; |
505 | + vm.columnWidth = {}; | |
343 | 506 | |
344 | 507 | for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { |
345 | 508 | var dataKey = vm.alarmSource.dataKeys[d]; |
509 | + | |
510 | + var translationId = types.translate.keyLabelPrefix + dataKey.label; | |
511 | + var translation = $translate.instant(translationId); | |
512 | + if (translation != translationId) { | |
513 | + dataKey.title = translation; | |
514 | + } else { | |
515 | + dataKey.title = dataKey.label; | |
516 | + } | |
517 | + | |
346 | 518 | var keySettings = dataKey.settings; |
347 | 519 | |
348 | 520 | var cellStyleFunction = null; |
... | ... | @@ -384,6 +556,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti |
384 | 556 | useCellContentFunction: useCellContentFunction, |
385 | 557 | cellContentFunction: cellContentFunction |
386 | 558 | }; |
559 | + | |
560 | + var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; | |
561 | + vm.columnWidth[dataKey.label] = columnWidth; | |
387 | 562 | } |
388 | 563 | } |
389 | 564 | ... | ... |
... | ... | @@ -17,14 +17,14 @@ |
17 | 17 | .tb-alarms-table { |
18 | 18 | margin-top: 15px; |
19 | 19 | &.tb-data-table { |
20 | - table.md-table { | |
20 | + table.md-table, table.md-table.md-row-select { | |
21 | 21 | tbody { |
22 | 22 | tr { |
23 | 23 | td { |
24 | - &.ag-action-cell { | |
25 | - min-width: 40px; | |
26 | - max-width: 40px; | |
27 | - width: 40px; | |
24 | + &.tb-action-cell { | |
25 | + min-width: 36px; | |
26 | + max-width: 36px; | |
27 | + width: 36px; | |
28 | 28 | } |
29 | 29 | } |
30 | 30 | } | ... | ... |
... | ... | @@ -22,7 +22,7 @@ |
22 | 22 | <div class="md-toolbar-tools"> |
23 | 23 | <span>{{ vm.alarmsTitle }}</span> |
24 | 24 | <span flex></span> |
25 | - <md-button class="md-icon-button" ng-click="vm.enterFilterMode()"> | |
25 | + <md-button ng-if="vm.enableSearch" class="md-icon-button" ng-click="vm.enterFilterMode()"> | |
26 | 26 | <md-icon>search</md-icon> |
27 | 27 | <md-tooltip md-direction="top"> |
28 | 28 | {{ 'action.search' | translate }} |
... | ... | @@ -57,13 +57,13 @@ |
57 | 57 | translate-values="{count: vm.selectedAlarms.length}" |
58 | 58 | translate-interpolation="messageformat"></span> |
59 | 59 | <span flex></span> |
60 | - <md-button class="md-icon-button" ng-click="vm.ackAlarms($event)"> | |
60 | + <md-button ng-if="vm.allowAcknowledgment" class="md-icon-button" ng-click="vm.ackAlarms($event)"> | |
61 | 61 | <md-icon>done</md-icon> |
62 | 62 | <md-tooltip md-direction="top"> |
63 | 63 | {{ 'alarm.acknowledge' | translate }} |
64 | 64 | </md-tooltip> |
65 | 65 | </md-button> |
66 | - <md-button class="md-icon-button" ng-click="vm.clearAlarms($event)"> | |
66 | + <md-button ng-if="vm.allowClear" class="md-icon-button" ng-click="vm.clearAlarms($event)"> | |
67 | 67 | <md-icon>clear</md-icon> |
68 | 68 | <md-tooltip md-direction="top"> |
69 | 69 | {{ 'alarm.clear' | translate }} |
... | ... | @@ -72,22 +72,22 @@ |
72 | 72 | </div> |
73 | 73 | </md-toolbar> |
74 | 74 | <md-table-container flex> |
75 | - <table md-table md-row-select multiple="" ng-model="vm.selectedAlarms"> | |
75 | + <table md-table md-row-select="vm.enableSelection" multiple="" ng-model="vm.selectedAlarms"> | |
76 | 76 | <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> |
77 | 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> | |
78 | + <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.title }}</span></th> | |
79 | + <th md-column ng-if="vm.displayDetails"><span> </span></th> | |
80 | 80 | </tr> |
81 | 81 | </thead> |
82 | 82 | <tbody md-body> |
83 | 83 | <tr ng-show="vm.alarms.length" md-row md-select="alarm" |
84 | 84 | md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms" |
85 | 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" | |
86 | + <td md-cell flex ng-repeat="key in vm.alarmSource.dataKeys" | |
87 | 87 | ng-style="vm.cellStyle(alarm, key)" |
88 | 88 | ng-bind-html="vm.cellContent(alarm, key)"> |
89 | 89 | </td> |
90 | - <td md-cell class="tb-action-cell"> | |
90 | + <td md-cell ng-if="vm.displayDetails" class="tb-action-cell"> | |
91 | 91 | <md-button class="md-icon-button" aria-label="{{ 'alarm.details' | translate }}" |
92 | 92 | ng-click="vm.openAlarmDetails($event, alarm)"> |
93 | 93 | <md-icon aria-label="{{ 'alarm.details' | translate }}" class="material-icons">more_horiz</md-icon> |
... | ... | @@ -104,7 +104,7 @@ |
104 | 104 | layout-align="center center" |
105 | 105 | class="no-data-found" translate>alarm.no-alarms-prompt</span> |
106 | 106 | </md-table-container> |
107 | - <md-table-pagination md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions" | |
107 | + <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions" | |
108 | 108 | md-page="vm.query.page" md-total="{{vm.alarmsCount}}" |
109 | 109 | md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd"> |
110 | 110 | </md-table-pagination> | ... | ... |
... | ... | @@ -281,7 +281,55 @@ pre.tb-highlight { |
281 | 281 | display: flex; |
282 | 282 | } |
283 | 283 | table.md-table { |
284 | + &.md-row-select td.md-cell, | |
285 | + &.md-row-select th.md-column { | |
286 | + &:first-child { | |
287 | + width: 20px; | |
288 | + padding: 0 0 0 12px; | |
289 | + } | |
290 | + | |
291 | + &:nth-child(2) { | |
292 | + padding: 0 12px; | |
293 | + } | |
294 | + | |
295 | + &:nth-child(n+3):nth-last-child(n+2) { | |
296 | + padding: 0 28px 0 0; | |
297 | + } | |
298 | + } | |
299 | + | |
300 | + &:not(.md-row-select) td.md-cell, | |
301 | + &:not(.md-row-select) th.md-column { | |
302 | + &:first-child { | |
303 | + padding: 0 12px; | |
304 | + } | |
305 | + | |
306 | + &:nth-child(n+2):nth-last-child(n+2) { | |
307 | + padding: 0 28px 0 0; | |
308 | + } | |
309 | + } | |
310 | + | |
311 | + td.md-cell, | |
312 | + th.md-column { | |
313 | + | |
314 | + &:last-child { | |
315 | + padding: 0 12px 0 0; | |
316 | + } | |
317 | + | |
318 | + } | |
319 | + } | |
320 | + | |
321 | + table.md-table, table.md-table.md-row-select { | |
284 | 322 | tbody { |
323 | + &.md-body { | |
324 | + tr { | |
325 | + &.md-row:not([disabled]) { | |
326 | + outline: none; | |
327 | + &.tb-current, &.tb-current:hover{ | |
328 | + background-color: #dddddd !important; | |
329 | + } | |
330 | + } | |
331 | + } | |
332 | + } | |
285 | 333 | tr { |
286 | 334 | td { |
287 | 335 | &.tb-action-cell { | ... | ... |