Commit f806c392b2f7128878b2847135ffb1fa9676e2cc
Merge remote-tracking branch 'upstream/master' into dao-refactoring-vs
Showing
56 changed files
with
2081 additions
and
284 deletions
... | ... | @@ -143,4 +143,30 @@ public class AlarmController extends BaseController { |
143 | 143 | } |
144 | 144 | } |
145 | 145 | |
146 | + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") | |
147 | + @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) | |
148 | + @ResponseBody | |
149 | + public AlarmSeverity getHighestAlarmSeverity( | |
150 | + @PathVariable("entityType") String strEntityType, | |
151 | + @PathVariable("entityId") String strEntityId, | |
152 | + @RequestParam(required = false) String searchStatus, | |
153 | + @RequestParam(required = false) String status | |
154 | + ) throws ThingsboardException { | |
155 | + checkParameter("EntityId", strEntityId); | |
156 | + checkParameter("EntityType", strEntityType); | |
157 | + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); | |
158 | + AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); | |
159 | + AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); | |
160 | + if (alarmSearchStatus != null && alarmStatus != null) { | |
161 | + throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + | |
162 | + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); | |
163 | + } | |
164 | + checkEntityId(entityId); | |
165 | + try { | |
166 | + return alarmService.findHighestAlarmSeverity(entityId, alarmSearchStatus, alarmStatus); | |
167 | + } catch (Exception e) { | |
168 | + throw handleException(e); | |
169 | + } | |
170 | + } | |
171 | + | |
146 | 172 | } | ... | ... |
... | ... | @@ -16,10 +16,8 @@ |
16 | 16 | package org.thingsboard.server.dao.alarm; |
17 | 17 | |
18 | 18 | import com.google.common.util.concurrent.ListenableFuture; |
19 | -import org.thingsboard.server.common.data.alarm.Alarm; | |
20 | -import org.thingsboard.server.common.data.alarm.AlarmId; | |
21 | -import org.thingsboard.server.common.data.alarm.AlarmInfo; | |
22 | -import org.thingsboard.server.common.data.alarm.AlarmQuery; | |
19 | +import org.thingsboard.server.common.data.alarm.*; | |
20 | +import org.thingsboard.server.common.data.id.EntityId; | |
23 | 21 | import org.thingsboard.server.common.data.page.TimePageData; |
24 | 22 | |
25 | 23 | /** |
... | ... | @@ -39,4 +37,7 @@ public interface AlarmService { |
39 | 37 | |
40 | 38 | ListenableFuture<TimePageData<AlarmInfo>> findAlarms(AlarmQuery query); |
41 | 39 | |
40 | + AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus, | |
41 | + AlarmStatus alarmStatus); | |
42 | + | |
42 | 43 | } | ... | ... |
... | ... | @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.Tenant; |
28 | 28 | import org.thingsboard.server.common.data.alarm.*; |
29 | 29 | import org.thingsboard.server.common.data.id.EntityId; |
30 | 30 | import org.thingsboard.server.common.data.page.TimePageData; |
31 | +import org.thingsboard.server.common.data.page.TimePageLink; | |
31 | 32 | import org.thingsboard.server.common.data.relation.EntityRelation; |
32 | 33 | import org.thingsboard.server.common.data.relation.RelationTypeGroup; |
33 | 34 | import org.thingsboard.server.dao.entity.AbstractEntityService; |
... | ... | @@ -45,6 +46,7 @@ import javax.annotation.PostConstruct; |
45 | 46 | import javax.annotation.PreDestroy; |
46 | 47 | import java.util.ArrayList; |
47 | 48 | import java.util.List; |
49 | +import java.util.UUID; | |
48 | 50 | import java.util.concurrent.ExecutionException; |
49 | 51 | import java.util.concurrent.ExecutorService; |
50 | 52 | import java.util.concurrent.Executors; |
... | ... | @@ -240,6 +242,46 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ |
240 | 242 | }); |
241 | 243 | } |
242 | 244 | |
245 | + @Override | |
246 | + public AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus, | |
247 | + AlarmStatus alarmStatus) { | |
248 | + TimePageLink nextPageLink = new TimePageLink(100); | |
249 | + boolean hasNext = true; | |
250 | + AlarmSeverity highestSeverity = null; | |
251 | + AlarmQuery query; | |
252 | + while (hasNext) { | |
253 | + query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false); | |
254 | + List<AlarmInfo> alarms; | |
255 | + try { | |
256 | + alarms = alarmDao.findAlarms(query).get(); | |
257 | + } catch (ExecutionException | InterruptedException e) { | |
258 | + log.warn("Failed to find highest alarm severity. EntityId: [{}], AlarmSearchStatus: [{}], AlarmStatus: [{}]", | |
259 | + entityId, alarmSearchStatus, alarmStatus); | |
260 | + throw new RuntimeException(e); | |
261 | + } | |
262 | + hasNext = alarms.size() == nextPageLink.getLimit(); | |
263 | + if (hasNext) { | |
264 | + nextPageLink = new TimePageData<>(alarms, nextPageLink).getNextPageLink(); | |
265 | + } | |
266 | + if (alarms.isEmpty()) { | |
267 | + continue; | |
268 | + } else { | |
269 | + List<AlarmInfo> sorted = new ArrayList(alarms); | |
270 | + sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity())); | |
271 | + AlarmSeverity severity = sorted.get(0).getSeverity(); | |
272 | + if (severity == AlarmSeverity.CRITICAL) { | |
273 | + highestSeverity = severity; | |
274 | + break; | |
275 | + } else if (highestSeverity == null) { | |
276 | + highestSeverity = severity; | |
277 | + } else { | |
278 | + highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity; | |
279 | + } | |
280 | + } | |
281 | + } | |
282 | + return highestSeverity; | |
283 | + } | |
284 | + | |
243 | 285 | private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException { |
244 | 286 | log.debug("Deleting Alarm relation: {}", alarmRelation); |
245 | 287 | relationService.deleteRelation(alarmRelation).get(); | ... | ... |
... | ... | @@ -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, | ... | ... |
... | ... | @@ -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, |
... | ... | @@ -26,8 +48,11 @@ function AlarmService($http, $q, $interval, $filter) { |
26 | 48 | ackAlarm: ackAlarm, |
27 | 49 | clearAlarm: clearAlarm, |
28 | 50 | getAlarms: getAlarms, |
51 | + getHighestAlarmSeverity: getHighestAlarmSeverity, | |
29 | 52 | pollAlarms: pollAlarms, |
30 | - cancelPollAlarms: cancelPollAlarms | |
53 | + cancelPollAlarms: cancelPollAlarms, | |
54 | + subscribeForAlarms: subscribeForAlarms, | |
55 | + unsubscribeFromAlarms: unsubscribeFromAlarms | |
31 | 56 | } |
32 | 57 | |
33 | 58 | return service; |
... | ... | @@ -141,6 +166,23 @@ function AlarmService($http, $q, $interval, $filter) { |
141 | 166 | return deferred.promise; |
142 | 167 | } |
143 | 168 | |
169 | + function getHighestAlarmSeverity(entityType, entityId, alarmSearchStatus, alarmStatus, config) { | |
170 | + var deferred = $q.defer(); | |
171 | + var url = '/api/alarm/highestSeverity/' + entityType + '/' + entityId; | |
172 | + | |
173 | + if (alarmSearchStatus) { | |
174 | + url += '?searchStatus=' + alarmSearchStatus; | |
175 | + } else if (alarmStatus) { | |
176 | + url += '?status=' + alarmStatus; | |
177 | + } | |
178 | + $http.get(url, config).then(function success(response) { | |
179 | + deferred.resolve(response.data); | |
180 | + }, function fail() { | |
181 | + deferred.reject(); | |
182 | + }); | |
183 | + return deferred.promise; | |
184 | + } | |
185 | + | |
144 | 186 | function fetchAlarms(alarmsQuery, pageLink, deferred, alarmsList) { |
145 | 187 | getAlarms(alarmsQuery.entityType, alarmsQuery.entityId, |
146 | 188 | pageLink, alarmsQuery.alarmSearchStatus, alarmsQuery.alarmStatus, |
... | ... | @@ -171,12 +213,21 @@ function AlarmService($http, $q, $interval, $filter) { |
171 | 213 | pageLink = { |
172 | 214 | limit: alarmsQuery.limit |
173 | 215 | }; |
174 | - } else { | |
216 | + } else if (alarmsQuery.interval) { | |
175 | 217 | pageLink = { |
176 | 218 | limit: 100, |
177 | 219 | startTime: time - alarmsQuery.interval |
178 | 220 | }; |
221 | + } else if (alarmsQuery.startTime) { | |
222 | + pageLink = { | |
223 | + limit: 100, | |
224 | + startTime: alarmsQuery.startTime | |
225 | + } | |
226 | + if (alarmsQuery.endTime) { | |
227 | + pageLink.endTime = alarmsQuery.endTime; | |
228 | + } | |
179 | 229 | } |
230 | + | |
180 | 231 | fetchAlarms(alarmsQuery, pageLink, deferred); |
181 | 232 | return deferred.promise; |
182 | 233 | } |
... | ... | @@ -211,4 +262,59 @@ function AlarmService($http, $q, $interval, $filter) { |
211 | 262 | } |
212 | 263 | } |
213 | 264 | |
265 | + function subscribeForAlarms(alarmSourceListener) { | |
266 | + alarmSourceListener.id = utils.guid(); | |
267 | + alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener; | |
268 | + var alarmSource = alarmSourceListener.alarmSource; | |
269 | + if (alarmSource.type == types.datasourceType.function) { | |
270 | + $timeout(function() { | |
271 | + alarmSourceListener.alarmsUpdated([simulatedAlarm], false); | |
272 | + }); | |
273 | + } else if (alarmSource.entityType && alarmSource.entityId) { | |
274 | + var pollingInterval = alarmSourceListener.alarmsPollingInterval; | |
275 | + alarmSourceListener.alarmsQuery = { | |
276 | + entityType: alarmSource.entityType, | |
277 | + entityId: alarmSource.entityId, | |
278 | + alarmSearchStatus: alarmSourceListener.alarmSearchStatus, | |
279 | + alarmStatus: null | |
280 | + } | |
281 | + var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'}); | |
282 | + if (originatorKeys && originatorKeys.length) { | |
283 | + alarmSourceListener.alarmsQuery.fetchOriginator = true; | |
284 | + } | |
285 | + var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow; | |
286 | + if (subscriptionTimewindow.realtimeWindowMs) { | |
287 | + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs; | |
288 | + } else { | |
289 | + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs; | |
290 | + alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs; | |
291 | + } | |
292 | + alarmSourceListener.alarmsQuery.onAlarms = function(alarms) { | |
293 | + if (subscriptionTimewindow.realtimeWindowMs) { | |
294 | + var now = Date.now(); | |
295 | + if (alarmSourceListener.lastUpdateTs) { | |
296 | + var interval = now - alarmSourceListener.lastUpdateTs; | |
297 | + alarmSourceListener.alarmsQuery.startTime += interval; | |
298 | + } else { | |
299 | + alarmSourceListener.lastUpdateTs = now; | |
300 | + } | |
301 | + } | |
302 | + alarmSourceListener.alarmsUpdated(alarms, false); | |
303 | + } | |
304 | + onPollAlarms(alarmSourceListener.alarmsQuery); | |
305 | + alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval, | |
306 | + 0, false, alarmSourceListener.alarmsQuery); | |
307 | + } | |
308 | + | |
309 | + } | |
310 | + | |
311 | + function unsubscribeFromAlarms(alarmSourceListener) { | |
312 | + if (alarmSourceListener && alarmSourceListener.id) { | |
313 | + if (alarmSourceListener.pollPromise) { | |
314 | + $interval.cancel(alarmSourceListener.pollPromise); | |
315 | + alarmSourceListener.pollPromise = null; | |
316 | + } | |
317 | + delete alarmSourceListeners[alarmSourceListener.id]; | |
318 | + } | |
319 | + } | |
214 | 320 | } | ... | ... |
... | ... | @@ -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,45 @@ 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 | + | |
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 | + | |
79 | + this.alarmSourceListener = null; | |
80 | + this.alarms = []; | |
81 | + | |
82 | + this.originalTimewindow = null; | |
83 | + this.timeWindow = { | |
84 | + stDiff: this.ctx.stDiff | |
85 | + } | |
86 | + this.useDashboardTimewindow = options.useDashboardTimewindow; | |
87 | + | |
88 | + if (this.useDashboardTimewindow) { | |
89 | + this.timeWindowConfig = angular.copy(options.dashboardTimewindow); | |
90 | + } else { | |
91 | + this.timeWindowConfig = angular.copy(options.timeWindowConfig); | |
92 | + } | |
93 | + | |
94 | + this.subscriptionTimewindow = null; | |
95 | + | |
96 | + this.loadingData = false; | |
97 | + this.displayLegend = false; | |
98 | + this.initAlarmSubscription().then( | |
99 | + function success() { | |
100 | + deferred.resolve(subscription); | |
101 | + }, | |
102 | + function fail() { | |
103 | + deferred.reject(); | |
104 | + } | |
105 | + ); | |
67 | 106 | } else { |
68 | 107 | this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; |
69 | 108 | this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; |
... | ... | @@ -132,6 +171,43 @@ export default class Subscription { |
132 | 171 | return deferred.promise; |
133 | 172 | } |
134 | 173 | |
174 | + initAlarmSubscription() { | |
175 | + var deferred = this.ctx.$q.defer(); | |
176 | + if (!this.ctx.aliasController) { | |
177 | + this.configureAlarmsData(); | |
178 | + deferred.resolve(); | |
179 | + } else { | |
180 | + var subscription = this; | |
181 | + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).then( | |
182 | + function success(alarmSource) { | |
183 | + subscription.alarmSource = alarmSource; | |
184 | + subscription.configureAlarmsData(); | |
185 | + deferred.resolve(); | |
186 | + }, | |
187 | + function fail() { | |
188 | + deferred.reject(); | |
189 | + } | |
190 | + ); | |
191 | + } | |
192 | + return deferred.promise; | |
193 | + } | |
194 | + | |
195 | + configureAlarmsData() { | |
196 | + var subscription = this; | |
197 | + var registration; | |
198 | + if (this.useDashboardTimewindow) { | |
199 | + registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { | |
200 | + if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { | |
201 | + subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); | |
202 | + subscription.update(); | |
203 | + } | |
204 | + }); | |
205 | + this.registrations.push(registration); | |
206 | + } else { | |
207 | + this.startWatchingTimewindow(); | |
208 | + } | |
209 | + } | |
210 | + | |
135 | 211 | initDataSubscription() { |
136 | 212 | var deferred = this.ctx.$q.defer(); |
137 | 213 | if (!this.ctx.aliasController) { |
... | ... | @@ -210,8 +286,7 @@ export default class Subscription { |
210 | 286 | registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { |
211 | 287 | if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { |
212 | 288 | subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); |
213 | - subscription.unsubscribe(); | |
214 | - subscription.subscribe(); | |
289 | + subscription.update(); | |
215 | 290 | } |
216 | 291 | }); |
217 | 292 | this.registrations.push(registration); |
... | ... | @@ -227,8 +302,7 @@ export default class Subscription { |
227 | 302 | return subscription.timeWindowConfig; |
228 | 303 | }, function (newTimewindow, prevTimewindow) { |
229 | 304 | if (!angular.equals(newTimewindow, prevTimewindow)) { |
230 | - subscription.unsubscribe(); | |
231 | - subscription.subscribe(); | |
305 | + subscription.update(); | |
232 | 306 | } |
233 | 307 | }, true); |
234 | 308 | this.registrations.push(this.timeWindowWatchRegistration); |
... | ... | @@ -393,6 +467,8 @@ export default class Subscription { |
393 | 467 | onAliasesChanged(aliasIds) { |
394 | 468 | if (this.type === this.ctx.types.widgetType.rpc.value) { |
395 | 469 | return this.checkRpcTarget(aliasIds); |
470 | + } else if (this.type === this.ctx.types.widgetType.alarm.value) { | |
471 | + return this.checkAlarmSource(aliasIds); | |
396 | 472 | } else { |
397 | 473 | return this.checkSubscriptions(aliasIds); |
398 | 474 | } |
... | ... | @@ -429,8 +505,7 @@ export default class Subscription { |
429 | 505 | this.timeWindowConfig = angular.copy(this.originalTimewindow); |
430 | 506 | this.originalTimewindow = null; |
431 | 507 | this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); |
432 | - this.unsubscribe(); | |
433 | - this.subscribe(); | |
508 | + this.update(); | |
434 | 509 | this.startWatchingTimewindow(); |
435 | 510 | } |
436 | 511 | } |
... | ... | @@ -446,8 +521,7 @@ export default class Subscription { |
446 | 521 | } |
447 | 522 | this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); |
448 | 523 | this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); |
449 | - this.unsubscribe(); | |
450 | - this.subscribe(); | |
524 | + this.update(); | |
451 | 525 | this.startWatchingTimewindow(); |
452 | 526 | } |
453 | 527 | } |
... | ... | @@ -516,6 +590,15 @@ export default class Subscription { |
516 | 590 | } |
517 | 591 | } |
518 | 592 | |
593 | + alarmsUpdated(alarms, apply) { | |
594 | + this.notifyDataLoaded(); | |
595 | + this.alarms = alarms; | |
596 | + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { | |
597 | + this.updateTimewindow(); | |
598 | + } | |
599 | + this.onDataUpdated(apply); | |
600 | + } | |
601 | + | |
519 | 602 | updateLegend(dataIndex, data, apply) { |
520 | 603 | var dataKey = this.legendData.keys[dataIndex].dataKey; |
521 | 604 | var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; |
... | ... | @@ -536,66 +619,115 @@ export default class Subscription { |
536 | 619 | this.callbacks.legendDataUpdated(this, apply !== false); |
537 | 620 | } |
538 | 621 | |
622 | + update() { | |
623 | + this.unsubscribe(); | |
624 | + this.subscribe(); | |
625 | + } | |
626 | + | |
539 | 627 | subscribe() { |
540 | 628 | if (this.type === this.ctx.types.widgetType.rpc.value) { |
541 | 629 | return; |
542 | 630 | } |
631 | + if (this.type === this.ctx.types.widgetType.alarm.value) { | |
632 | + this.alarmsSubscribe(); | |
633 | + } else { | |
634 | + this.notifyDataLoading(); | |
635 | + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { | |
636 | + this.updateRealtimeSubscription(); | |
637 | + if (this.subscriptionTimewindow.fixedWindow) { | |
638 | + this.onDataUpdated(); | |
639 | + } | |
640 | + } | |
641 | + var index = 0; | |
642 | + for (var i = 0; i < this.datasources.length; i++) { | |
643 | + var datasource = this.datasources[i]; | |
644 | + if (angular.isFunction(datasource)) | |
645 | + continue; | |
646 | + | |
647 | + var subscription = this; | |
648 | + | |
649 | + var listener = { | |
650 | + subscriptionType: this.type, | |
651 | + subscriptionTimewindow: this.subscriptionTimewindow, | |
652 | + datasource: datasource, | |
653 | + entityType: datasource.entityType, | |
654 | + entityId: datasource.entityId, | |
655 | + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { | |
656 | + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); | |
657 | + }, | |
658 | + updateRealtimeSubscription: function () { | |
659 | + this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); | |
660 | + return this.subscriptionTimewindow; | |
661 | + }, | |
662 | + setRealtimeSubscription: function (subscriptionTimewindow) { | |
663 | + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); | |
664 | + }, | |
665 | + datasourceIndex: index | |
666 | + }; | |
667 | + | |
668 | + for (var a = 0; a < datasource.dataKeys.length; a++) { | |
669 | + this.data[index + a].data = []; | |
670 | + } | |
671 | + | |
672 | + index += datasource.dataKeys.length; | |
673 | + | |
674 | + this.datasourceListeners.push(listener); | |
675 | + this.ctx.datasourceService.subscribeToDatasource(listener); | |
676 | + if (datasource.unresolvedStateEntity) { | |
677 | + this.notifyDataLoaded(); | |
678 | + this.onDataUpdated(); | |
679 | + } | |
680 | + | |
681 | + } | |
682 | + } | |
683 | + } | |
684 | + | |
685 | + alarmsSubscribe() { | |
543 | 686 | this.notifyDataLoading(); |
544 | - if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { | |
687 | + if (this.timeWindowConfig) { | |
545 | 688 | this.updateRealtimeSubscription(); |
546 | 689 | if (this.subscriptionTimewindow.fixedWindow) { |
547 | 690 | this.onDataUpdated(); |
548 | 691 | } |
549 | 692 | } |
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 = []; | |
693 | + var subscription = this; | |
694 | + this.alarmSourceListener = { | |
695 | + subscriptionTimewindow: this.subscriptionTimewindow, | |
696 | + alarmSource: this.alarmSource, | |
697 | + alarmSearchStatus: this.alarmSearchStatus, | |
698 | + alarmsPollingInterval: this.alarmsPollingInterval, | |
699 | + alarmsUpdated: function(alarms, apply) { | |
700 | + subscription.alarmsUpdated(alarms, apply); | |
579 | 701 | } |
702 | + } | |
703 | + this.alarms = []; | |
580 | 704 | |
581 | - index += datasource.dataKeys.length; | |
705 | + this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener); | |
582 | 706 | |
583 | - this.datasourceListeners.push(listener); | |
584 | - this.ctx.datasourceService.subscribeToDatasource(listener); | |
585 | - if (datasource.unresolvedStateEntity) { | |
586 | - this.notifyDataLoaded(); | |
587 | - this.onDataUpdated(); | |
588 | - } | |
707 | + if (this.alarmSource.unresolvedStateEntity) { | |
708 | + this.notifyDataLoaded(); | |
709 | + this.onDataUpdated(); | |
589 | 710 | } |
590 | 711 | } |
591 | 712 | |
592 | 713 | unsubscribe() { |
593 | 714 | 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); | |
715 | + if (this.type == this.ctx.types.widgetType.alarm.value) { | |
716 | + this.alarmsUnsubscribe(); | |
717 | + } else { | |
718 | + for (var i = 0; i < this.datasourceListeners.length; i++) { | |
719 | + var listener = this.datasourceListeners[i]; | |
720 | + this.ctx.datasourceService.unsubscribeFromDatasource(listener); | |
721 | + } | |
722 | + this.datasourceListeners = []; | |
597 | 723 | } |
598 | - this.datasourceListeners = []; | |
724 | + } | |
725 | + } | |
726 | + | |
727 | + alarmsUnsubscribe() { | |
728 | + if (this.alarmSourceListener) { | |
729 | + this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener); | |
730 | + this.alarmSourceListener = null; | |
599 | 731 | } |
600 | 732 | } |
601 | 733 | |
... | ... | @@ -607,6 +739,14 @@ export default class Subscription { |
607 | 739 | } |
608 | 740 | } |
609 | 741 | |
742 | + checkAlarmSource(aliasIds) { | |
743 | + if (this.alarmSource && this.alarmSource.entityAliasId) { | |
744 | + return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1; | |
745 | + } else { | |
746 | + return false; | |
747 | + } | |
748 | + } | |
749 | + | |
610 | 750 | checkSubscriptions(aliasIds) { |
611 | 751 | var subscriptionsChanged = false; |
612 | 752 | 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 | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)" |
32 | 32 | on-delete-asset="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-asset> |
33 | 33 | </md-tab> |
34 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}"> | |
34 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}"> | |
35 | 35 | <tb-attribute-table flex |
36 | 36 | entity-id="vm.grid.operatingItem().id.id" |
37 | 37 | entity-type="{{vm.types.entityType.asset}}" |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | default-attribute-scope="{{vm.types.attributesScope.server.value}}"> |
40 | 40 | </tb-attribute-table> |
41 | 41 | </md-tab> |
42 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
42 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
43 | 43 | <tb-attribute-table flex |
44 | 44 | entity-id="vm.grid.operatingItem().id.id" |
45 | 45 | entity-type="{{vm.types.entityType.asset}}" |
... | ... | @@ -48,19 +48,19 @@ |
48 | 48 | disable-attribute-scope-selection="true"> |
49 | 49 | </tb-attribute-table> |
50 | 50 | </md-tab> |
51 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}"> | |
51 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}"> | |
52 | 52 | <tb-alarm-table flex entity-type="vm.types.entityType.asset" |
53 | 53 | entity-id="vm.grid.operatingItem().id.id"> |
54 | 54 | </tb-alarm-table> |
55 | 55 | </md-tab> |
56 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'asset.events' | translate }}"> | |
56 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'asset.events' | translate }}"> | |
57 | 57 | <tb-event-table flex entity-type="vm.types.entityType.asset" |
58 | 58 | entity-id="vm.grid.operatingItem().id.id" |
59 | 59 | tenant-id="vm.grid.operatingItem().tenantId.id" |
60 | 60 | default-event-type="{{vm.types.eventType.error.value}}"> |
61 | 61 | </tb-event-table> |
62 | 62 | </md-tab> |
63 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}"> | |
63 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> | |
64 | 64 | <tb-relation-table flex |
65 | 65 | entity-id="vm.grid.operatingItem().id.id" |
66 | 66 | entity-type="{{vm.types.entityType.asset}}"> | ... | ... |
... | ... | @@ -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", |
... | ... | @@ -333,7 +394,7 @@ export default angular.module('thingsboard.types', []) |
333 | 394 | cards: "cards" |
334 | 395 | }, |
335 | 396 | translate: { |
336 | - dashboardStatePrefix: "dashboardState.state." | |
397 | + customTranslationsPrefix: "custom." | |
337 | 398 | } |
338 | 399 | } |
339 | 400 | ).name; | ... | ... |
... | ... | @@ -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,9 @@ 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, | |
139 | + insertVariable: insertVariable | |
113 | 140 | } |
114 | 141 | |
115 | 142 | return service; |
... | ... | @@ -212,6 +239,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
212 | 239 | return angular.toJson(getDefaultDatasource(dataKeySchema)); |
213 | 240 | } |
214 | 241 | |
242 | + function getDefaultAlarmDataKeys() { | |
243 | + return angular.copy(defaultAlarmDataKeys); | |
244 | + } | |
245 | + | |
215 | 246 | function isDescriptorSchemaNotEmpty(descriptor) { |
216 | 247 | if (descriptor && descriptor.schema && descriptor.schema.properties) { |
217 | 248 | for(var prop in descriptor.schema.properties) { |
... | ... | @@ -357,4 +388,38 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
357 | 388 | return dataKey; |
358 | 389 | } |
359 | 390 | |
391 | + function createLabelFromDatasource(datasource, pattern) { | |
392 | + var label = angular.copy(pattern); | |
393 | + var match = varsRegex.exec(pattern); | |
394 | + while (match !== null) { | |
395 | + var variable = match[0]; | |
396 | + var variableName = match[1]; | |
397 | + if (variableName === 'dsName') { | |
398 | + label = label.split(variable).join(datasource.name); | |
399 | + } else if (variableName === 'entityName') { | |
400 | + label = label.split(variable).join(datasource.entityName); | |
401 | + } else if (variableName === 'deviceName') { | |
402 | + label = label.split(variable).join(datasource.entityName); | |
403 | + } else if (variableName === 'aliasName') { | |
404 | + label = label.split(variable).join(datasource.aliasName); | |
405 | + } | |
406 | + match = varsRegex.exec(pattern); | |
407 | + } | |
408 | + return label; | |
409 | + } | |
410 | + | |
411 | + function insertVariable(pattern, name, value) { | |
412 | + var result = angular.copy(pattern); | |
413 | + var match = varsRegex.exec(pattern); | |
414 | + while (match !== null) { | |
415 | + var variable = match[0]; | |
416 | + var variableName = match[1]; | |
417 | + if (variableName === name) { | |
418 | + result = result.split(variable).join(value); | |
419 | + } | |
420 | + match = varsRegex.exec(pattern); | |
421 | + } | |
422 | + return result; | |
423 | + } | |
424 | + | |
360 | 425 | } | ... | ... |
... | ... | @@ -58,6 +58,7 @@ function Dashboard() { |
58 | 58 | columns: '=', |
59 | 59 | margins: '=', |
60 | 60 | isEdit: '=', |
61 | + autofillHeight: '=', | |
61 | 62 | isMobile: '=', |
62 | 63 | isMobileDisabled: '=?', |
63 | 64 | isEditActionEnabled: '=', |
... | ... | @@ -102,6 +103,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
102 | 103 | |
103 | 104 | vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; |
104 | 105 | |
106 | + vm.isMobileSize = false; | |
107 | + | |
105 | 108 | if (!('dashboardTimewindow' in vm)) { |
106 | 109 | vm.dashboardTimewindow = timeService.defaultTimewindow(); |
107 | 110 | } |
... | ... | @@ -177,10 +180,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
177 | 180 | vm.widgetBackgroundColor = widgetBackgroundColor; |
178 | 181 | vm.widgetPadding = widgetPadding; |
179 | 182 | vm.showWidgetTitle = showWidgetTitle; |
183 | + vm.showWidgetTitlePanel = showWidgetTitlePanel; | |
184 | + vm.showWidgetActions = showWidgetActions; | |
180 | 185 | vm.widgetTitleStyle = widgetTitleStyle; |
186 | + vm.widgetTitle = widgetTitle; | |
187 | + vm.widgetActions = widgetActions; | |
181 | 188 | vm.dropWidgetShadow = dropWidgetShadow; |
182 | 189 | vm.enableWidgetFullscreen = enableWidgetFullscreen; |
183 | 190 | vm.hasTimewindow = hasTimewindow; |
191 | + vm.hasAggregation = hasAggregation; | |
184 | 192 | vm.editWidget = editWidget; |
185 | 193 | vm.exportWidget = exportWidget; |
186 | 194 | vm.removeWidget = removeWidget; |
... | ... | @@ -267,12 +275,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
267 | 275 | |
268 | 276 | function updateMobileOpts() { |
269 | 277 | var isMobileDisabled = vm.isMobileDisabled === true; |
270 | - var isMobile = vm.isMobile === true && !isMobileDisabled; | |
278 | + var isMobile = vm.isMobile === true && !isMobileDisabled || vm.autofillHeight; | |
271 | 279 | var mobileBreakPoint = isMobileDisabled ? 0 : (isMobile ? 20000 : 960); |
280 | + | |
272 | 281 | if (!isMobile && !isMobileDisabled) { |
273 | 282 | isMobile = !$mdMedia('gt-sm'); |
274 | 283 | } |
275 | - var rowHeight = isMobile ? 70 : 'match'; | |
284 | + | |
285 | + var rowHeight = detectRowSize(isMobile); | |
286 | + | |
276 | 287 | if (vm.gridsterOpts.isMobile != isMobile) { |
277 | 288 | vm.gridsterOpts.isMobile = isMobile; |
278 | 289 | vm.gridsterOpts.mobileModeEnabled = isMobile; |
... | ... | @@ -283,6 +294,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
283 | 294 | if (vm.gridsterOpts.rowHeight != rowHeight) { |
284 | 295 | vm.gridsterOpts.rowHeight = rowHeight; |
285 | 296 | } |
297 | + | |
298 | + vm.isMobileSize = checkIsMobileSize(); | |
299 | + } | |
300 | + | |
301 | + function checkIsMobileSize() { | |
302 | + var isMobileDisabled = vm.isMobileDisabled === true; | |
303 | + var isMobileSize = vm.isMobile === true && !isMobileDisabled; | |
304 | + if (!isMobileSize && !isMobileDisabled) { | |
305 | + isMobileSize = !$mdMedia('gt-sm'); | |
306 | + } | |
307 | + return isMobileSize; | |
286 | 308 | } |
287 | 309 | |
288 | 310 | $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() { |
... | ... | @@ -293,6 +315,34 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
293 | 315 | updateMobileOpts(); |
294 | 316 | }); |
295 | 317 | |
318 | + $scope.$watch('vm.autofillHeight', function () { | |
319 | + if (vm.autofillHeight) { | |
320 | + //if (gridsterParent.height()) { | |
321 | + // updateMobileOpts(); | |
322 | + //} else { | |
323 | + if ($scope.parentHeighWatcher) { | |
324 | + $scope.parentHeighWatcher(); | |
325 | + } | |
326 | + if (gridsterParent.height()) { | |
327 | + updateMobileOpts(); | |
328 | + } | |
329 | + $scope.parentHeighWatcher = $scope.$watch(function() { return gridsterParent.height(); }, | |
330 | + function(newHeight) { | |
331 | + if (newHeight) { | |
332 | + updateMobileOpts(); | |
333 | + } | |
334 | + } | |
335 | + ); | |
336 | + } else { | |
337 | + if ($scope.parentHeighWatcher) { | |
338 | + $scope.parentHeighWatcher(); | |
339 | + $scope.parentHeighWatcher = null; | |
340 | + } | |
341 | + | |
342 | + updateMobileOpts(); | |
343 | + } | |
344 | + }); | |
345 | + | |
296 | 346 | $scope.$watch('vm.isMobileDisabled', function () { |
297 | 347 | updateMobileOpts(); |
298 | 348 | }); |
... | ... | @@ -329,6 +379,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
329 | 379 | $scope.$broadcast('toggleDashboardEditMode', vm.isEdit); |
330 | 380 | }); |
331 | 381 | |
382 | + $scope.$watch('vm.isMobileSize', function (newVal, prevVal) { | |
383 | + if (!angular.equals(newVal, prevVal)) { | |
384 | + $scope.$broadcast('mobileModeChanged', vm.isMobileSize); | |
385 | + } | |
386 | + }); | |
387 | + | |
332 | 388 | $scope.$on('gridster-resized', function (event, sizes, theGridster) { |
333 | 389 | if (checkIsLocalGridsterElement(theGridster)) { |
334 | 390 | vm.gridster = theGridster; |
... | ... | @@ -341,13 +397,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
341 | 397 | $scope.$on('gridster-mobile-changed', function (event, theGridster) { |
342 | 398 | if (checkIsLocalGridsterElement(theGridster)) { |
343 | 399 | vm.gridster = theGridster; |
344 | - var rowHeight = vm.gridster.isMobile ? 70 : 'match'; | |
400 | + var rowHeight = detectRowSize(vm.gridster.isMobile); | |
345 | 401 | if (vm.gridsterOpts.rowHeight != rowHeight) { |
346 | 402 | vm.gridsterOpts.rowHeight = rowHeight; |
347 | 403 | updateGridsterParams(); |
348 | 404 | } |
349 | - | |
350 | - $scope.$broadcast('mobileModeChanged', vm.gridster.isMobile); | |
405 | + vm.isMobileSize = checkIsMobileSize(); | |
351 | 406 | |
352 | 407 | //TODO: widgets visibility |
353 | 408 | /*$timeout(function () { |
... | ... | @@ -356,6 +411,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
356 | 411 | } |
357 | 412 | }); |
358 | 413 | |
414 | + function detectRowSize(isMobile) { | |
415 | + var rowHeight = isMobile ? 70 : 'match'; | |
416 | + if (vm.autofillHeight) { | |
417 | + var viewportHeight = gridsterParent.height(); | |
418 | + var totalRows = 0; | |
419 | + for (var i = 0; i < vm.widgets.length; i++) { | |
420 | + var w = vm.widgets[i]; | |
421 | + var sizeY = widgetSizeY(w); | |
422 | + totalRows += sizeY; | |
423 | + } | |
424 | + rowHeight = (viewportHeight - (vm.gridsterOpts.margins[1])) / totalRows; | |
425 | + } | |
426 | + return rowHeight; | |
427 | + } | |
428 | + | |
359 | 429 | function widgetOrder(widget) { |
360 | 430 | var order; |
361 | 431 | if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { |
... | ... | @@ -646,7 +716,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
646 | 716 | } |
647 | 717 | |
648 | 718 | function widgetSizeY(widget) { |
649 | - if (vm.gridsterOpts.isMobile) { | |
719 | + if (vm.gridsterOpts.isMobile && !vm.autofillHeight) { | |
650 | 720 | var mobileHeight; |
651 | 721 | if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { |
652 | 722 | mobileHeight = vm.widgetLayouts[widget.id].mobileHeight; |
... | ... | @@ -669,7 +739,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
669 | 739 | } |
670 | 740 | |
671 | 741 | function setWidgetSizeY(widget, sizeY) { |
672 | - if (!vm.gridsterOpts.isMobile) { | |
742 | + if (!vm.gridsterOpts.isMobile && !vm.autofillHeight) { | |
673 | 743 | if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { |
674 | 744 | vm.widgetLayouts[widget.id].sizeY = sizeY; |
675 | 745 | } else { |
... | ... | @@ -746,6 +816,24 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
746 | 816 | } |
747 | 817 | } |
748 | 818 | |
819 | + function showWidgetTitlePanel(widget) { | |
820 | + var ctx = widgetContext(widget); | |
821 | + if (ctx && ctx.hideTitlePanel) { | |
822 | + return false; | |
823 | + } else { | |
824 | + return showWidgetTitle(widget) || hasTimewindow(widget); | |
825 | + } | |
826 | + } | |
827 | + | |
828 | + function showWidgetActions(widget) { | |
829 | + var ctx = widgetContext(widget); | |
830 | + if (ctx && ctx.hideTitlePanel) { | |
831 | + return false; | |
832 | + } else { | |
833 | + return true; | |
834 | + } | |
835 | + } | |
836 | + | |
749 | 837 | function widgetTitleStyle(widget) { |
750 | 838 | if (angular.isDefined(widget.config.titleStyle)) { |
751 | 839 | return widget.config.titleStyle; |
... | ... | @@ -754,6 +842,33 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
754 | 842 | } |
755 | 843 | } |
756 | 844 | |
845 | + function widgetTitle(widget) { | |
846 | + var ctx = widgetContext(widget); | |
847 | + if (ctx && ctx.widgetTitle | |
848 | + && ctx.widgetTitle.length) { | |
849 | + return ctx.widgetTitle; | |
850 | + } else { | |
851 | + return widget.config.title; | |
852 | + } | |
853 | + } | |
854 | + | |
855 | + function widgetActions(widget) { | |
856 | + var ctx = widgetContext(widget); | |
857 | + if (ctx && ctx.widgetActions && ctx.widgetActions.length) { | |
858 | + return ctx.widgetActions; | |
859 | + } else { | |
860 | + return []; | |
861 | + } | |
862 | + } | |
863 | + | |
864 | + function widgetContext(widget) { | |
865 | + var context; | |
866 | + if (widget.$ctx) { | |
867 | + context = widget.$ctx(); | |
868 | + } | |
869 | + return context; | |
870 | + } | |
871 | + | |
757 | 872 | function dropWidgetShadow(widget) { |
758 | 873 | if (angular.isDefined(widget.config.dropShadow)) { |
759 | 874 | return widget.config.dropShadow; |
... | ... | @@ -771,7 +886,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
771 | 886 | } |
772 | 887 | |
773 | 888 | function hasTimewindow(widget) { |
774 | - if (widget.type === types.widgetType.timeseries.value) { | |
889 | + if (widget.type === types.widgetType.timeseries.value || widget.type === types.widgetType.alarm.value) { | |
775 | 890 | return angular.isDefined(widget.config.useDashboardTimewindow) ? |
776 | 891 | !widget.config.useDashboardTimewindow : false; |
777 | 892 | } else { |
... | ... | @@ -779,6 +894,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ |
779 | 894 | } |
780 | 895 | } |
781 | 896 | |
897 | + function hasAggregation(widget) { | |
898 | + return widget.type === types.widgetType.timeseries.value; | |
899 | + } | |
900 | + | |
782 | 901 | function adoptMaxRows() { |
783 | 902 | if (vm.widgets) { |
784 | 903 | var maxRows = vm.gridsterOpts.maxRows; | ... | ... |
... | ... | @@ -23,7 +23,7 @@ |
23 | 23 | </md-content> |
24 | 24 | <md-menu md-position-mode="target target" tb-mousepoint-menu> |
25 | 25 | <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)"> |
26 | - <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%;"> | |
26 | + <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> | |
27 | 27 | <div id="gridster-child" gridster="vm.gridsterOpts"> |
28 | 28 | <ul> |
29 | 29 | <li gridster-item="vm.widgetItemMap" class="tb-noselect" ng-repeat="widget in vm.widgets"> |
... | ... | @@ -37,7 +37,8 @@ |
37 | 37 | class="tb-widget" |
38 | 38 | ng-class="{'tb-highlighted': vm.isHighlighted(widget), |
39 | 39 | 'tb-not-highlighted': vm.isNotHighlighted(widget), |
40 | - 'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}" | |
40 | + 'md-whiteframe-4dp': vm.dropWidgetShadow(widget), | |
41 | + 'tb-has-timewindow': vm.hasTimewindow(widget)}" | |
41 | 42 | tb-mousedown="vm.widgetMouseDown($event, widget)" |
42 | 43 | ng-click="vm.widgetClicked($event, widget)" |
43 | 44 | tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)" |
... | ... | @@ -45,11 +46,21 @@ |
45 | 46 | color: vm.widgetColor(widget), |
46 | 47 | backgroundColor: vm.widgetBackgroundColor(widget), |
47 | 48 | padding: vm.widgetPadding(widget)}"> |
48 | - <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> | |
49 | - <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span> | |
50 | - <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> | |
49 | + <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitlePanel(widget)"> | |
50 | + <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{vm.widgetTitle(widget)}}</span> | |
51 | + <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> | |
51 | 52 | </div> |
52 | - <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()"> | |
53 | + <div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()"> | |
54 | + <md-button ng-repeat="action in vm.widgetActions(widget)" | |
55 | + aria-label="{{ action.name | translate }}" | |
56 | + ng-show="!vm.isEdit && action.show" | |
57 | + ng-click="action.onAction($event)" | |
58 | + class="md-icon-button"> | |
59 | + <md-tooltip md-direction="top"> | |
60 | + {{ action.name | translate }} | |
61 | + </md-tooltip> | |
62 | + <ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon> | |
63 | + </md-button> | |
53 | 64 | <md-button id="expand-button" |
54 | 65 | ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)" |
55 | 66 | aria-label="{{ 'fullscreen.fullscreen' | translate }}" |
... | ... | @@ -92,6 +103,7 @@ |
92 | 103 | aliasController: vm.aliasController, |
93 | 104 | stateController: vm.stateController, |
94 | 105 | isEdit: vm.isEdit, |
106 | + isMobile: vm.isMobileSize, | |
95 | 107 | stDiff: vm.stDiff, |
96 | 108 | dashboardTimewindow: vm.dashboardTimewindow, |
97 | 109 | dashboardTimewindowApi: vm.dashboardTimewindowApi }"> | ... | ... |
... | ... | @@ -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"> | ... | ... |
... | ... | @@ -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); | ... | ... |
... | ... | @@ -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; | ... | ... |
... | ... | @@ -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,63 @@ |
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="false"> | |
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 | + <span> | |
156 | + <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a> | |
157 | + </span> | |
158 | + </div> | |
159 | + </div> | |
160 | + </md-not-found> | |
161 | + </md-autocomplete> | |
162 | + <md-chip-template> | |
163 | + <div layout="row" layout-align="start center" class="tb-attribute-chip"> | |
164 | + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | |
165 | + <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | |
166 | + </div> | |
167 | + <div layout="row" flex> | |
168 | + <div class="tb-chip-label"> | |
169 | + {{$chip.label}} | |
170 | + </div> | |
171 | + <div class="tb-chip-separator">: </div> | |
172 | + <div class="tb-chip-label"> | |
173 | + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong> | |
174 | + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong> | |
175 | + </div> | |
176 | + </div> | |
177 | + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | |
178 | + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | |
179 | + </md-button> | |
180 | + </div> | |
181 | + </md-chip-template> | |
182 | + </md-chips> | |
131 | 183 | </section> |
132 | 184 | <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> |
133 | 185 | <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div> |
134 | 186 | <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div> |
187 | + <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div> | |
135 | 188 | </div> |
136 | 189 | </section> |
137 | 190 | </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" |
... | ... | @@ -47,9 +48,9 @@ |
47 | 48 | <span translate>device.no-keys-found</span> |
48 | 49 | </div> |
49 | 50 | <div ng-if="textIsNotEmpty(dataKeySearchText)"> |
50 | - <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> | |
51 | 52 | <span> |
52 | - <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> | |
53 | 54 | </span> |
54 | 55 | </div> |
55 | 56 | </div> |
... | ... | @@ -75,8 +76,61 @@ |
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="false"> | |
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 | + <span> | |
104 | + <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a> | |
105 | + </span> | |
106 | + </div> | |
107 | + </div> | |
108 | + </md-not-found> | |
109 | + </md-autocomplete> | |
110 | + <md-chip-template> | |
111 | + <div layout="row" layout-align="start center" class="tb-attribute-chip"> | |
112 | + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | |
113 | + <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | |
114 | + </div> | |
115 | + <div layout="row" flex> | |
116 | + <div class="tb-chip-label"> | |
117 | + {{$chip.label}} | |
118 | + </div> | |
119 | + <div class="tb-chip-separator">: </div> | |
120 | + <div class="tb-chip-label"> | |
121 | + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong> | |
122 | + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong> | |
123 | + </div> | |
124 | + </div> | |
125 | + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | |
126 | + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | |
127 | + </md-button> | |
128 | + </div> | |
129 | + </md-chip-template> | |
130 | + </md-chips> | |
78 | 131 | <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> | |
132 | + <div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div> | |
133 | + <div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div> | |
80 | 134 | </div> |
81 | 135 | </section> |
82 | 136 | </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 | ... | ... |
... | ... | @@ -124,7 +124,7 @@ function Grid() { |
124 | 124 | } |
125 | 125 | |
126 | 126 | /*@ngInject*/ |
127 | -function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache) { | |
127 | +function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache, $window) { | |
128 | 128 | |
129 | 129 | var vm = this; |
130 | 130 | |
... | ... | @@ -155,6 +155,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra |
155 | 155 | vm.refreshList = refreshList; |
156 | 156 | vm.saveItem = saveItem; |
157 | 157 | vm.toggleItemSelection = toggleItemSelection; |
158 | + vm.triggerResize = triggerResize; | |
158 | 159 | |
159 | 160 | $scope.$watch(function () { |
160 | 161 | return $mdMedia('xs') || $mdMedia('sm'); |
... | ... | @@ -600,6 +601,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra |
600 | 601 | } |
601 | 602 | } |
602 | 603 | |
604 | + function triggerResize() { | |
605 | + var w = angular.element($window); | |
606 | + w.triggerHandler('resize'); | |
607 | + } | |
608 | + | |
603 | 609 | function moveToTop() { |
604 | 610 | moveToIndex(0, true); |
605 | 611 | } | ... | ... |
... | ... | @@ -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,16 @@ 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 | + scope.alarmSearchStatus = angular.isDefined(config.alarmSearchStatus) ? | |
148 | + config.alarmSearchStatus : types.alarmSearchStatus.any; | |
149 | + scope.alarmsPollingInterval = angular.isDefined(config.alarmsPollingInterval) ? | |
150 | + config.alarmsPollingInterval : 5; | |
151 | + if (config.alarmSource) { | |
152 | + scope.alarmSource.value = config.alarmSource; | |
153 | + } else { | |
154 | + scope.alarmSource.value = null; | |
155 | + } | |
140 | 156 | } |
141 | 157 | |
142 | 158 | scope.settings = config.settings; |
... | ... | @@ -175,6 +191,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
175 | 191 | if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { |
176 | 192 | valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; |
177 | 193 | ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); |
194 | + } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { | |
195 | + valid = config && config.alarmSource; | |
196 | + ngModelCtrl.$setValidity('alarmSource', valid); | |
178 | 197 | } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { |
179 | 198 | valid = config && config.datasources && config.datasources.length > 0; |
180 | 199 | ngModelCtrl.$setValidity('datasources', valid); |
... | ... | @@ -190,7 +209,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
190 | 209 | }; |
191 | 210 | |
192 | 211 | scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' + |
193 | - 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () { | |
212 | + 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' + | |
213 | + 'alarmSearchStatus + alarmsPollingInterval + showLegend', function () { | |
194 | 214 | if (ngModelCtrl.$viewValue) { |
195 | 215 | var value = ngModelCtrl.$viewValue; |
196 | 216 | if (value.config) { |
... | ... | @@ -210,6 +230,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
210 | 230 | config.units = scope.units; |
211 | 231 | config.decimals = scope.decimals; |
212 | 232 | config.useDashboardTimewindow = scope.useDashboardTimewindow; |
233 | + config.alarmSearchStatus = scope.alarmSearchStatus; | |
234 | + config.alarmsPollingInterval = scope.alarmsPollingInterval; | |
213 | 235 | config.showLegend = scope.showLegend; |
214 | 236 | } |
215 | 237 | if (value.layout) { |
... | ... | @@ -253,7 +275,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
253 | 275 | }, true); |
254 | 276 | |
255 | 277 | scope.$watch('datasources', function () { |
256 | - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value | |
278 | + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config | |
279 | + && scope.widgetType !== types.widgetType.rpc.value | |
280 | + && scope.widgetType !== types.widgetType.alarm.value | |
257 | 281 | && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { |
258 | 282 | var value = ngModelCtrl.$viewValue; |
259 | 283 | var config = value.config; |
... | ... | @@ -286,6 +310,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
286 | 310 | } |
287 | 311 | }); |
288 | 312 | |
313 | + scope.$watch('alarmSource.value', function () { | |
314 | + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { | |
315 | + var value = ngModelCtrl.$viewValue; | |
316 | + var config = value.config; | |
317 | + if (scope.alarmSource.value) { | |
318 | + config.alarmSource = scope.alarmSource.value; | |
319 | + } else { | |
320 | + config.alarmSource = null; | |
321 | + } | |
322 | + ngModelCtrl.$setViewValue(value); | |
323 | + scope.updateValidity(); | |
324 | + } | |
325 | + }); | |
326 | + | |
289 | 327 | scope.addDatasource = function () { |
290 | 328 | var newDatasource; |
291 | 329 | if (scope.functionsOnly) { |
... | ... | @@ -320,10 +358,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
320 | 358 | return chip; |
321 | 359 | } |
322 | 360 | |
361 | + var label = chip; | |
362 | + if (type === types.dataKeyType.alarm) { | |
363 | + var alarmField = types.alarmFields[chip]; | |
364 | + if (alarmField) { | |
365 | + label = $translate.instant(alarmField.name)+''; | |
366 | + } | |
367 | + } | |
368 | + label = scope.genNextLabel(label); | |
369 | + | |
323 | 370 | var result = { |
324 | 371 | name: chip, |
325 | 372 | type: type, |
326 | - label: scope.genNextLabel(chip), | |
373 | + label: label, | |
327 | 374 | color: scope.genNextColor(), |
328 | 375 | settings: {}, |
329 | 376 | _hash: Math.random() |
... | ... | @@ -351,15 +398,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
351 | 398 | var matches = false; |
352 | 399 | do { |
353 | 400 | 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; | |
401 | + var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources; | |
402 | + if (datasources) { | |
403 | + for (var d=0;d<datasources.length;d++) { | |
404 | + var datasource = datasources[d]; | |
405 | + if (datasource && datasource.dataKeys) { | |
406 | + for (var k = 0; k < datasource.dataKeys.length; k++) { | |
407 | + var dataKey = datasource.dataKeys[k]; | |
408 | + if (dataKey.label === label) { | |
409 | + i++; | |
410 | + label = name + ' ' + i; | |
411 | + matches = true; | |
412 | + } | |
363 | 413 | } |
364 | 414 | } |
365 | 415 | } |
... | ... | @@ -371,10 +421,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti |
371 | 421 | scope.genNextColor = function () { |
372 | 422 | var i = 0; |
373 | 423 | 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; | |
424 | + var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources; | |
425 | + if (datasources) { | |
426 | + for (var d=0;d<datasources.length;d++) { | |
427 | + var datasource = datasources[d]; | |
428 | + if (datasource && datasource.dataKeys) { | |
429 | + i += datasource.dataKeys.length; | |
430 | + } | |
378 | 431 | } |
379 | 432 | } |
380 | 433 | return utils.getMaterialColor(i); | ... | ... |
... | ... | @@ -20,18 +20,46 @@ |
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> |
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> | |
33 | 58 | <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" |
34 | - ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled"> | |
59 | + ng-show="widgetType !== types.widgetType.rpc.value | |
60 | + && widgetType !== types.widgetType.alarm.value | |
61 | + && widgetType !== types.widgetType.static.value | |
62 | + && isDataEnabled"> | |
35 | 63 | <v-pane id="datasources-pane" expanded="true"> |
36 | 64 | <v-pane-header> |
37 | 65 | {{ 'widget-config.datasources' | translate }} |
... | ... | @@ -112,6 +140,24 @@ |
112 | 140 | </v-pane-content> |
113 | 141 | </v-pane> |
114 | 142 | </v-accordion> |
143 | + <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default" | |
144 | + ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled"> | |
145 | + <v-pane id="alarm-source-pane" expanded="true"> | |
146 | + <v-pane-header> | |
147 | + {{ 'widget-config.alarm-source' | translate }} | |
148 | + </v-pane-header> | |
149 | + <v-pane-content style="padding: 0 5px;"> | |
150 | + <tb-datasource flex | |
151 | + ng-model="alarmSource.value" | |
152 | + widget-type="widgetType" | |
153 | + functions-only="functionsOnly" | |
154 | + alias-controller="aliasController" | |
155 | + datakey-settings-schema="datakeySettingsSchema" | |
156 | + generate-data-key="generateDataKey(chip,type)" | |
157 | + on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"></tb-datasource> | |
158 | + </v-pane-content> | |
159 | + </v-pane> | |
160 | + </v-accordion> | |
115 | 161 | </md-content> |
116 | 162 | </md-tab> |
117 | 163 | <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, isMobile, stDiff, dashboardTimewindow, | |
25 | 25 | dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { |
26 | 26 | |
27 | 27 | var vm = this; |
... | ... | @@ -44,13 +44,13 @@ 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, |
51 | 50 | height: 0, |
51 | + hideTitlePanel: false, | |
52 | 52 | isEdit: isEdit, |
53 | - isMobile: false, | |
53 | + isMobile: isMobile, | |
54 | 54 | widgetConfig: widget.config, |
55 | 55 | settings: widget.config.settings, |
56 | 56 | units: widget.config.units || '', |
... | ... | @@ -113,6 +113,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
113 | 113 | timeService: timeService, |
114 | 114 | deviceService: deviceService, |
115 | 115 | datasourceService: datasourceService, |
116 | + alarmService: alarmService, | |
116 | 117 | utils: utils, |
117 | 118 | widgetUtils: widgetContext.utils, |
118 | 119 | dashboardTimewindowApi: dashboardTimewindowApi, |
... | ... | @@ -121,6 +122,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
121 | 122 | aliasController: aliasController |
122 | 123 | }; |
123 | 124 | |
125 | + widget.$ctx = function() { | |
126 | + return widgetContext; | |
127 | + } | |
128 | + | |
124 | 129 | var widgetTypeInstance; |
125 | 130 | |
126 | 131 | vm.useCustomDatasources = false; |
... | ... | @@ -285,9 +290,18 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
285 | 290 | var deferred = $q.defer(); |
286 | 291 | if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { |
287 | 292 | options = { |
288 | - type: widget.type, | |
289 | - datasources: angular.copy(widget.config.datasources) | |
290 | - }; | |
293 | + type: widget.type | |
294 | + } | |
295 | + if (widget.type == types.widgetType.alarm.value) { | |
296 | + options.alarmSource = angular.copy(widget.config.alarmSource); | |
297 | + options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ? | |
298 | + widget.config.alarmSearchStatus : types.alarmSearchStatus.any; | |
299 | + options.alarmsPollingInterval = angular.isDefined(widget.config.alarmsPollingInterval) ? | |
300 | + widget.config.alarmsPollingInterval * 1000 : 5000; | |
301 | + } else { | |
302 | + options.datasources = angular.copy(widget.config.datasources) | |
303 | + } | |
304 | + | |
291 | 305 | defaultComponentsOptions(options); |
292 | 306 | |
293 | 307 | createSubscription(options).then( |
... | ... | @@ -320,7 +334,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
320 | 334 | $scope.executingRpcRequest = subscription.executingRpcRequest; |
321 | 335 | }, |
322 | 336 | onRpcSuccess: function(subscription) { |
323 | - $scope.executingRpcRequest = subscription.executingRpcRequest; | |
337 | + $scope.executingRpcRequest = subscription.executingRpcRequest; | |
324 | 338 | $scope.rpcErrorText = subscription.rpcErrorText; |
325 | 339 | $scope.rpcRejection = subscription.rpcRejection; |
326 | 340 | }, |
... | ... | @@ -436,7 +450,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
436 | 450 | widgetContext.$container = $('#container', containerElement); |
437 | 451 | widgetContext.$containerParent = $(containerElement); |
438 | 452 | |
439 | - $compile($element.contents())($scope); | |
453 | + if (widgetSizeDetected) { | |
454 | + widgetContext.$container.css('height', widgetContext.height + 'px'); | |
455 | + widgetContext.$container.css('width', widgetContext.width + 'px'); | |
456 | + } | |
457 | + | |
458 | + widgetContext.$scope = $scope.$new(); | |
459 | + | |
460 | + $compile($element.contents())(widgetContext.$scope); | |
440 | 461 | |
441 | 462 | addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef |
442 | 463 | } |
... | ... | @@ -444,6 +465,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
444 | 465 | function destroyWidgetElement() { |
445 | 466 | removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef |
446 | 467 | $element.html(''); |
468 | + if (widgetContext.$scope) { | |
469 | + widgetContext.$scope.$destroy(); | |
470 | + } | |
447 | 471 | widgetContext.$container = null; |
448 | 472 | widgetContext.$containerParent = null; |
449 | 473 | } |
... | ... | @@ -594,7 +618,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
594 | 618 | |
595 | 619 | function gridsterItemInitialized(item) { |
596 | 620 | if (item && item.gridster) { |
597 | - widgetContext.isMobile = item.gridster.isMobile; | |
598 | 621 | gridsterItemInited = true; |
599 | 622 | onInit(); |
600 | 623 | // gridsterItemElement = $(item.$element); | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)" |
32 | 32 | on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer> |
33 | 33 | </md-tab> |
34 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}"> | |
34 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}"> | |
35 | 35 | <tb-attribute-table flex |
36 | 36 | entity-id="vm.grid.operatingItem().id.id" |
37 | 37 | entity-type="{{vm.types.entityType.customer}}" |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | default-attribute-scope="{{vm.types.attributesScope.server.value}}"> |
40 | 40 | </tb-attribute-table> |
41 | 41 | </md-tab> |
42 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
42 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
43 | 43 | <tb-attribute-table flex |
44 | 44 | entity-id="vm.grid.operatingItem().id.id" |
45 | 45 | entity-type="{{vm.types.entityType.customer}}" |
... | ... | @@ -48,19 +48,19 @@ |
48 | 48 | disable-attribute-scope-selection="true"> |
49 | 49 | </tb-attribute-table> |
50 | 50 | </md-tab> |
51 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}"> | |
51 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}"> | |
52 | 52 | <tb-alarm-table flex entity-type="vm.types.entityType.customer" |
53 | 53 | entity-id="vm.grid.operatingItem().id.id"> |
54 | 54 | </tb-alarm-table> |
55 | 55 | </md-tab> |
56 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'customer.events' | translate }}"> | |
56 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'customer.events' | translate }}"> | |
57 | 57 | <tb-event-table flex entity-type="vm.types.entityType.customer" |
58 | 58 | entity-id="vm.grid.operatingItem().id.id" |
59 | 59 | tenant-id="vm.grid.operatingItem().tenantId.id" |
60 | 60 | default-event-type="{{vm.types.eventType.error.value}}"> |
61 | 61 | </tb-event-table> |
62 | 62 | </md-tab> |
63 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}"> | |
63 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> | |
64 | 64 | <tb-relation-table flex |
65 | 65 | entity-id="vm.grid.operatingItem().id.id" |
66 | 66 | entity-type="{{vm.types.entityType.customer}}"> | ... | ... |
... | ... | @@ -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; | ... | ... |
... | ... | @@ -68,6 +68,7 @@ export default function DashboardSettingsController($scope, $mdDialog, statesCon |
68 | 68 | vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)'; |
69 | 69 | vm.gridSettings.columns = vm.gridSettings.columns || 24; |
70 | 70 | vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; |
71 | + vm.gridSettings.autoFillHeight = angular.isDefined(vm.gridSettings.autoFillHeight) ? vm.gridSettings.autoFillHeight : false; | |
71 | 72 | vm.hMargin = vm.gridSettings.margins[0]; |
72 | 73 | vm.vMargin = vm.gridSettings.margins[1]; |
73 | 74 | vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; | ... | ... |
... | ... | @@ -121,6 +121,9 @@ |
121 | 121 | </div> |
122 | 122 | </md-input-container> |
123 | 123 | </div> |
124 | + <md-checkbox flex aria-label="{{ 'dashboard.autofill-height' | translate }}" | |
125 | + ng-model="vm.gridSettings.autoFillHeight">{{ 'dashboard.autofill-height' | translate }} | |
126 | + </md-checkbox> | |
124 | 127 | <div flex |
125 | 128 | ng-required="false" |
126 | 129 | md-color-picker | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -49,6 +49,7 @@ |
49 | 49 | state-controller="vm.dashboardCtx.stateController" |
50 | 50 | dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow" |
51 | 51 | is-edit="vm.isEdit" |
52 | + autofill-height="vm.layoutCtx.gridSettings.autoFillHeight && !vm.isEdit" | |
52 | 53 | is-mobile="vm.isMobile" |
53 | 54 | is-mobile-disabled="vm.widgetEditMode" |
54 | 55 | is-edit-action-enabled="vm.isEdit" | ... | ... |
... | ... | @@ -53,12 +53,6 @@ export default function DashboardStateDialogController($scope, $mdDialog, $filte |
53 | 53 | if (!vm.stateIdTouched && vm.isAdd) { |
54 | 54 | vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_"); |
55 | 55 | } |
56 | - var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true); | |
57 | - if (result && result.length && result[0].id !== vm.prevStateId) { | |
58 | - $scope.theForm.name.$setValidity('stateExists', false); | |
59 | - } else { | |
60 | - $scope.theForm.name.$setValidity('stateExists', true); | |
61 | - } | |
62 | 56 | } |
63 | 57 | |
64 | 58 | function checkStateId() { | ... | ... |
... | ... | @@ -37,18 +37,15 @@ |
37 | 37 | <input name="name" required ng-model="vm.state.name"> |
38 | 38 | <div ng-messages="theForm.name.$error"> |
39 | 39 | <div ng-message="required" translate>dashboard.state-name-required</div> |
40 | - <div ng-message="stateExists" translate>dashboard.state-name-exists</div> | |
41 | 40 | </div> |
42 | 41 | </md-input-container> |
43 | 42 | <md-input-container class="md-block"> |
44 | 43 | <label translate>dashboard.state-id</label> |
45 | - <input name="stateId" ng-model="vm.state.id" | |
46 | - ng-change="vm.stateIdTouched = true" | |
47 | - ng-pattern="/^[a-zA-Z0-9_]*$/"> | |
44 | + <input name="stateId" required ng-model="vm.state.id" | |
45 | + ng-change="vm.stateIdTouched = true"> | |
48 | 46 | <div ng-messages="theForm.stateId.$error"> |
49 | 47 | <div ng-message="required" translate>dashboard.state-id-required</div> |
50 | 48 | <div ng-message="stateExists" translate>dashboard.state-id-exists</div> |
51 | - <div ng-message="pattern" translate>dashboard.invalid-state-id-format</div> | |
52 | 49 | </div> |
53 | 50 | </md-input-container> |
54 | 51 | <md-checkbox flex aria-label="{{ 'dashboard.is-root-state' | translate }}" | ... | ... |
... | ... | @@ -97,10 +97,10 @@ export default function DefaultStateController($scope, $location, $state, $state |
97 | 97 | |
98 | 98 | function getStateName(id, state) { |
99 | 99 | var result = ''; |
100 | - var translationId = types.translate.dashboardStatePrefix + id; | |
100 | + var translationId = types.translate.customTranslationsPrefix + state.name; | |
101 | 101 | var translation = $translate.instant(translationId); |
102 | 102 | if (translation != translationId) { |
103 | - result = translation; | |
103 | + result = translation + ''; | |
104 | 104 | } else { |
105 | 105 | result = state.name; |
106 | 106 | } | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | import './entity-state-controller.scss'; |
18 | 18 | |
19 | 19 | /*@ngInject*/ |
20 | -export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, types, dashboardUtils, entityService) { | |
20 | +export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, utils, types, dashboardUtils, entityService) { | |
21 | 21 | |
22 | 22 | var vm = this; |
23 | 23 | |
... | ... | @@ -106,18 +106,17 @@ export default function EntityStateController($scope, $location, $state, $stateP |
106 | 106 | function getStateName(index) { |
107 | 107 | var result = ''; |
108 | 108 | if (vm.stateObject[index]) { |
109 | + var stateName = vm.states[vm.stateObject[index].id].name; | |
110 | + var translationId = types.translate.customTranslationsPrefix + stateName; | |
111 | + var translation = $translate.instant(translationId); | |
112 | + if (translation != translationId) { | |
113 | + stateName = translation + ''; | |
114 | + } | |
109 | 115 | var params = vm.stateObject[index].params; |
110 | 116 | if (params && params.entityName) { |
111 | - result = params.entityName; | |
117 | + result = utils.insertVariable(stateName, 'entityName', params.entityName); | |
112 | 118 | } else { |
113 | - var id = vm.stateObject[index].id; | |
114 | - var translationId = types.translate.dashboardStatePrefix + id; | |
115 | - var translation = $translate.instant(translationId); | |
116 | - if (translation != translationId) { | |
117 | - result = translation; | |
118 | - } else { | |
119 | - result = vm.states[vm.stateObject[index].id].name; | |
120 | - } | |
119 | + result = stateName; | |
121 | 120 | } |
122 | 121 | } |
123 | 122 | return result; |
... | ... | @@ -243,11 +242,9 @@ export default function EntityStateController($scope, $location, $state, $stateP |
243 | 242 | } |
244 | 243 | |
245 | 244 | function gotoState(stateId, update, openRightLayout) { |
246 | - if (vm.dashboardCtrl.dashboardCtx.state != stateId) { | |
247 | - vm.dashboardCtrl.openDashboardState(stateId, openRightLayout); | |
248 | - if (update) { | |
249 | - updateLocation(); | |
250 | - } | |
245 | + vm.dashboardCtrl.openDashboardState(stateId, openRightLayout); | |
246 | + if (update) { | |
247 | + updateLocation(); | |
251 | 248 | } |
252 | 249 | } |
253 | 250 | ... | ... |
... | ... | @@ -32,7 +32,7 @@ |
32 | 32 | on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)" |
33 | 33 | on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device> |
34 | 34 | </md-tab> |
35 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}"> | |
35 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}"> | |
36 | 36 | <tb-attribute-table flex |
37 | 37 | entity-id="vm.grid.operatingItem().id.id" |
38 | 38 | entity-type="{{vm.types.entityType.device}}" |
... | ... | @@ -40,7 +40,7 @@ |
40 | 40 | default-attribute-scope="{{vm.types.attributesScope.client.value}}"> |
41 | 41 | </tb-attribute-table> |
42 | 42 | </md-tab> |
43 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
43 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
44 | 44 | <tb-attribute-table flex |
45 | 45 | entity-id="vm.grid.operatingItem().id.id" |
46 | 46 | entity-type="{{vm.types.entityType.device}}" |
... | ... | @@ -49,19 +49,19 @@ |
49 | 49 | disable-attribute-scope-selection="true"> |
50 | 50 | </tb-attribute-table> |
51 | 51 | </md-tab> |
52 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}"> | |
52 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}"> | |
53 | 53 | <tb-alarm-table flex entity-type="vm.types.entityType.device" |
54 | 54 | entity-id="vm.grid.operatingItem().id.id"> |
55 | 55 | </tb-alarm-table> |
56 | 56 | </md-tab> |
57 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'device.events' | translate }}"> | |
57 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'device.events' | translate }}"> | |
58 | 58 | <tb-event-table flex entity-type="vm.types.entityType.device" |
59 | 59 | entity-id="vm.grid.operatingItem().id.id" |
60 | 60 | tenant-id="vm.grid.operatingItem().tenantId.id" |
61 | 61 | default-event-type="{{vm.types.eventType.error.value}}"> |
62 | 62 | </tb-event-table> |
63 | 63 | </md-tab> |
64 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}"> | |
64 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> | |
65 | 65 | <tb-relation-table flex |
66 | 66 | entity-id="vm.grid.operatingItem().id.id" |
67 | 67 | entity-type="{{vm.types.entityType.device}}"> | ... | ... |
... | ... | @@ -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,17 @@ 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", | |
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} }?" | |
148 | 159 | }, |
149 | 160 | "alias": { |
150 | 161 | "add": "Add alias", |
... | ... | @@ -420,6 +431,7 @@ export default angular.module('thingsboard.locale', []) |
420 | 431 | "vertical-margin-required": "Vertical margin value is required.", |
421 | 432 | "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", |
422 | 433 | "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", |
434 | + "autofill-height": "Auto fill layout height", | |
423 | 435 | "display-title": "Display dashboard title", |
424 | 436 | "toolbar-always-open": "Keep toolbar opened", |
425 | 437 | "title-color": "Title color", |
... | ... | @@ -461,11 +473,9 @@ export default angular.module('thingsboard.locale', []) |
461 | 473 | "state": "Dashboard state", |
462 | 474 | "state-name": "Name", |
463 | 475 | "state-name-required": "Dashboard state name is required.", |
464 | - "state-name-exists": "Dashboard state with the same name is already exists.", | |
465 | 476 | "state-id": "State Id", |
466 | 477 | "state-id-required": "Dashboard state id is required.", |
467 | 478 | "state-id-exists": "Dashboard state with the same id is already exists.", |
468 | - "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.", | |
469 | 479 | "is-root-state": "Root state", |
470 | 480 | "delete-state-title": "Delete dashboard state", |
471 | 481 | "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?", |
... | ... | @@ -486,8 +496,10 @@ export default angular.module('thingsboard.locale', []) |
486 | 496 | "configuration": "Data key configuration", |
487 | 497 | "timeseries": "Timeseries", |
488 | 498 | "attributes": "Attributes", |
499 | + "alarm": "Alarm fields", | |
489 | 500 | "timeseries-required": "Entity timeseries are required.", |
490 | 501 | "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", |
502 | + "alarm-fields-required": "Alarm fields are required.", | |
491 | 503 | "function-types": "Function types", |
492 | 504 | "function-types-required": "Function types are required." |
493 | 505 | }, |
... | ... | @@ -637,6 +649,8 @@ export default angular.module('thingsboard.locale', []) |
637 | 649 | "no-aliases-found": "No aliases found.", |
638 | 650 | "no-alias-matching": "'{{alias}}' not found.", |
639 | 651 | "create-new-alias": "Create a new one!", |
652 | + "key": "Key", | |
653 | + "key-name": "Key name", | |
640 | 654 | "no-keys-found": "No keys found.", |
641 | 655 | "no-key-matching": "'{{key}}' not found.", |
642 | 656 | "create-new-key": "Create a new one!", |
... | ... | @@ -1046,6 +1060,7 @@ export default angular.module('thingsboard.locale', []) |
1046 | 1060 | "timeseries": "Time series", |
1047 | 1061 | "latest-values": "Latest values", |
1048 | 1062 | "rpc": "Control widget", |
1063 | + "alarm": "Alarm widget", | |
1049 | 1064 | "static": "Static widget", |
1050 | 1065 | "select-widget-type": "Select widget type", |
1051 | 1066 | "missing-widget-title-error": "Widget title must be specified!", |
... | ... | @@ -1133,7 +1148,8 @@ export default angular.module('thingsboard.locale', []) |
1133 | 1148 | "datasource-parameters": "Parameters", |
1134 | 1149 | "remove-datasource": "Remove datasource", |
1135 | 1150 | "add-datasource": "Add datasource", |
1136 | - "target-device": "Target device" | |
1151 | + "target-device": "Target device", | |
1152 | + "alarm-source": "Alarm source" | |
1137 | 1153 | }, |
1138 | 1154 | "widget-type": { |
1139 | 1155 | "import": "Import widget type", |
... | ... | @@ -1150,6 +1166,8 @@ export default angular.module('thingsboard.locale', []) |
1150 | 1166 | "zh_CN": "Chinese", |
1151 | 1167 | "ru_RU": "Russian", |
1152 | 1168 | "es_ES": "Spanish" |
1169 | + }, | |
1170 | + "custom": { | |
1153 | 1171 | } |
1154 | 1172 | } |
1155 | 1173 | } | ... | ... |
... | ... | @@ -18,7 +18,7 @@ |
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.customTranslationsPrefix)) { | |
22 | 22 | $log.warn('Translation for ' + translationId + ' doesn\'t exist'); |
23 | 23 | } |
24 | 24 | }; | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | on-export-plugin="vm.exportPlugin(event, vm.grid.detailsConfig.currentItem)" |
32 | 32 | on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-plugin> |
33 | 33 | </md-tab> |
34 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'attribute.attributes' | translate }}"> | |
34 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}"> | |
35 | 35 | <tb-attribute-table flex |
36 | 36 | entity-id="vm.grid.operatingItem().id.id" |
37 | 37 | entity-type="{{vm.types.entityType.plugin}}" |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | default-attribute-scope="{{vm.types.attributesScope.server.value}}"> |
40 | 40 | </tb-attribute-table> |
41 | 41 | </md-tab> |
42 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
42 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
43 | 43 | <tb-attribute-table flex |
44 | 44 | entity-id="vm.grid.operatingItem().id.id" |
45 | 45 | entity-type="{{vm.types.entityType.plugin}}" |
... | ... | @@ -48,19 +48,19 @@ |
48 | 48 | disable-attribute-scope-selection="true"> |
49 | 49 | </tb-attribute-table> |
50 | 50 | </md-tab> |
51 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'alarm.alarms' | translate }}"> | |
51 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}"> | |
52 | 52 | <tb-alarm-table flex entity-type="vm.types.entityType.plugin" |
53 | 53 | entity-id="vm.grid.operatingItem().id.id"> |
54 | 54 | </tb-alarm-table> |
55 | 55 | </md-tab> |
56 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'plugin.events' | translate }}"> | |
56 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'plugin.events' | translate }}"> | |
57 | 57 | <tb-event-table flex entity-type="vm.types.entityType.plugin" |
58 | 58 | entity-id="vm.grid.operatingItem().id.id" |
59 | 59 | tenant-id="vm.grid.operatingItem().tenantId.id" |
60 | 60 | default-event-type="{{vm.types.eventType.lcEvent.value}}"> |
61 | 61 | </tb-event-table> |
62 | 62 | </md-tab> |
63 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'relation.relations' | translate }}"> | |
63 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> | |
64 | 64 | <tb-relation-table flex |
65 | 65 | entity-id="vm.grid.operatingItem().id.id" |
66 | 66 | entity-type="{{vm.types.entityType.plugin}}"> | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | on-export-rule="vm.exportRule(event, vm.grid.detailsConfig.currentItem)" |
32 | 32 | on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule> |
33 | 33 | </md-tab> |
34 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'attribute.attributes' | translate }}"> | |
34 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}"> | |
35 | 35 | <tb-attribute-table flex |
36 | 36 | entity-id="vm.grid.operatingItem().id.id" |
37 | 37 | entity-type="{{vm.types.entityType.rule}}" |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | default-attribute-scope="{{vm.types.attributesScope.server.value}}"> |
40 | 40 | </tb-attribute-table> |
41 | 41 | </md-tab> |
42 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
42 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
43 | 43 | <tb-attribute-table flex |
44 | 44 | entity-id="vm.grid.operatingItem().id.id" |
45 | 45 | entity-type="{{vm.types.entityType.rule}}" |
... | ... | @@ -48,19 +48,19 @@ |
48 | 48 | disable-attribute-scope-selection="true"> |
49 | 49 | </tb-attribute-table> |
50 | 50 | </md-tab> |
51 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'alarm.alarms' | translate }}"> | |
51 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}"> | |
52 | 52 | <tb-alarm-table flex entity-type="vm.types.entityType.rule" |
53 | 53 | entity-id="vm.grid.operatingItem().id.id"> |
54 | 54 | </tb-alarm-table> |
55 | 55 | </md-tab> |
56 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'rule.events' | translate }}"> | |
56 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rule.events' | translate }}"> | |
57 | 57 | <tb-event-table flex entity-type="vm.types.entityType.rule" |
58 | 58 | entity-id="vm.grid.operatingItem().id.id" |
59 | 59 | tenant-id="vm.grid.operatingItem().tenantId.id" |
60 | 60 | default-event-type="{{vm.types.eventType.lcEvent.value}}"> |
61 | 61 | </tb-event-table> |
62 | 62 | </md-tab> |
63 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'relation.relations' | translate }}"> | |
63 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> | |
64 | 64 | <tb-relation-table flex |
65 | 65 | entity-id="vm.grid.operatingItem().id.id" |
66 | 66 | entity-type="{{vm.types.entityType.rule}}"> | ... | ... |
... | ... | @@ -29,7 +29,7 @@ |
29 | 29 | on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)" |
30 | 30 | on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant> |
31 | 31 | </md-tab> |
32 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}"> | |
32 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}"> | |
33 | 33 | <tb-attribute-table flex |
34 | 34 | entity-id="vm.grid.operatingItem().id.id" |
35 | 35 | entity-type="{{vm.types.entityType.tenant}}" |
... | ... | @@ -37,7 +37,7 @@ |
37 | 37 | default-attribute-scope="{{vm.types.attributesScope.server.value}}"> |
38 | 38 | </tb-attribute-table> |
39 | 39 | </md-tab> |
40 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
40 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}"> | |
41 | 41 | <tb-attribute-table flex |
42 | 42 | entity-id="vm.grid.operatingItem().id.id" |
43 | 43 | entity-type="{{vm.types.entityType.tenant}}" |
... | ... | @@ -46,19 +46,19 @@ |
46 | 46 | disable-attribute-scope-selection="true"> |
47 | 47 | </tb-attribute-table> |
48 | 48 | </md-tab> |
49 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}"> | |
49 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}"> | |
50 | 50 | <tb-alarm-table flex entity-type="vm.types.entityType.tenant" |
51 | 51 | entity-id="vm.grid.operatingItem().id.id"> |
52 | 52 | </tb-alarm-table> |
53 | 53 | </md-tab> |
54 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'tenant.events' | translate }}"> | |
54 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'tenant.events' | translate }}"> | |
55 | 55 | <tb-event-table flex entity-type="vm.types.entityType.tenant" |
56 | 56 | entity-id="vm.grid.operatingItem().id.id" |
57 | 57 | tenant-id="vm.types.id.nullUid" |
58 | 58 | default-event-type="{{vm.types.eventType.error.value}}"> |
59 | 59 | </tb-event-table> |
60 | 60 | </md-tab> |
61 | - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}"> | |
61 | + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> | |
62 | 62 | <tb-relation-table flex |
63 | 63 | entity-id="vm.grid.operatingItem().id.id" |
64 | 64 | entity-type="{{vm.types.entityType.tenant}}"> | ... | ... |
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 | +import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html'; | |
23 | + | |
24 | +/* eslint-enable import/no-unresolved, import/default */ | |
25 | + | |
26 | +import tinycolor from 'tinycolor2'; | |
27 | +import cssjs from '../../../vendor/css.js/css'; | |
28 | + | |
29 | +export default angular.module('thingsboard.widgets.alarmsTableWidget', []) | |
30 | + .directive('tbAlarmsTableWidget', AlarmsTableWidget) | |
31 | + .name; | |
32 | + | |
33 | +/*@ngInject*/ | |
34 | +function AlarmsTableWidget() { | |
35 | + return { | |
36 | + restrict: "E", | |
37 | + scope: true, | |
38 | + bindToController: { | |
39 | + tableId: '=', | |
40 | + ctx: '=' | |
41 | + }, | |
42 | + controller: AlarmsTableWidgetController, | |
43 | + controllerAs: 'vm', | |
44 | + templateUrl: alarmsTableWidgetTemplate | |
45 | + }; | |
46 | +} | |
47 | + | |
48 | +/*@ngInject*/ | |
49 | +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, alarmService, utils, types) { | |
50 | + var vm = this; | |
51 | + | |
52 | + vm.stylesInfo = {}; | |
53 | + vm.contentsInfo = {}; | |
54 | + vm.columnWidth = {}; | |
55 | + | |
56 | + vm.showData = true; | |
57 | + vm.hasData = false; | |
58 | + | |
59 | + vm.alarms = []; | |
60 | + vm.alarmsCount = 0; | |
61 | + vm.selectedAlarms = [] | |
62 | + | |
63 | + vm.alarmSource = null; | |
64 | + vm.allAlarms = null; | |
65 | + | |
66 | + vm.currentAlarm = null; | |
67 | + | |
68 | + vm.enableSelection = true; | |
69 | + vm.displayDetails = true; | |
70 | + vm.allowAcknowledgment = true; | |
71 | + vm.allowClear = true; | |
72 | + vm.displayPagination = true; | |
73 | + vm.defaultPageSize = 10; | |
74 | + vm.defaultSortOrder = '-'+types.alarmFields.createdTime.value; | |
75 | + | |
76 | + vm.query = { | |
77 | + order: vm.defaultSortOrder, | |
78 | + limit: vm.defaultPageSize, | |
79 | + page: 1, | |
80 | + search: null | |
81 | + }; | |
82 | + | |
83 | + vm.searchAction = { | |
84 | + name: 'action.search', | |
85 | + show: true, | |
86 | + onAction: function() { | |
87 | + vm.enterFilterMode(); | |
88 | + }, | |
89 | + icon: 'search' | |
90 | + }; | |
91 | + | |
92 | + vm.enterFilterMode = enterFilterMode; | |
93 | + vm.exitFilterMode = exitFilterMode; | |
94 | + vm.onReorder = onReorder; | |
95 | + vm.onPaginate = onPaginate; | |
96 | + vm.onRowClick = onRowClick; | |
97 | + vm.isCurrent = isCurrent; | |
98 | + vm.openAlarmDetails = openAlarmDetails; | |
99 | + vm.ackAlarms = ackAlarms; | |
100 | + vm.clearAlarms = clearAlarms; | |
101 | + | |
102 | + vm.cellStyle = cellStyle; | |
103 | + vm.cellContent = cellContent; | |
104 | + | |
105 | + $scope.$watch('vm.ctx', function() { | |
106 | + if (vm.ctx) { | |
107 | + vm.settings = vm.ctx.settings; | |
108 | + vm.widgetConfig = vm.ctx.widgetConfig; | |
109 | + vm.subscription = vm.ctx.defaultSubscription; | |
110 | + vm.alarmSource = vm.subscription.alarmSource; | |
111 | + initializeConfig(); | |
112 | + updateAlarmSource(); | |
113 | + } | |
114 | + }); | |
115 | + | |
116 | + $scope.$watch("vm.query.search", function(newVal, prevVal) { | |
117 | + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { | |
118 | + updateAlarms(); | |
119 | + } | |
120 | + }); | |
121 | + | |
122 | + $scope.$on('alarms-table-data-updated', function(event, tableId) { | |
123 | + if (vm.tableId == tableId) { | |
124 | + if (vm.subscription) { | |
125 | + vm.allAlarms = vm.subscription.alarms; | |
126 | + updateAlarms(true); | |
127 | + $scope.$digest(); | |
128 | + } | |
129 | + } | |
130 | + }); | |
131 | + | |
132 | + $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) { | |
133 | + vm.isGtXs = isGtXs; | |
134 | + }); | |
135 | + | |
136 | + $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) { | |
137 | + vm.isGtMd = isGtMd; | |
138 | + if (vm.isGtMd) { | |
139 | + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; | |
140 | + } else { | |
141 | + vm.limitOptions = null; | |
142 | + } | |
143 | + }); | |
144 | + | |
145 | + $scope.$watch('vm.selectedAlarms.length', function (newLength) { | |
146 | + var selectionMode = newLength ? true : false; | |
147 | + if (vm.ctx) { | |
148 | + if (selectionMode) { | |
149 | + vm.ctx.hideTitlePanel = true; | |
150 | + } else if (vm.query.search == null) { | |
151 | + vm.ctx.hideTitlePanel = false; | |
152 | + } | |
153 | + } | |
154 | + }); | |
155 | + | |
156 | + function initializeConfig() { | |
157 | + | |
158 | + vm.ctx.widgetActions = [ vm.searchAction ]; | |
159 | + | |
160 | + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { | |
161 | + var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle; | |
162 | + var translation = $translate.instant(translationId); | |
163 | + if (translation != translationId) { | |
164 | + vm.alarmsTitle = translation + ''; | |
165 | + } else { | |
166 | + vm.alarmsTitle = vm.settings.alarmsTitle; | |
167 | + } | |
168 | + } else { | |
169 | + vm.alarmsTitle = $translate.instant('alarm.alarms'); | |
170 | + } | |
171 | + | |
172 | + vm.ctx.widgetTitle = vm.alarmsTitle; | |
173 | + | |
174 | + vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; | |
175 | + vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; | |
176 | + vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; | |
177 | + vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; | |
178 | + vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true; | |
179 | + if (!vm.allowAcknowledgment && !vm.allowClear) { | |
180 | + vm.enableSelection = false; | |
181 | + } | |
182 | + | |
183 | + vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; | |
184 | + | |
185 | + var pageSize = vm.settings.defaultPageSize; | |
186 | + if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) { | |
187 | + vm.defaultPageSize = pageSize; | |
188 | + } | |
189 | + | |
190 | + if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) { | |
191 | + vm.defaultSortOrder = vm.settings.defaultSortOrder; | |
192 | + } | |
193 | + | |
194 | + vm.query.order = vm.defaultSortOrder; | |
195 | + vm.query.limit = vm.defaultPageSize; | |
196 | + if (vm.isGtMd) { | |
197 | + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; | |
198 | + } else { | |
199 | + vm.limitOptions = null; | |
200 | + } | |
201 | + | |
202 | + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; | |
203 | + var defaultColor = tinycolor(origColor); | |
204 | + var mdDark = defaultColor.setAlpha(0.87).toRgbString(); | |
205 | + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); | |
206 | + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); | |
207 | + //var mdDarkIcon = mdDarkSecondary; | |
208 | + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); | |
209 | + | |
210 | + var cssString = 'table.md-table th.md-column {\n'+ | |
211 | + 'color: ' + mdDarkSecondary + ';\n'+ | |
212 | + '}\n'+ | |
213 | + 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+ | |
214 | + 'border-color: ' + mdDarkSecondary + ';\n'+ | |
215 | + '}\n'+ | |
216 | + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ | |
217 | + 'color: ' + mdDarkDisabled + ';\n'+ | |
218 | + '}\n'+ | |
219 | + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ | |
220 | + 'color: ' + mdDark + ';\n'+ | |
221 | + '}\n'+ | |
222 | + 'table.md-table td.md-cell {\n'+ | |
223 | + 'color: ' + mdDark + ';\n'+ | |
224 | + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ | |
225 | + '}\n'+ | |
226 | + 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ | |
227 | + 'border-color: ' + mdDarkSecondary + ';\n'+ | |
228 | + '}\n'+ | |
229 | + 'table.md-table td.md-cell.md-placeholder {\n'+ | |
230 | + 'color: ' + mdDarkDisabled + ';\n'+ | |
231 | + '}\n'+ | |
232 | + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ | |
233 | + 'color: ' + mdDarkSecondary + ';\n'+ | |
234 | + '}\n'+ | |
235 | + '.md-table-pagination {\n'+ | |
236 | + 'color: ' + mdDarkSecondary + ';\n'+ | |
237 | + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ | |
238 | + '}\n'+ | |
239 | + '.md-table-pagination .buttons md-icon {\n'+ | |
240 | + 'color: ' + mdDarkSecondary + ';\n'+ | |
241 | + '}\n'+ | |
242 | + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ | |
243 | + 'color: ' + mdDarkSecondary + ';\n'+ | |
244 | + '}'; | |
245 | + | |
246 | + var cssParser = new cssjs(); | |
247 | + cssParser.testMode = false; | |
248 | + var namespace = 'ts-table-' + hashCode(cssString); | |
249 | + cssParser.cssPreviewNamespace = namespace; | |
250 | + cssParser.createStyleElement(namespace, cssString); | |
251 | + $element.addClass(namespace); | |
252 | + | |
253 | + function hashCode(str) { | |
254 | + var hash = 0; | |
255 | + var i, char; | |
256 | + if (str.length === 0) return hash; | |
257 | + for (i = 0; i < str.length; i++) { | |
258 | + char = str.charCodeAt(i); | |
259 | + hash = ((hash << 5) - hash) + char; | |
260 | + hash = hash & hash; | |
261 | + } | |
262 | + return hash; | |
263 | + } | |
264 | + } | |
265 | + | |
266 | + function enterFilterMode () { | |
267 | + vm.query.search = ''; | |
268 | + vm.ctx.hideTitlePanel = true; | |
269 | + } | |
270 | + | |
271 | + function exitFilterMode () { | |
272 | + vm.query.search = null; | |
273 | + updateAlarms(); | |
274 | + vm.ctx.hideTitlePanel = false; | |
275 | + } | |
276 | + | |
277 | + function onReorder () { | |
278 | + updateAlarms(); | |
279 | + } | |
280 | + | |
281 | + function onPaginate () { | |
282 | + updateAlarms(); | |
283 | + } | |
284 | + | |
285 | + function onRowClick($event, alarm) { | |
286 | + if (vm.currentAlarm != alarm) { | |
287 | + vm.currentAlarm = alarm; | |
288 | + } | |
289 | + } | |
290 | + | |
291 | + function isCurrent(alarm) { | |
292 | + return (vm.currentAlarm && alarm && vm.currentAlarm.id && alarm.id) && | |
293 | + (vm.currentAlarm.id.id === alarm.id.id); | |
294 | + } | |
295 | + | |
296 | + function openAlarmDetails($event, alarm) { | |
297 | + if (alarm && alarm.id) { | |
298 | + var onShowingCallback = { | |
299 | + onShowing: function(){} | |
300 | + } | |
301 | + $mdDialog.show({ | |
302 | + controller: 'AlarmDetailsDialogController', | |
303 | + controllerAs: 'vm', | |
304 | + templateUrl: alarmDetailsDialogTemplate, | |
305 | + locals: { | |
306 | + alarmId: alarm.id.id, | |
307 | + allowAcknowledgment: vm.allowAcknowledgment, | |
308 | + allowClear: vm.allowClear, | |
309 | + showingCallback: onShowingCallback | |
310 | + }, | |
311 | + parent: angular.element($document[0].body), | |
312 | + targetEvent: $event, | |
313 | + fullscreen: true, | |
314 | + skipHide: true, | |
315 | + onShowing: function(scope, element) { | |
316 | + onShowingCallback.onShowing(scope, element); | |
317 | + } | |
318 | + }).then(function (alarm) { | |
319 | + if (alarm) { | |
320 | + vm.subscription.update(); | |
321 | + } | |
322 | + }); | |
323 | + | |
324 | + } | |
325 | + } | |
326 | + | |
327 | + function ackAlarms($event) { | |
328 | + if ($event) { | |
329 | + $event.stopPropagation(); | |
330 | + } | |
331 | + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) { | |
332 | + var title = $translate.instant('alarm.aknowledge-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); | |
333 | + var content = $translate.instant('alarm.aknowledge-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); | |
334 | + var confirm = $mdDialog.confirm() | |
335 | + .targetEvent($event) | |
336 | + .title(title) | |
337 | + .htmlContent(content) | |
338 | + .ariaLabel(title) | |
339 | + .cancel($translate.instant('action.no')) | |
340 | + .ok($translate.instant('action.yes')); | |
341 | + $mdDialog.show(confirm).then(function () { | |
342 | + var tasks = []; | |
343 | + for (var i=0;i<vm.selectedAlarms.length;i++) { | |
344 | + var alarm = vm.selectedAlarms[i]; | |
345 | + if (alarm.id) { | |
346 | + tasks.push(alarmService.ackAlarm(alarm.id.id)); | |
347 | + } | |
348 | + } | |
349 | + if (tasks.length) { | |
350 | + $q.all(tasks).then(function () { | |
351 | + vm.selectedAlarms = []; | |
352 | + vm.subscription.update(); | |
353 | + }); | |
354 | + } | |
355 | + | |
356 | + }); | |
357 | + } | |
358 | + } | |
359 | + | |
360 | + function clearAlarms($event) { | |
361 | + if ($event) { | |
362 | + $event.stopPropagation(); | |
363 | + } | |
364 | + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) { | |
365 | + var title = $translate.instant('alarm.clear-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); | |
366 | + var content = $translate.instant('alarm.clear-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); | |
367 | + var confirm = $mdDialog.confirm() | |
368 | + .targetEvent($event) | |
369 | + .title(title) | |
370 | + .htmlContent(content) | |
371 | + .ariaLabel(title) | |
372 | + .cancel($translate.instant('action.no')) | |
373 | + .ok($translate.instant('action.yes')); | |
374 | + $mdDialog.show(confirm).then(function () { | |
375 | + var tasks = []; | |
376 | + for (var i=0;i<vm.selectedAlarms.length;i++) { | |
377 | + var alarm = vm.selectedAlarms[i]; | |
378 | + if (alarm.id) { | |
379 | + tasks.push(alarmService.clearAlarm(alarm.id.id)); | |
380 | + } | |
381 | + } | |
382 | + if (tasks.length) { | |
383 | + $q.all(tasks).then(function () { | |
384 | + vm.selectedAlarms = []; | |
385 | + vm.subscription.update(); | |
386 | + }); | |
387 | + } | |
388 | + | |
389 | + }); | |
390 | + } | |
391 | + } | |
392 | + | |
393 | + | |
394 | + function updateAlarms(preserveSelections) { | |
395 | + if (!preserveSelections) { | |
396 | + vm.selectedAlarms = []; | |
397 | + } | |
398 | + var result = $filter('orderBy')(vm.allAlarms, vm.query.order); | |
399 | + if (vm.query.search != null) { | |
400 | + result = $filter('filter')(result, {$: vm.query.search}); | |
401 | + } | |
402 | + vm.alarmsCount = result.length; | |
403 | + | |
404 | + if (vm.displayPagination) { | |
405 | + var startIndex = vm.query.limit * (vm.query.page - 1); | |
406 | + vm.alarms = result.slice(startIndex, startIndex + vm.query.limit); | |
407 | + } else { | |
408 | + vm.alarms = result; | |
409 | + } | |
410 | + | |
411 | + if (preserveSelections) { | |
412 | + var newSelectedAlarms = []; | |
413 | + if (vm.selectedAlarms && vm.selectedAlarms.length) { | |
414 | + var i = vm.selectedAlarms.length; | |
415 | + while (i--) { | |
416 | + var selectedAlarm = vm.selectedAlarms[i]; | |
417 | + if (selectedAlarm.id) { | |
418 | + result = $filter('filter')(vm.alarms, {id: {id: selectedAlarm.id.id} }); | |
419 | + if (result && result.length) { | |
420 | + newSelectedAlarms.push(result[0]); | |
421 | + } | |
422 | + } | |
423 | + } | |
424 | + } | |
425 | + vm.selectedAlarms = newSelectedAlarms; | |
426 | + } | |
427 | + } | |
428 | + | |
429 | + function cellStyle(alarm, key) { | |
430 | + var style = {}; | |
431 | + if (alarm && key) { | |
432 | + var styleInfo = vm.stylesInfo[key.label]; | |
433 | + var value = getAlarmValue(alarm, key); | |
434 | + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { | |
435 | + try { | |
436 | + style = styleInfo.cellStyleFunction(value); | |
437 | + } catch (e) { | |
438 | + style = {}; | |
439 | + } | |
440 | + } else { | |
441 | + style = defaultStyle(key, value); | |
442 | + } | |
443 | + } | |
444 | + if (!style.width) { | |
445 | + var columnWidth = vm.columnWidth[key.label]; | |
446 | + style.width = columnWidth; | |
447 | + } | |
448 | + return style; | |
449 | + } | |
450 | + | |
451 | + function cellContent(alarm, key) { | |
452 | + var strContent = ''; | |
453 | + if (alarm && key) { | |
454 | + var contentInfo = vm.contentsInfo[key.label]; | |
455 | + var value = getAlarmValue(alarm, key); | |
456 | + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { | |
457 | + if (angular.isDefined(value)) { | |
458 | + strContent = '' + value; | |
459 | + } | |
460 | + var content = strContent; | |
461 | + try { | |
462 | + content = contentInfo.cellContentFunction(value, alarm, $filter); | |
463 | + } catch (e) { | |
464 | + content = strContent; | |
465 | + } | |
466 | + } else { | |
467 | + content = defaultContent(key, value); | |
468 | + } | |
469 | + return content; | |
470 | + } else { | |
471 | + return strContent; | |
472 | + } | |
473 | + } | |
474 | + | |
475 | + function defaultContent(key, value) { | |
476 | + if (angular.isDefined(value)) { | |
477 | + var alarmField = types.alarmFields[key.name]; | |
478 | + if (alarmField) { | |
479 | + if (alarmField.time) { | |
480 | + return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss'); | |
481 | + } else if (alarmField.value == types.alarmFields.severity.value) { | |
482 | + return $translate.instant(types.alarmSeverity[value].name); | |
483 | + } else if (alarmField.value == types.alarmFields.status.value) { | |
484 | + return $translate.instant('alarm.display-status.'+value); | |
485 | + } else if (alarmField.value == types.alarmFields.originatorType.value) { | |
486 | + return $translate.instant(types.entityTypeTranslations[value].type); | |
487 | + } | |
488 | + else { | |
489 | + return value; | |
490 | + } | |
491 | + } else { | |
492 | + return value; | |
493 | + } | |
494 | + } else { | |
495 | + return ''; | |
496 | + } | |
497 | + } | |
498 | + function defaultStyle(key, value) { | |
499 | + if (angular.isDefined(value)) { | |
500 | + var alarmField = types.alarmFields[key.name]; | |
501 | + if (alarmField) { | |
502 | + if (alarmField.value == types.alarmFields.severity.value) { | |
503 | + return { | |
504 | + fontWeight: 'bold', | |
505 | + color: types.alarmSeverity[value].color | |
506 | + }; | |
507 | + } else { | |
508 | + return {}; | |
509 | + } | |
510 | + } else { | |
511 | + return {}; | |
512 | + } | |
513 | + } else { | |
514 | + return {}; | |
515 | + } | |
516 | + } | |
517 | + | |
518 | + const getDescendantProp = (obj, path) => ( | |
519 | + path.split('.').reduce((acc, part) => acc && acc[part], obj) | |
520 | + ); | |
521 | + | |
522 | + function getAlarmValue(alarm, key) { | |
523 | + var alarmField = types.alarmFields[key.name]; | |
524 | + if (alarmField) { | |
525 | + return getDescendantProp(alarm, alarmField.value); | |
526 | + } else { | |
527 | + return getDescendantProp(alarm, key.name); | |
528 | + } | |
529 | + } | |
530 | + | |
531 | + function updateAlarmSource() { | |
532 | + | |
533 | + vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle); | |
534 | + | |
535 | + vm.stylesInfo = {}; | |
536 | + vm.contentsInfo = {}; | |
537 | + vm.columnWidth = {}; | |
538 | + | |
539 | + for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { | |
540 | + var dataKey = vm.alarmSource.dataKeys[d]; | |
541 | + | |
542 | + var translationId = types.translate.customTranslationsPrefix + dataKey.label; | |
543 | + var translation = $translate.instant(translationId); | |
544 | + if (translation != translationId) { | |
545 | + dataKey.title = translation + ''; | |
546 | + } else { | |
547 | + dataKey.title = dataKey.label; | |
548 | + } | |
549 | + | |
550 | + var keySettings = dataKey.settings; | |
551 | + | |
552 | + var cellStyleFunction = null; | |
553 | + var useCellStyleFunction = false; | |
554 | + | |
555 | + if (keySettings.useCellStyleFunction === true) { | |
556 | + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { | |
557 | + try { | |
558 | + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); | |
559 | + useCellStyleFunction = true; | |
560 | + } catch (e) { | |
561 | + cellStyleFunction = null; | |
562 | + useCellStyleFunction = false; | |
563 | + } | |
564 | + } | |
565 | + } | |
566 | + | |
567 | + vm.stylesInfo[dataKey.label] = { | |
568 | + useCellStyleFunction: useCellStyleFunction, | |
569 | + cellStyleFunction: cellStyleFunction | |
570 | + }; | |
571 | + | |
572 | + var cellContentFunction = null; | |
573 | + var useCellContentFunction = false; | |
574 | + | |
575 | + if (keySettings.useCellContentFunction === true) { | |
576 | + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { | |
577 | + try { | |
578 | + cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction); | |
579 | + useCellContentFunction = true; | |
580 | + } catch (e) { | |
581 | + cellContentFunction = null; | |
582 | + useCellContentFunction = false; | |
583 | + } | |
584 | + } | |
585 | + } | |
586 | + | |
587 | + vm.contentsInfo[dataKey.label] = { | |
588 | + useCellContentFunction: useCellContentFunction, | |
589 | + cellContentFunction: cellContentFunction | |
590 | + }; | |
591 | + | |
592 | + var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; | |
593 | + vm.columnWidth[dataKey.label] = columnWidth; | |
594 | + } | |
595 | + } | |
596 | + | |
597 | +} | |
\ 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-has-timewindow { | |
18 | + .tb-alarms-table { | |
19 | + md-toolbar { | |
20 | + min-height: 60px; | |
21 | + max-height: 60px; | |
22 | + &.md-table-toolbar { | |
23 | + .md-toolbar-tools { | |
24 | + max-height: 60px; | |
25 | + } | |
26 | + } | |
27 | + } | |
28 | + } | |
29 | +} | |
30 | + | |
31 | +.tb-alarms-table { | |
32 | + | |
33 | + md-toolbar { | |
34 | + min-height: 39px; | |
35 | + max-height: 39px; | |
36 | + &.md-table-toolbar { | |
37 | + .md-toolbar-tools { | |
38 | + max-height: 39px; | |
39 | + } | |
40 | + } | |
41 | + } | |
42 | + | |
43 | + &.tb-data-table { | |
44 | + table.md-table, table.md-table.md-row-select { | |
45 | + tbody { | |
46 | + tr { | |
47 | + td { | |
48 | + &.tb-action-cell { | |
49 | + min-width: 36px; | |
50 | + max-width: 36px; | |
51 | + width: 36px; | |
52 | + } | |
53 | + } | |
54 | + } | |
55 | + } | |
56 | + } | |
57 | + } | |
58 | +} | ... | ... |
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 | + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}"> | |
24 | + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon> | |
25 | + <md-tooltip md-direction="top"> | |
26 | + {{'alarm.search' | translate}} | |
27 | + </md-tooltip> | |
28 | + </md-button> | |
29 | + <md-input-container flex> | |
30 | + <label> </label> | |
31 | + <input ng-model="vm.query.search" placeholder="{{'alarm.search' | translate}}"/> | |
32 | + </md-input-container> | |
33 | + <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()"> | |
34 | + <md-icon aria-label="Close" class="material-icons">close</md-icon> | |
35 | + <md-tooltip md-direction="top"> | |
36 | + {{ 'action.close' | translate }} | |
37 | + </md-tooltip> | |
38 | + </md-button> | |
39 | + </div> | |
40 | + </md-toolbar> | |
41 | + <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedAlarms.length"> | |
42 | + <div class="md-toolbar-tools"> | |
43 | + <span translate="alarm.selected-alarms" | |
44 | + translate-values="{count: vm.selectedAlarms.length}" | |
45 | + translate-interpolation="messageformat"></span> | |
46 | + <span flex></span> | |
47 | + <md-button ng-if="vm.allowAcknowledgment" class="md-icon-button" ng-click="vm.ackAlarms($event)"> | |
48 | + <md-icon>done</md-icon> | |
49 | + <md-tooltip md-direction="top"> | |
50 | + {{ 'alarm.acknowledge' | translate }} | |
51 | + </md-tooltip> | |
52 | + </md-button> | |
53 | + <md-button ng-if="vm.allowClear" class="md-icon-button" ng-click="vm.clearAlarms($event)"> | |
54 | + <md-icon>clear</md-icon> | |
55 | + <md-tooltip md-direction="top"> | |
56 | + {{ 'alarm.clear' | translate }} | |
57 | + </md-tooltip> | |
58 | + </md-button> | |
59 | + </div> | |
60 | + </md-toolbar> | |
61 | + <md-table-container flex> | |
62 | + <table md-table md-row-select="vm.enableSelection" multiple="" ng-model="vm.selectedAlarms"> | |
63 | + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> | |
64 | + <tr md-row> | |
65 | + <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.title }}</span></th> | |
66 | + <th md-column ng-if="vm.displayDetails"><span> </span></th> | |
67 | + </tr> | |
68 | + </thead> | |
69 | + <tbody md-body> | |
70 | + <tr ng-show="vm.alarms.length" md-row md-select="alarm" | |
71 | + md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms" | |
72 | + ng-click="vm.onRowClick($event, alarm)" ng-class="{'tb-current': vm.isCurrent(alarm)}"> | |
73 | + <td md-cell flex ng-repeat="key in vm.alarmSource.dataKeys" | |
74 | + ng-style="vm.cellStyle(alarm, key)" | |
75 | + ng-bind-html="vm.cellContent(alarm, key)"> | |
76 | + </td> | |
77 | + <td md-cell ng-if="vm.displayDetails" class="tb-action-cell"> | |
78 | + <md-button class="md-icon-button" aria-label="{{ 'alarm.details' | translate }}" | |
79 | + ng-click="vm.openAlarmDetails($event, alarm)"> | |
80 | + <md-icon aria-label="{{ 'alarm.details' | translate }}" class="material-icons">more_horiz</md-icon> | |
81 | + <md-tooltip md-direction="top"> | |
82 | + {{ 'alarm.details' | translate }} | |
83 | + </md-tooltip> | |
84 | + </md-button> | |
85 | + </td> | |
86 | + </tr> | |
87 | + </tbody> | |
88 | + </table> | |
89 | + <md-divider></md-divider> | |
90 | + <span ng-show="!vm.alarms.length" | |
91 | + layout-align="center center" | |
92 | + class="no-data-found" translate>alarm.no-alarms-prompt</span> | |
93 | + </md-table-container> | |
94 | + <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions" | |
95 | + md-page="vm.query.page" md-total="{{vm.alarmsCount}}" | |
96 | + md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd"> | |
97 | + </md-table-pagination> | |
98 | + </div> | |
99 | + <span ng-show="!vm.showData" | |
100 | + layout-align="center center" | |
101 | + style="text-transform: uppercase; display: flex;" | |
102 | + class="tb-absolute-fill" translate>alarm.no-data</span> | |
103 | +</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 | } | ... | ... |
... | ... | @@ -281,7 +281,61 @@ 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 | + &:hover { | |
328 | + background-color: rgba(221, 221, 221, 0.3) !important; | |
329 | + } | |
330 | + &.md-selected { | |
331 | + background-color: rgba(221, 221, 221, 0.5) !important; | |
332 | + } | |
333 | + &.tb-current, &.tb-current:hover{ | |
334 | + background-color: rgba(221, 221, 221, 0.65) !important; | |
335 | + } | |
336 | + } | |
337 | + } | |
338 | + } | |
285 | 339 | tr { |
286 | 340 | td { |
287 | 341 | &.tb-action-cell { | ... | ... |