diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 19c75e7b9b..baee88965e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -143,4 +143,30 @@ public class AlarmController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) + @ResponseBody + public AlarmSeverity getHighestAlarmSeverity( + @PathVariable("entityType") String strEntityType, + @PathVariable("entityId") String strEntityId, + @RequestParam(required = false) String searchStatus, + @RequestParam(required = false) String status + ) throws ThingsboardException { + checkParameter("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); + AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); + AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); + if (alarmSearchStatus != null && alarmStatus != null) { + throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + checkEntityId(entityId); + try { + return alarmService.findHighestAlarmSeverity(entityId, alarmSearchStatus, alarmStatus); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 3556d51f09..63fba03a52 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -16,10 +16,8 @@ package org.thingsboard.server.dao.alarm; import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmId; -import org.thingsboard.server.common.data.alarm.AlarmInfo; -import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.*; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.TimePageData; /** @@ -39,4 +37,7 @@ public interface AlarmService { ListenableFuture> findAlarms(AlarmQuery query); + AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus, + AlarmStatus alarmStatus); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 94f8fdbf76..c3c1886d56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.*; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -45,6 +46,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -240,6 +242,46 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ }); } + @Override + public AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus, + AlarmStatus alarmStatus) { + TimePageLink nextPageLink = new TimePageLink(100); + boolean hasNext = true; + AlarmSeverity highestSeverity = null; + AlarmQuery query; + while (hasNext) { + query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false); + List alarms; + try { + alarms = alarmDao.findAlarms(query).get(); + } catch (ExecutionException | InterruptedException e) { + log.warn("Failed to find highest alarm severity. EntityId: [{}], AlarmSearchStatus: [{}], AlarmStatus: [{}]", + entityId, alarmSearchStatus, alarmStatus); + throw new RuntimeException(e); + } + hasNext = alarms.size() == nextPageLink.getLimit(); + if (hasNext) { + nextPageLink = new TimePageData<>(alarms, nextPageLink).getNextPageLink(); + } + if (alarms.isEmpty()) { + continue; + } else { + List sorted = new ArrayList(alarms); + sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity())); + AlarmSeverity severity = sorted.get(0).getSeverity(); + if (severity == AlarmSeverity.CRITICAL) { + highestSeverity = severity; + break; + } else if (highestSeverity == null) { + highestSeverity = severity; + } else { + highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity; + } + } + } + return highestSeverity; + } + private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException { log.debug("Deleting Alarm relation: {}", alarmRelation); relationService.deleteRelation(alarmRelation).get(); diff --git a/ui/src/app/alarm/alarm-details-dialog.controller.js b/ui/src/app/alarm/alarm-details-dialog.controller.js index 0cc05ef295..6c82e2086c 100644 --- a/ui/src/app/alarm/alarm-details-dialog.controller.js +++ b/ui/src/app/alarm/alarm-details-dialog.controller.js @@ -23,11 +23,14 @@ import './alarm-details-dialog.scss'; const js_beautify = beautify.js; /*@ngInject*/ -export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, alarmService, alarmId, showingCallback) { +export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, + alarmService, alarmId, allowAcknowledgment, allowClear, showingCallback) { var vm = this; vm.alarmId = alarmId; + vm.allowAcknowledgment = allowAcknowledgment; + vm.allowClear = allowClear; vm.types = types; vm.alarm = null; diff --git a/ui/src/app/alarm/alarm-details-dialog.tpl.html b/ui/src/app/alarm/alarm-details-dialog.tpl.html index c958201ea9..48f991c52a 100644 --- a/ui/src/app/alarm/alarm-details-dialog.tpl.html +++ b/ui/src/app/alarm/alarm-details-dialog.tpl.html @@ -84,16 +84,16 @@ - {{ 'alarm.acknowledge' | translate }} - {{ 'alarm.clear' | diff --git a/ui/src/app/alarm/alarm-row.directive.js b/ui/src/app/alarm/alarm-row.directive.js index 9cb9bedd68..09c04f9594 100644 --- a/ui/src/app/alarm/alarm-row.directive.js +++ b/ui/src/app/alarm/alarm-row.directive.js @@ -40,7 +40,12 @@ export default function AlarmRowDirective($compile, $templateCache, types, $mdDi controller: 'AlarmDetailsDialogController', controllerAs: 'vm', templateUrl: alarmDetailsDialogTemplate, - locals: {alarmId: scope.alarm.id.id, showingCallback: onShowingCallback}, + locals: { + alarmId: scope.alarm.id.id, + allowAcknowledgment: true, + allowClear: true, + showingCallback: onShowingCallback + }, parent: angular.element($document[0].body), targetEvent: $event, fullscreen: true, diff --git a/ui/src/app/api/alarm.service.js b/ui/src/app/api/alarm.service.js index 34e6b59e20..3b7a735be2 100644 --- a/ui/src/app/api/alarm.service.js +++ b/ui/src/app/api/alarm.service.js @@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', []) .name; /*@ngInject*/ -function AlarmService($http, $q, $interval, $filter) { +function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) { + + var alarmSourceListeners = {}; + + var simulatedAlarm = { + createdTime: (new Date).getTime(), + startTs: (new Date).getTime(), + endTs: 0, + ackTs: 0, + clearTs: 0, + originatorName: 'Simulated', + originator: { + entityType: "DEVICE", + id: "1" + }, + type: 'TEMPERATURE', + severity: "MAJOR", + status: types.alarmStatus.activeUnack, + details: { + message: "Temperature is high!" + } + }; + var service = { getAlarm: getAlarm, getAlarmInfo: getAlarmInfo, @@ -26,8 +48,11 @@ function AlarmService($http, $q, $interval, $filter) { ackAlarm: ackAlarm, clearAlarm: clearAlarm, getAlarms: getAlarms, + getHighestAlarmSeverity: getHighestAlarmSeverity, pollAlarms: pollAlarms, - cancelPollAlarms: cancelPollAlarms + cancelPollAlarms: cancelPollAlarms, + subscribeForAlarms: subscribeForAlarms, + unsubscribeFromAlarms: unsubscribeFromAlarms } return service; @@ -141,6 +166,23 @@ function AlarmService($http, $q, $interval, $filter) { return deferred.promise; } + function getHighestAlarmSeverity(entityType, entityId, alarmSearchStatus, alarmStatus, config) { + var deferred = $q.defer(); + var url = '/api/alarm/highestSeverity/' + entityType + '/' + entityId; + + if (alarmSearchStatus) { + url += '?searchStatus=' + alarmSearchStatus; + } else if (alarmStatus) { + url += '?status=' + alarmStatus; + } + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + function fetchAlarms(alarmsQuery, pageLink, deferred, alarmsList) { getAlarms(alarmsQuery.entityType, alarmsQuery.entityId, pageLink, alarmsQuery.alarmSearchStatus, alarmsQuery.alarmStatus, @@ -171,12 +213,21 @@ function AlarmService($http, $q, $interval, $filter) { pageLink = { limit: alarmsQuery.limit }; - } else { + } else if (alarmsQuery.interval) { pageLink = { limit: 100, startTime: time - alarmsQuery.interval }; + } else if (alarmsQuery.startTime) { + pageLink = { + limit: 100, + startTime: alarmsQuery.startTime + } + if (alarmsQuery.endTime) { + pageLink.endTime = alarmsQuery.endTime; + } } + fetchAlarms(alarmsQuery, pageLink, deferred); return deferred.promise; } @@ -211,4 +262,59 @@ function AlarmService($http, $q, $interval, $filter) { } } + function subscribeForAlarms(alarmSourceListener) { + alarmSourceListener.id = utils.guid(); + alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener; + var alarmSource = alarmSourceListener.alarmSource; + if (alarmSource.type == types.datasourceType.function) { + $timeout(function() { + alarmSourceListener.alarmsUpdated([simulatedAlarm], false); + }); + } else if (alarmSource.entityType && alarmSource.entityId) { + var pollingInterval = alarmSourceListener.alarmsPollingInterval; + alarmSourceListener.alarmsQuery = { + entityType: alarmSource.entityType, + entityId: alarmSource.entityId, + alarmSearchStatus: alarmSourceListener.alarmSearchStatus, + alarmStatus: null + } + var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'}); + if (originatorKeys && originatorKeys.length) { + alarmSourceListener.alarmsQuery.fetchOriginator = true; + } + var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow; + if (subscriptionTimewindow.realtimeWindowMs) { + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs; + } else { + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs; + alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs; + } + alarmSourceListener.alarmsQuery.onAlarms = function(alarms) { + if (subscriptionTimewindow.realtimeWindowMs) { + var now = Date.now(); + if (alarmSourceListener.lastUpdateTs) { + var interval = now - alarmSourceListener.lastUpdateTs; + alarmSourceListener.alarmsQuery.startTime += interval; + } else { + alarmSourceListener.lastUpdateTs = now; + } + } + alarmSourceListener.alarmsUpdated(alarms, false); + } + onPollAlarms(alarmSourceListener.alarmsQuery); + alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval, + 0, false, alarmSourceListener.alarmsQuery); + } + + } + + function unsubscribeFromAlarms(alarmSourceListener) { + if (alarmSourceListener && alarmSourceListener.id) { + if (alarmSourceListener.pollPromise) { + $interval.cancel(alarmSourceListener.pollPromise); + alarmSourceListener.pollPromise = null; + } + delete alarmSourceListeners[alarmSourceListener.id]; + } + } } diff --git a/ui/src/app/api/alias-controller.js b/ui/src/app/api/alias-controller.js index 60e8a31005..9acd5aaa8b 100644 --- a/ui/src/app/api/alias-controller.js +++ b/ui/src/app/api/alias-controller.js @@ -14,8 +14,6 @@ * limitations under the License. */ -const varsRegex = /\$\{([^\}]*)\}/g; - export default class AliasController { constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) { @@ -113,14 +111,14 @@ export default class AliasController { } } - resolveDatasource(datasource) { + resolveDatasource(datasource, isSingle) { var deferred = this.$q.defer(); if (datasource.type === this.types.datasourceType.entity) { if (datasource.entityAliasId) { this.getAliasInfo(datasource.entityAliasId).then( function success(aliasInfo) { datasource.aliasName = aliasInfo.alias; - if (aliasInfo.resolveMultiple) { + if (aliasInfo.resolveMultiple && !isSingle) { var newDatasource; var resolvedEntities = aliasInfo.resolvedEntities; if (resolvedEntities && resolvedEntities.length) { @@ -178,30 +176,44 @@ export default class AliasController { return deferred.promise; } + resolveAlarmSource(alarmSource) { + var deferred = this.$q.defer(); + var aliasCtrl = this; + this.resolveDatasource(alarmSource, true).then( + function success(datasources) { + var datasource = datasources[0]; + if (datasource.type === aliasCtrl.types.datasourceType.function) { + var name; + if (datasource.name && datasource.name.length) { + name = datasource.name; + } else { + name = aliasCtrl.types.datasourceType.function; + } + datasource.name = name; + datasource.aliasName = name; + datasource.entityName = name; + } else if (datasource.unresolvedStateEntity) { + datasource.name = "Unresolved"; + datasource.entityName = "Unresolved"; + } + deferred.resolve(datasource); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + resolveDatasources(datasources) { + var aliasCtrl = this; + function updateDataKeyLabel(dataKey, datasource) { if (!dataKey.pattern) { dataKey.pattern = angular.copy(dataKey.label); } - var pattern = dataKey.pattern; - var label = dataKey.pattern; - var match = varsRegex.exec(pattern); - while (match !== null) { - var variable = match[0]; - var variableName = match[1]; - if (variableName === 'dsName') { - label = label.split(variable).join(datasource.name); - } else if (variableName === 'entityName') { - label = label.split(variable).join(datasource.entityName); - } else if (variableName === 'deviceName') { - label = label.split(variable).join(datasource.entityName); - } else if (variableName === 'aliasName') { - label = label.split(variable).join(datasource.aliasName); - } - match = varsRegex.exec(pattern); - } - dataKey.label = label; + dataKey.label = aliasCtrl.utils.createLabelFromDatasource(datasource, dataKey.pattern); } function updateDatasourceKeyLabels(datasource) { @@ -213,7 +225,7 @@ export default class AliasController { var deferred = this.$q.defer(); var newDatasources = angular.copy(datasources); var datasorceResolveTasks = []; - var aliasCtrl = this; + newDatasources.forEach(function (datasource) { var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource); datasorceResolveTasks.push(resolveDatasourceTask); diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index 1d4eb301c4..030b8521ee 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -64,6 +64,45 @@ export default class Subscription { deferred.resolve(subscription); } ); + } else if (this.type === this.ctx.types.widgetType.alarm.value) { + this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; + this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; + this.callbacks.dataLoading = this.callbacks.dataLoading || function(){}; + this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){}; + this.alarmSource = options.alarmSource; + + this.alarmSearchStatus = angular.isDefined(options.alarmSearchStatus) ? + options.alarmSearchStatus : this.ctx.types.alarmSearchStatus.any; + this.alarmsPollingInterval = angular.isDefined(options.alarmsPollingInterval) ? + options.alarmsPollingInterval : 5000; + + this.alarmSourceListener = null; + this.alarms = []; + + this.originalTimewindow = null; + this.timeWindow = { + stDiff: this.ctx.stDiff + } + this.useDashboardTimewindow = options.useDashboardTimewindow; + + if (this.useDashboardTimewindow) { + this.timeWindowConfig = angular.copy(options.dashboardTimewindow); + } else { + this.timeWindowConfig = angular.copy(options.timeWindowConfig); + } + + this.subscriptionTimewindow = null; + + this.loadingData = false; + this.displayLegend = false; + this.initAlarmSubscription().then( + function success() { + deferred.resolve(subscription); + }, + function fail() { + deferred.reject(); + } + ); } else { this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; @@ -132,6 +171,43 @@ export default class Subscription { return deferred.promise; } + initAlarmSubscription() { + var deferred = this.ctx.$q.defer(); + if (!this.ctx.aliasController) { + this.configureAlarmsData(); + deferred.resolve(); + } else { + var subscription = this; + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).then( + function success(alarmSource) { + subscription.alarmSource = alarmSource; + subscription.configureAlarmsData(); + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + } + return deferred.promise; + } + + configureAlarmsData() { + var subscription = this; + var registration; + if (this.useDashboardTimewindow) { + registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { + if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { + subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); + subscription.update(); + } + }); + this.registrations.push(registration); + } else { + this.startWatchingTimewindow(); + } + } + initDataSubscription() { var deferred = this.ctx.$q.defer(); if (!this.ctx.aliasController) { @@ -210,8 +286,7 @@ export default class Subscription { registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); - subscription.unsubscribe(); - subscription.subscribe(); + subscription.update(); } }); this.registrations.push(registration); @@ -227,8 +302,7 @@ export default class Subscription { return subscription.timeWindowConfig; }, function (newTimewindow, prevTimewindow) { if (!angular.equals(newTimewindow, prevTimewindow)) { - subscription.unsubscribe(); - subscription.subscribe(); + subscription.update(); } }, true); this.registrations.push(this.timeWindowWatchRegistration); @@ -393,6 +467,8 @@ export default class Subscription { onAliasesChanged(aliasIds) { if (this.type === this.ctx.types.widgetType.rpc.value) { return this.checkRpcTarget(aliasIds); + } else if (this.type === this.ctx.types.widgetType.alarm.value) { + return this.checkAlarmSource(aliasIds); } else { return this.checkSubscriptions(aliasIds); } @@ -429,8 +505,7 @@ export default class Subscription { this.timeWindowConfig = angular.copy(this.originalTimewindow); this.originalTimewindow = null; this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); - this.unsubscribe(); - this.subscribe(); + this.update(); this.startWatchingTimewindow(); } } @@ -446,8 +521,7 @@ export default class Subscription { } this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); - this.unsubscribe(); - this.subscribe(); + this.update(); this.startWatchingTimewindow(); } } @@ -516,6 +590,15 @@ export default class Subscription { } } + alarmsUpdated(alarms, apply) { + this.notifyDataLoaded(); + this.alarms = alarms; + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { + this.updateTimewindow(); + } + this.onDataUpdated(apply); + } + updateLegend(dataIndex, data, apply) { var dataKey = this.legendData.keys[dataIndex].dataKey; var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; @@ -536,66 +619,115 @@ export default class Subscription { this.callbacks.legendDataUpdated(this, apply !== false); } + update() { + this.unsubscribe(); + this.subscribe(); + } + subscribe() { if (this.type === this.ctx.types.widgetType.rpc.value) { return; } + if (this.type === this.ctx.types.widgetType.alarm.value) { + this.alarmsSubscribe(); + } else { + this.notifyDataLoading(); + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { + this.updateRealtimeSubscription(); + if (this.subscriptionTimewindow.fixedWindow) { + this.onDataUpdated(); + } + } + var index = 0; + for (var i = 0; i < this.datasources.length; i++) { + var datasource = this.datasources[i]; + if (angular.isFunction(datasource)) + continue; + + var subscription = this; + + var listener = { + subscriptionType: this.type, + subscriptionTimewindow: this.subscriptionTimewindow, + datasource: datasource, + entityType: datasource.entityType, + entityId: datasource.entityId, + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); + }, + updateRealtimeSubscription: function () { + this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); + return this.subscriptionTimewindow; + }, + setRealtimeSubscription: function (subscriptionTimewindow) { + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); + }, + datasourceIndex: index + }; + + for (var a = 0; a < datasource.dataKeys.length; a++) { + this.data[index + a].data = []; + } + + index += datasource.dataKeys.length; + + this.datasourceListeners.push(listener); + this.ctx.datasourceService.subscribeToDatasource(listener); + if (datasource.unresolvedStateEntity) { + this.notifyDataLoaded(); + this.onDataUpdated(); + } + + } + } + } + + alarmsSubscribe() { this.notifyDataLoading(); - if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { + if (this.timeWindowConfig) { this.updateRealtimeSubscription(); if (this.subscriptionTimewindow.fixedWindow) { this.onDataUpdated(); } } - var index = 0; - for (var i = 0; i < this.datasources.length; i++) { - var datasource = this.datasources[i]; - if (angular.isFunction(datasource)) - continue; - - var subscription = this; - - var listener = { - subscriptionType: this.type, - subscriptionTimewindow: this.subscriptionTimewindow, - datasource: datasource, - entityType: datasource.entityType, - entityId: datasource.entityId, - dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { - subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); - }, - updateRealtimeSubscription: function () { - this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); - return this.subscriptionTimewindow; - }, - setRealtimeSubscription: function (subscriptionTimewindow) { - subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); - }, - datasourceIndex: index - }; - - for (var a = 0; a < datasource.dataKeys.length; a++) { - this.data[index + a].data = []; + var subscription = this; + this.alarmSourceListener = { + subscriptionTimewindow: this.subscriptionTimewindow, + alarmSource: this.alarmSource, + alarmSearchStatus: this.alarmSearchStatus, + alarmsPollingInterval: this.alarmsPollingInterval, + alarmsUpdated: function(alarms, apply) { + subscription.alarmsUpdated(alarms, apply); } + } + this.alarms = []; - index += datasource.dataKeys.length; + this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener); - this.datasourceListeners.push(listener); - this.ctx.datasourceService.subscribeToDatasource(listener); - if (datasource.unresolvedStateEntity) { - this.notifyDataLoaded(); - this.onDataUpdated(); - } + if (this.alarmSource.unresolvedStateEntity) { + this.notifyDataLoaded(); + this.onDataUpdated(); } } unsubscribe() { if (this.type !== this.ctx.types.widgetType.rpc.value) { - for (var i = 0; i < this.datasourceListeners.length; i++) { - var listener = this.datasourceListeners[i]; - this.ctx.datasourceService.unsubscribeFromDatasource(listener); + if (this.type == this.ctx.types.widgetType.alarm.value) { + this.alarmsUnsubscribe(); + } else { + for (var i = 0; i < this.datasourceListeners.length; i++) { + var listener = this.datasourceListeners[i]; + this.ctx.datasourceService.unsubscribeFromDatasource(listener); + } + this.datasourceListeners = []; } - this.datasourceListeners = []; + } + } + + alarmsUnsubscribe() { + if (this.alarmSourceListener) { + this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener); + this.alarmSourceListener = null; } } @@ -607,6 +739,14 @@ export default class Subscription { } } + checkAlarmSource(aliasIds) { + if (this.alarmSource && this.alarmSource.entityAliasId) { + return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1; + } else { + return false; + } + } + checkSubscriptions(aliasIds) { var subscriptionsChanged = false; for (var i = 0; i < this.datasourceListeners.length; i++) { diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 21c38e254c..1c5e5f4126 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -19,6 +19,7 @@ import tinycolor from 'tinycolor2'; import thingsboardLedLight from '../components/led-light.directive'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; +import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; import TbFlot from '../widget/lib/flot-widget'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; @@ -33,7 +34,7 @@ import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, - thingsboardTypes, thingsboardUtils]) + thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html index 8370d3dbac..c4c0267993 100644 --- a/ui/src/app/asset/assets.tpl.html +++ b/ui/src/app/asset/assets.tpl.html @@ -31,7 +31,7 @@ on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)" on-delete-asset="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> - + - + - + - + - + diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index b281ad1043..81ce0fa4f9 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', []) name: "aggregation.none" } }, + alarmFields: { + createdTime: { + value: "createdTime", + name: "alarm.created-time", + time: true + }, + startTime: { + value: "startTs", + name: "alarm.start-time", + time: true + }, + endTime: { + value: "endTs", + name: "alarm.end-time", + time: true + }, + ackTime: { + value: "ackTs", + name: "alarm.ack-time", + time: true + }, + clearTime: { + value: "clearTs", + name: "alarm.clear-time", + time: true + }, + originator: { + value: "originatorName", + name: "alarm.originator" + }, + originatorType: { + value: "originator.entityType", + name: "alarm.originator-type" + }, + type: { + value: "type", + name: "alarm.type" + }, + severity: { + value: "severity", + name: "alarm.severity" + }, + status: { + value: "status", + name: "alarm.status" + } + }, alarmStatus: { activeUnack: "ACTIVE_UNACK", activeAck: "ACTIVE_ACK", @@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', []) alarmSeverity: { "CRITICAL": { name: "alarm.severity-critical", - class: "tb-critical" + class: "tb-critical", + color: "red" }, "MAJOR": { name: "alarm.severity-major", - class: "tb-major" + class: "tb-major", + color: "orange" }, "MINOR": { name: "alarm.severity-minor", - class: "tb-minor" + class: "tb-minor", + color: "#ffca3d" }, "WARNING": { name: "alarm.severity-warning", - class: "tb-warning" + class: "tb-warning", + color: "#abab00" }, "INDETERMINATE": { name: "alarm.severity-indeterminate", - class: "tb-indeterminate" + class: "tb-indeterminate", + color: "green" } }, aliasFilterType: { @@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', []) dataKeyType: { timeseries: "timeseries", attribute: "attribute", - function: "function" + function: "function", + alarm: "alarm" }, componentType: { filter: "FILTER", @@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', []) alias: "basic_gpio_control" } }, + alarm: { + value: "alarm", + name: "widget.alarm", + template: { + bundleAlias: "alarm_widgets", + alias: "alarms_table" + } + }, static: { value: "static", name: "widget.static", @@ -333,7 +394,7 @@ export default angular.module('thingsboard.types', []) cards: "cards" }, translate: { - dashboardStatePrefix: "dashboardState.state." + customTranslationsPrefix: "custom." } } ).name; diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 8d2e565f4a..a3433c255f 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -21,8 +21,10 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) .factory('utils', Utils) .name; +const varsRegex = /\$\{([^\}]*)\}/g; + /*@ngInject*/ -function Utils($mdColorPalette, $rootScope, $window, types) { +function Utils($mdColorPalette, $rootScope, $window, $translate, types) { var predefinedFunctions = {}, predefinedFunctionsList = [], @@ -93,9 +95,32 @@ function Utils($mdColorPalette, $rootScope, $window, types) { dataKeys: [angular.copy(defaultDataKey)] }; + var defaultAlarmFields = [ + 'createdTime', + 'originator', + 'type', + 'severity', + 'status' + ]; + + var defaultAlarmDataKeys = []; + for (var i=0;i -
+
  • @@ -37,7 +37,8 @@ class="tb-widget" ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget), - 'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}" + 'md-whiteframe-4dp': vm.dropWidgetShadow(widget), + 'tb-has-timewindow': vm.hasTimewindow(widget)}" tb-mousedown="vm.widgetMouseDown($event, widget)" ng-click="vm.widgetClicked($event, widget)" tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)" @@ -45,11 +46,21 @@ color: vm.widgetColor(widget), backgroundColor: vm.widgetBackgroundColor(widget), padding: vm.widgetPadding(widget)}"> -
    - {{widget.config.title}} - +
    + {{vm.widgetTitle(widget)}} +
    -
    +
    + + + {{ action.name | translate }} + + + diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js index 90810afeb2..d19617f440 100644 --- a/ui/src/app/components/datakey-config.directive.js +++ b/ui/src/app/components/datakey-config.directive.js @@ -63,6 +63,12 @@ function DatakeyConfig($compile, $templateCache, $q, types) { element.html(template); scope.types = types; + + scope.alarmFields = []; + for (var alarmField in types.alarmFields) { + scope.alarmFields.push(alarmField); + } + scope.selectedKey = null; scope.keySearchText = null; scope.usePostProcessing = false; @@ -112,21 +118,39 @@ function DatakeyConfig($compile, $templateCache, $q, types) { }, true); scope.keysSearch = function (searchText) { - if (scope.entityAlias) { - var deferred = $q.defer(); - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: scope.model.type}) - .then(function (keys) { - keys.push(searchText); - deferred.resolve(keys); - }, function (e) { - deferred.reject(e); - }); - return deferred.promise; + if (scope.model.type === types.dataKeyType.alarm) { + var dataKeys = searchText ? scope.alarmFields.filter( + scope.createFilterForDataKey(searchText)) : scope.alarmFields; + dataKeys.push(searchText); + return dataKeys; } else { - return $q.when([]); + if (scope.entityAlias) { + var deferred = $q.defer(); + scope.fetchEntityKeys({ + entityAliasId: scope.entityAlias.id, + query: searchText, + type: scope.model.type + }) + .then(function (keys) { + keys.push(searchText); + deferred.resolve(keys); + }, function (e) { + deferred.reject(e); + }); + return deferred.promise; + } else { + return $q.when([]); + } } }; + scope.createFilterForDataKey = function (query) { + var lowercaseQuery = angular.lowercase(query); + return function filterFn(dataKey) { + return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0); + }; + }; + $compile(element.contents())(scope); } diff --git a/ui/src/app/components/datakey-config.tpl.html b/ui/src/app/components/datakey-config.tpl.html index 755c4f49a8..bb7a4b9e9b 100644 --- a/ui/src/app/components/datakey-config.tpl.html +++ b/ui/src/app/components/datakey-config.tpl.html @@ -16,7 +16,9 @@ --> - + placeholder="{{ 'entity.key-name' | translate }}" + md-floating-label="{{ 'entity.key' | translate }}"> {{item}}
    @@ -48,7 +50,7 @@ md-color-history="false">
    -
    +
    diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js index da4327491e..e6ec8f83a6 100644 --- a/ui/src/app/components/datasource-entity.directive.js +++ b/ui/src/app/components/datasource-entity.directive.js @@ -45,12 +45,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc scope.ngModelCtrl = ngModelCtrl; scope.types = types; + scope.alarmFields = []; + for (var alarmField in types.alarmFields) { + scope.alarmFields.push(alarmField); + } + scope.selectedTimeseriesDataKey = null; scope.timeseriesDataKeySearchText = null; scope.selectedAttributeDataKey = null; scope.attributeDataKeySearchText = null; + scope.selectedAlarmDataKey = null; + scope.alarmDataKeySearchText = null; + scope.updateValidity = function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; @@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc }); scope.$watch('timeseriesDataKeys', function () { - if (ngModelCtrl.$viewValue) { - var dataKeys = []; - dataKeys = dataKeys.concat(scope.timeseriesDataKeys); - dataKeys = dataKeys.concat(scope.attributeDataKeys); - ngModelCtrl.$viewValue.dataKeys = dataKeys; - scope.updateValidity(); - } + updateDataKeys(); }, true); scope.$watch('attributeDataKeys', function () { + updateDataKeys(); + }, true); + + scope.$watch('alarmDataKeys', function () { + updateDataKeys(); + }, true); + + function updateDataKeys() { if (ngModelCtrl.$viewValue) { var dataKeys = []; dataKeys = dataKeys.concat(scope.timeseriesDataKeys); dataKeys = dataKeys.concat(scope.attributeDataKeys); + dataKeys = dataKeys.concat(scope.alarmDataKeys); ngModelCtrl.$viewValue.dataKeys = dataKeys; scope.updateValidity(); } - }, true); + } ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { @@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc } var timeseriesDataKeys = []; var attributeDataKeys = []; + var alarmDataKeys = []; for (var d in ngModelCtrl.$viewValue.dataKeys) { var dataKey = ngModelCtrl.$viewValue.dataKeys[d]; if (dataKey.type === types.dataKeyType.timeseries) { timeseriesDataKeys.push(dataKey); } else if (dataKey.type === types.dataKeyType.attribute) { attributeDataKeys.push(dataKey); + } else if (dataKey.type === types.dataKeyType.alarm) { + alarmDataKeys.push(dataKey); } } scope.timeseriesDataKeys = timeseriesDataKeys; scope.attributeDataKeys = attributeDataKeys; + scope.alarmDataKeys = alarmDataKeys; } }; @@ -135,6 +150,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') { scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : ''; } + if (!scope.alarmDataKeySearchText || scope.alarmDataKeySearchText === '') { + scope.alarmDataKeySearchText = scope.alarmDataKeySearchText === '' ? null : ''; + } }; scope.transformTimeseriesDataKeyChip = function (chip) { @@ -145,6 +163,10 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute}); }; + scope.transformAlarmDataKeyChip = function (chip) { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm}); + }; + scope.showColorPicker = function (event, dataKey) { $mdColorPicker.show({ value: dataKey.color, @@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc scope.timeseriesDataKeys[index] = dataKey; } else if (dataKey.type === types.dataKeyType.attribute) { scope.attributeDataKeys[index] = dataKey; + } else if (dataKey.type === types.dataKeyType.alarm) { + scope.alarmDataKeys[index] = dataKey; } ngModelCtrl.$setDirty(); }, function () { @@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc }; scope.dataKeysSearch = function (searchText, type) { - if (scope.entityAlias) { - var deferred = $q.defer(); - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type}) - .then(function (dataKeys) { - deferred.resolve(dataKeys); - }, function (e) { - deferred.reject(e); - }); - return deferred.promise; + if (scope.widgetType == types.widgetType.alarm.value) { + var dataKeys = searchText ? scope.alarmFields.filter( + scope.createFilterForDataKey(searchText)) : scope.alarmFields; + return dataKeys; } else { - return $q.when([]); + if (scope.entityAlias) { + var deferred = $q.defer(); + scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type}) + .then(function (dataKeys) { + deferred.resolve(dataKeys); + }, function (e) { + deferred.reject(e); + }); + return deferred.promise; + } else { + return $q.when([]); + } } }; + scope.createFilterForDataKey = function (query) { + var lowercaseQuery = angular.lowercase(query); + return function filterFn(dataKey) { + return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0); + }; + }; + scope.createKey = function (event, chipsId) { var chipsChild = $(chipsId, element)[0].firstElementChild; var el = angular.element(chipsChild); diff --git a/ui/src/app/components/datasource-entity.scss b/ui/src/app/components/datasource-entity.scss index 7a87fc700c..b9e892bf14 100644 --- a/ui/src/app/components/datasource-entity.scss +++ b/ui/src/app/components/datasource-entity.scss @@ -15,7 +15,7 @@ */ @import '../../scss/constants'; -.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete { +.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete, .tb-alarm-datakey-autocomplete { .tb-not-found { display: block; line-height: 1.5; diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html index 2fb4608a31..9876491086 100644 --- a/ui/src/app/components/datasource-entity.tpl.html +++ b/ui/src/app/components/datasource-entity.tpl.html @@ -24,7 +24,7 @@
    - + + + {{item}} + +
    +
    + entity.no-keys-found +
    +
    + entity.no-key-matching + + entity.create-new-key + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + {{$chip.label}} +
    +
    :
    +
    + {{$chip.name}} + f({{$chip.name}}) +
    +
    + + edit + +
    +
    +
    diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js index fe546962c7..2a2ae82111 100644 --- a/ui/src/app/components/datasource-func.directive.js +++ b/ui/src/app/components/datasource-func.directive.js @@ -43,18 +43,27 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, element.html(template); scope.ngModelCtrl = ngModelCtrl; + scope.types = types; + scope.functionTypes = utils.getPredefinedFunctionsList(); + scope.alarmFields = []; + for (var alarmField in types.alarmFields) { + scope.alarmFields.push(alarmField); + } scope.selectedDataKey = null; scope.dataKeySearchText = null; + scope.selectedAlarmDataKey = null; + scope.alarmDataKeySearchText = null; + scope.updateValidity = function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; var dataValid = angular.isDefined(value) && value != null; ngModelCtrl.$setValidity('deviceData', dataValid); if (dataValid) { - ngModelCtrl.$setValidity('funcTypes', + ngModelCtrl.$setValidity('datasourceKeys', angular.isDefined(value.dataKeys) && value.dataKeys != null && value.dataKeys.length > 0); @@ -63,13 +72,22 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, }; scope.$watch('funcDataKeys', function () { + updateDataKeys(); + }, true); + + scope.$watch('alarmDataKeys', function () { + updateDataKeys(); + }, true); + + function updateDataKeys() { if (ngModelCtrl.$viewValue) { var dataKeys = []; dataKeys = dataKeys.concat(scope.funcDataKeys); + dataKeys = dataKeys.concat(scope.alarmDataKeys); ngModelCtrl.$viewValue.dataKeys = dataKeys; scope.updateValidity(); } - }, true); + } scope.$watch('datasourceName', function () { if (ngModelCtrl.$viewValue) { @@ -81,18 +99,31 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { var funcDataKeys = []; + var alarmDataKeys = []; if (ngModelCtrl.$viewValue.dataKeys) { - funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys); + for (var d=0;d - +
    - device.no-keys-found
    - device.no-key-matching + entity.no-key-matching - device.create-new-key + entity.create-new-key
    @@ -75,8 +76,61 @@
    + + + {{item}} + +
    +
    + entity.no-keys-found +
    +
    + entity.no-key-matching + + entity.create-new-key + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + {{$chip.label}} +
    +
    :
    +
    + {{$chip.name}} + f({{$chip.name}}) +
    +
    + + edit + +
    +
    +
    diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js index eb9eafc4a4..b5327e298c 100644 --- a/ui/src/app/components/datasource.directive.js +++ b/ui/src/app/components/datasource.directive.js @@ -30,7 +30,7 @@ export default angular.module('thingsboard.directives.datasource', [thingsboardT .name; /*@ngInject*/ -function Datasource($compile, $templateCache, types) { +function Datasource($compile, $templateCache, utils, types) { var linker = function (scope, element, attrs, ngModelCtrl) { @@ -53,8 +53,12 @@ function Datasource($compile, $templateCache, types) { } scope.$watch('model.type', function (newType, prevType) { - if (newType != prevType) { - scope.model.dataKeys = []; + if (newType && prevType && newType != prevType) { + if (scope.widgetType == types.widgetType.alarm.value) { + scope.model.dataKeys = utils.getDefaultAlarmDataKeys(); + } else { + scope.model.dataKeys = []; + } } }); @@ -63,9 +67,10 @@ function Datasource($compile, $templateCache, types) { }, true); ngModelCtrl.$render = function () { - scope.model = {}; if (ngModelCtrl.$viewValue) { scope.model = ngModelCtrl.$viewValue; + } else { + scope.model = {}; } }; diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html index 0e91d9832e..0633f5f2bf 100644 --- a/ui/src/app/components/datasource.tpl.html +++ b/ui/src/app/components/datasource.tpl.html @@ -29,6 +29,7 @@ ng-model="model" datakey-settings-schema="datakeySettingsSchema" ng-required="model.type === types.datasourceType.function" + widget-type="widgetType" generate-data-key="generateDataKey({chip: chip, type: type})"> 0; ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); + } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { + valid = config && config.alarmSource; + ngModelCtrl.$setValidity('alarmSource', valid); } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { valid = config && config.datasources && config.datasources.length > 0; ngModelCtrl.$setValidity('datasources', valid); @@ -190,7 +209,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti }; scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' + - 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () { + 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' + + 'alarmSearchStatus + alarmsPollingInterval + showLegend', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; if (value.config) { @@ -210,6 +230,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti config.units = scope.units; config.decimals = scope.decimals; config.useDashboardTimewindow = scope.useDashboardTimewindow; + config.alarmSearchStatus = scope.alarmSearchStatus; + config.alarmsPollingInterval = scope.alarmsPollingInterval; config.showLegend = scope.showLegend; } if (value.layout) { @@ -253,7 +275,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti }, true); scope.$watch('datasources', function () { - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config + && scope.widgetType !== types.widgetType.rpc.value + && scope.widgetType !== types.widgetType.alarm.value && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; var config = value.config; @@ -286,6 +310,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti } }); + scope.$watch('alarmSource.value', function () { + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { + var value = ngModelCtrl.$viewValue; + var config = value.config; + if (scope.alarmSource.value) { + config.alarmSource = scope.alarmSource.value; + } else { + config.alarmSource = null; + } + ngModelCtrl.$setViewValue(value); + scope.updateValidity(); + } + }); + scope.addDatasource = function () { var newDatasource; if (scope.functionsOnly) { @@ -320,10 +358,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti return chip; } + var label = chip; + if (type === types.dataKeyType.alarm) { + var alarmField = types.alarmFields[chip]; + if (alarmField) { + label = $translate.instant(alarmField.name)+''; + } + } + label = scope.genNextLabel(label); + var result = { name: chip, type: type, - label: scope.genNextLabel(chip), + label: label, color: scope.genNextColor(), settings: {}, _hash: Math.random() @@ -351,15 +398,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti var matches = false; do { matches = false; - if (value.config.datasources) { - for (var d=0;d -
    {{ 'widget-config.use-dashboard-timewindow' | translate }}
    widget-config.timewindow - +
    +
    + + + + + {{ ('alarm.search-status.' + searchStatus) | translate }} + + + + + + +
    +
    alarm.polling-interval-required
    +
    alarm.min-polling-interval-message
    +
    +
    +
    + ng-show="widgetType !== types.widgetType.rpc.value + && widgetType !== types.widgetType.alarm.value + && widgetType !== types.widgetType.static.value + && isDataEnabled"> {{ 'widget-config.datasources' | translate }} @@ -112,6 +140,24 @@ + + + + {{ 'widget-config.alarm-source' | translate }} + + + + + +
    diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index 08885b841e..8d91b2d750 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -21,7 +21,7 @@ import Subscription from '../api/subscription'; /*@ngInject*/ export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, - datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, + datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow, dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { var vm = this; @@ -44,13 +44,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q var widgetContext = { inited: false, - $scope: $scope, $container: null, $containerParent: null, width: 0, height: 0, + hideTitlePanel: false, isEdit: isEdit, - isMobile: false, + isMobile: isMobile, widgetConfig: widget.config, settings: widget.config.settings, units: widget.config.units || '', @@ -113,6 +113,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q timeService: timeService, deviceService: deviceService, datasourceService: datasourceService, + alarmService: alarmService, utils: utils, widgetUtils: widgetContext.utils, dashboardTimewindowApi: dashboardTimewindowApi, @@ -121,6 +122,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q aliasController: aliasController }; + widget.$ctx = function() { + return widgetContext; + } + var widgetTypeInstance; vm.useCustomDatasources = false; @@ -285,9 +290,18 @@ export default function WidgetController($scope, $timeout, $window, $element, $q var deferred = $q.defer(); if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { options = { - type: widget.type, - datasources: angular.copy(widget.config.datasources) - }; + type: widget.type + } + if (widget.type == types.widgetType.alarm.value) { + options.alarmSource = angular.copy(widget.config.alarmSource); + options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ? + widget.config.alarmSearchStatus : types.alarmSearchStatus.any; + options.alarmsPollingInterval = angular.isDefined(widget.config.alarmsPollingInterval) ? + widget.config.alarmsPollingInterval * 1000 : 5000; + } else { + options.datasources = angular.copy(widget.config.datasources) + } + defaultComponentsOptions(options); createSubscription(options).then( @@ -320,7 +334,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q $scope.executingRpcRequest = subscription.executingRpcRequest; }, onRpcSuccess: function(subscription) { - $scope.executingRpcRequest = subscription.executingRpcRequest; + $scope.executingRpcRequest = subscription.executingRpcRequest; $scope.rpcErrorText = subscription.rpcErrorText; $scope.rpcRejection = subscription.rpcRejection; }, @@ -436,7 +450,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q widgetContext.$container = $('#container', containerElement); widgetContext.$containerParent = $(containerElement); - $compile($element.contents())($scope); + if (widgetSizeDetected) { + widgetContext.$container.css('height', widgetContext.height + 'px'); + widgetContext.$container.css('width', widgetContext.width + 'px'); + } + + widgetContext.$scope = $scope.$new(); + + $compile($element.contents())(widgetContext.$scope); addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef } @@ -444,6 +465,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q function destroyWidgetElement() { removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef $element.html(''); + if (widgetContext.$scope) { + widgetContext.$scope.$destroy(); + } widgetContext.$container = null; widgetContext.$containerParent = null; } @@ -594,7 +618,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q function gridsterItemInitialized(item) { if (item && item.gridster) { - widgetContext.isMobile = item.gridster.isMobile; gridsterItemInited = true; onInit(); // gridsterItemElement = $(item.$element); diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html index a6521f066d..da0a2e950b 100644 --- a/ui/src/app/customer/customers.tpl.html +++ b/ui/src/app/customer/customers.tpl.html @@ -31,7 +31,7 @@ on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)" on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> - + - + - + - + - + diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js index 5372b6217e..fd2ada7b4a 100644 --- a/ui/src/app/dashboard/add-widget.controller.js +++ b/ui/src/app/dashboard/add-widget.controller.js @@ -76,6 +76,10 @@ export default function AddWidgetController($scope, widgetService, entityService link = 'widgetsConfigRpc'; break; } + case types.widgetType.alarm.value: { + link = 'widgetsConfigAlarm'; + break; + } case types.widgetType.static.value: { link = 'widgetsConfigStatic'; break; diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js index ac987093e6..c5743c9e78 100644 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ b/ui/src/app/dashboard/dashboard-settings.controller.js @@ -68,6 +68,7 @@ export default function DashboardSettingsController($scope, $mdDialog, statesCon vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)'; vm.gridSettings.columns = vm.gridSettings.columns || 24; vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; + vm.gridSettings.autoFillHeight = angular.isDefined(vm.gridSettings.autoFillHeight) ? vm.gridSettings.autoFillHeight : false; vm.hMargin = vm.gridSettings.margins[0]; vm.vMargin = vm.gridSettings.margins[1]; vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html index f3ff704c60..427ca1933e 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -121,6 +121,9 @@
+ {{ 'dashboard.autofill-height' | translate }} +
@@ -205,7 +206,8 @@
0 || vm.alarmWidgetTypes.length > 0 || + vm.staticWidgetTypes.length > 0" flex class="tb-absolute-fill" md-border-bottom> @@ -238,6 +240,16 @@ on-widget-clicked="vm.addWidgetFromType(event, widget)"> + + + + widgets-bundle.empty diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index 65329149e5..d0c651a474 100644 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ b/ui/src/app/dashboard/edit-widget.directive.js @@ -131,6 +131,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid scope: { dashboard: '=', aliasController: '=', + widgetEditMode: '=', widget: '=', widgetLayout: '=', theForm: '=' diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html index a9ff0b6d4d..4b4382e1d2 100644 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ b/ui/src/app/dashboard/edit-widget.tpl.html @@ -22,7 +22,7 @@ widget-settings-schema="settingsSchema" datakey-settings-schema="dataKeySettingsSchema" alias-controller="aliasController" - functions-only="functionsOnly" + functions-only="widgetEditMode" fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" the-form="theForm"> diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html index 4a4b25ea30..3df1327fad 100644 --- a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html +++ b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html @@ -49,6 +49,7 @@ state-controller="vm.dashboardCtx.stateController" dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow" is-edit="vm.isEdit" + autofill-height="vm.layoutCtx.gridSettings.autoFillHeight && !vm.isEdit" is-mobile="vm.isMobile" is-mobile-disabled="vm.widgetEditMode" is-edit-action-enabled="vm.isEdit" diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js index 86eb9ec217..8af5a77fe8 100644 --- a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js +++ b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js @@ -53,12 +53,6 @@ export default function DashboardStateDialogController($scope, $mdDialog, $filte if (!vm.stateIdTouched && vm.isAdd) { vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_"); } - var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true); - if (result && result.length && result[0].id !== vm.prevStateId) { - $scope.theForm.name.$setValidity('stateExists', false); - } else { - $scope.theForm.name.$setValidity('stateExists', true); - } } function checkStateId() { diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html index fa45d7af4a..385de94375 100644 --- a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html +++ b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html @@ -37,18 +37,15 @@
dashboard.state-name-required
-
dashboard.state-name-exists
- +
dashboard.state-id-required
dashboard.state-id-exists
-
dashboard.invalid-state-id-format
- + - + - + - + - + diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js index 22f9d75dbb..faed304a4e 100644 --- a/ui/src/app/help/help-links.constant.js +++ b/ui/src/app/help/help-links.constant.js @@ -87,6 +87,7 @@ export default angular.module('thingsboard.help', []) widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries", widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest", widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", + widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm", widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static", }, getPluginLink: function(plugin) { diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index a54eba4993..b21374600e 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', []) "type": "Type", "severity": "Severity", "originator": "Originator", + "originator-type": "Originator type", "details": "Details", "status": "Status", "alarm-details": "Alarm details", @@ -144,7 +145,17 @@ export default angular.module('thingsboard.locale', []) "severity-warning": "Warning", "severity-indeterminate": "Indeterminate", "acknowledge": "Acknowledge", - "clear": "Clear" + "clear": "Clear", + "search": "Search alarms", + "selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected", + "no-data": "No data to display", + "polling-interval": "Alarms polling interval (sec)", + "polling-interval-required": "Alarms polling interval is required.", + "min-polling-interval-message": "At least 1 sec polling interval is allowed.", + "aknowledge-alarms-title": "Acknowledge { count, select, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, select, 1 {1 alarm} other {# alarms} }?", + "clear-alarms-title": "Clear { count, select, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "Are you sure you want to clear { count, select, 1 {1 alarm} other {# alarms} }?" }, "alias": { "add": "Add alias", @@ -420,6 +431,7 @@ export default angular.module('thingsboard.locale', []) "vertical-margin-required": "Vertical margin value is required.", "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", + "autofill-height": "Auto fill layout height", "display-title": "Display dashboard title", "toolbar-always-open": "Keep toolbar opened", "title-color": "Title color", @@ -461,11 +473,9 @@ export default angular.module('thingsboard.locale', []) "state": "Dashboard state", "state-name": "Name", "state-name-required": "Dashboard state name is required.", - "state-name-exists": "Dashboard state with the same name is already exists.", "state-id": "State Id", "state-id-required": "Dashboard state id is required.", "state-id-exists": "Dashboard state with the same id is already exists.", - "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.", "is-root-state": "Root state", "delete-state-title": "Delete dashboard state", "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?", @@ -486,8 +496,10 @@ export default angular.module('thingsboard.locale', []) "configuration": "Data key configuration", "timeseries": "Timeseries", "attributes": "Attributes", + "alarm": "Alarm fields", "timeseries-required": "Entity timeseries are required.", "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", + "alarm-fields-required": "Alarm fields are required.", "function-types": "Function types", "function-types-required": "Function types are required." }, @@ -637,6 +649,8 @@ export default angular.module('thingsboard.locale', []) "no-aliases-found": "No aliases found.", "no-alias-matching": "'{{alias}}' not found.", "create-new-alias": "Create a new one!", + "key": "Key", + "key-name": "Key name", "no-keys-found": "No keys found.", "no-key-matching": "'{{key}}' not found.", "create-new-key": "Create a new one!", @@ -1046,6 +1060,7 @@ export default angular.module('thingsboard.locale', []) "timeseries": "Time series", "latest-values": "Latest values", "rpc": "Control widget", + "alarm": "Alarm widget", "static": "Static widget", "select-widget-type": "Select widget type", "missing-widget-title-error": "Widget title must be specified!", @@ -1133,7 +1148,8 @@ export default angular.module('thingsboard.locale', []) "datasource-parameters": "Parameters", "remove-datasource": "Remove datasource", "add-datasource": "Add datasource", - "target-device": "Target device" + "target-device": "Target device", + "alarm-source": "Alarm source" }, "widget-type": { "import": "Import widget type", @@ -1150,6 +1166,8 @@ export default angular.module('thingsboard.locale', []) "zh_CN": "Chinese", "ru_RU": "Russian", "es_ES": "Spanish" + }, + "custom": { } } } diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js index d227041532..7410062a57 100644 --- a/ui/src/app/locale/translate-handler.js +++ b/ui/src/app/locale/translate-handler.js @@ -18,7 +18,7 @@ export default function ThingsboardMissingTranslateHandler($log, types) { return function (translationId) { - if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) { + if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) { $log.warn('Translation for ' + translationId + ' doesn\'t exist'); } }; diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html index 73b0adc39e..b3563e9945 100644 --- a/ui/src/app/plugin/plugins.tpl.html +++ b/ui/src/app/plugin/plugins.tpl.html @@ -31,7 +31,7 @@ on-export-plugin="vm.exportPlugin(event, vm.grid.detailsConfig.currentItem)" on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> - + - + - + - + - + diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html index 336fe63ce9..064728baa9 100644 --- a/ui/src/app/rule/rules.tpl.html +++ b/ui/src/app/rule/rules.tpl.html @@ -31,7 +31,7 @@ on-export-rule="vm.exportRule(event, vm.grid.detailsConfig.currentItem)" on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> - + - + - + - + - + diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html index e407291757..94a827f45f 100644 --- a/ui/src/app/tenant/tenants.tpl.html +++ b/ui/src/app/tenant/tenants.tpl.html @@ -29,7 +29,7 @@ on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)" on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> - + - + - + - + - + diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js new file mode 100644 index 0000000000..5a65af20b0 --- /dev/null +++ b/ui/src/app/widget/lib/alarms-table-widget.js @@ -0,0 +1,597 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './alarms-table-widget.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html'; +import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import tinycolor from 'tinycolor2'; +import cssjs from '../../../vendor/css.js/css'; + +export default angular.module('thingsboard.widgets.alarmsTableWidget', []) + .directive('tbAlarmsTableWidget', AlarmsTableWidget) + .name; + +/*@ngInject*/ +function AlarmsTableWidget() { + return { + restrict: "E", + scope: true, + bindToController: { + tableId: '=', + ctx: '=' + }, + controller: AlarmsTableWidgetController, + controllerAs: 'vm', + templateUrl: alarmsTableWidgetTemplate + }; +} + +/*@ngInject*/ +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, alarmService, utils, types) { + var vm = this; + + vm.stylesInfo = {}; + vm.contentsInfo = {}; + vm.columnWidth = {}; + + vm.showData = true; + vm.hasData = false; + + vm.alarms = []; + vm.alarmsCount = 0; + vm.selectedAlarms = [] + + vm.alarmSource = null; + vm.allAlarms = null; + + vm.currentAlarm = null; + + vm.enableSelection = true; + vm.displayDetails = true; + vm.allowAcknowledgment = true; + vm.allowClear = true; + vm.displayPagination = true; + vm.defaultPageSize = 10; + vm.defaultSortOrder = '-'+types.alarmFields.createdTime.value; + + vm.query = { + order: vm.defaultSortOrder, + limit: vm.defaultPageSize, + page: 1, + search: null + }; + + vm.searchAction = { + name: 'action.search', + show: true, + onAction: function() { + vm.enterFilterMode(); + }, + icon: 'search' + }; + + vm.enterFilterMode = enterFilterMode; + vm.exitFilterMode = exitFilterMode; + vm.onReorder = onReorder; + vm.onPaginate = onPaginate; + vm.onRowClick = onRowClick; + vm.isCurrent = isCurrent; + vm.openAlarmDetails = openAlarmDetails; + vm.ackAlarms = ackAlarms; + vm.clearAlarms = clearAlarms; + + vm.cellStyle = cellStyle; + vm.cellContent = cellContent; + + $scope.$watch('vm.ctx', function() { + if (vm.ctx) { + vm.settings = vm.ctx.settings; + vm.widgetConfig = vm.ctx.widgetConfig; + vm.subscription = vm.ctx.defaultSubscription; + vm.alarmSource = vm.subscription.alarmSource; + initializeConfig(); + updateAlarmSource(); + } + }); + + $scope.$watch("vm.query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { + updateAlarms(); + } + }); + + $scope.$on('alarms-table-data-updated', function(event, tableId) { + if (vm.tableId == tableId) { + if (vm.subscription) { + vm.allAlarms = vm.subscription.alarms; + updateAlarms(true); + $scope.$digest(); + } + } + }); + + $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) { + vm.isGtXs = isGtXs; + }); + + $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) { + vm.isGtMd = isGtMd; + if (vm.isGtMd) { + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; + } else { + vm.limitOptions = null; + } + }); + + $scope.$watch('vm.selectedAlarms.length', function (newLength) { + var selectionMode = newLength ? true : false; + if (vm.ctx) { + if (selectionMode) { + vm.ctx.hideTitlePanel = true; + } else if (vm.query.search == null) { + vm.ctx.hideTitlePanel = false; + } + } + }); + + function initializeConfig() { + + vm.ctx.widgetActions = [ vm.searchAction ]; + + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { + var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle; + var translation = $translate.instant(translationId); + if (translation != translationId) { + vm.alarmsTitle = translation + ''; + } else { + vm.alarmsTitle = vm.settings.alarmsTitle; + } + } else { + vm.alarmsTitle = $translate.instant('alarm.alarms'); + } + + vm.ctx.widgetTitle = vm.alarmsTitle; + + vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; + vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; + vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; + vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; + vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true; + if (!vm.allowAcknowledgment && !vm.allowClear) { + vm.enableSelection = false; + } + + vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; + + var pageSize = vm.settings.defaultPageSize; + if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) { + vm.defaultPageSize = pageSize; + } + + if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) { + vm.defaultSortOrder = vm.settings.defaultSortOrder; + } + + vm.query.order = vm.defaultSortOrder; + vm.query.limit = vm.defaultPageSize; + if (vm.isGtMd) { + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; + } else { + vm.limitOptions = null; + } + + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; + var defaultColor = tinycolor(origColor); + var mdDark = defaultColor.setAlpha(0.87).toRgbString(); + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); + //var mdDarkIcon = mdDarkSecondary; + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); + + var cssString = 'table.md-table th.md-column {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+ + 'border-color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ + 'color: ' + mdDark + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell {\n'+ + 'color: ' + mdDark + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ + 'border-color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell.md-placeholder {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + '.md-table-pagination .buttons md-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}'; + + var cssParser = new cssjs(); + cssParser.testMode = false; + var namespace = 'ts-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $element.addClass(namespace); + + function hashCode(str) { + var hash = 0; + var i, char; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return hash; + } + } + + function enterFilterMode () { + vm.query.search = ''; + vm.ctx.hideTitlePanel = true; + } + + function exitFilterMode () { + vm.query.search = null; + updateAlarms(); + vm.ctx.hideTitlePanel = false; + } + + function onReorder () { + updateAlarms(); + } + + function onPaginate () { + updateAlarms(); + } + + function onRowClick($event, alarm) { + if (vm.currentAlarm != alarm) { + vm.currentAlarm = alarm; + } + } + + function isCurrent(alarm) { + return (vm.currentAlarm && alarm && vm.currentAlarm.id && alarm.id) && + (vm.currentAlarm.id.id === alarm.id.id); + } + + function openAlarmDetails($event, alarm) { + if (alarm && alarm.id) { + var onShowingCallback = { + onShowing: function(){} + } + $mdDialog.show({ + controller: 'AlarmDetailsDialogController', + controllerAs: 'vm', + templateUrl: alarmDetailsDialogTemplate, + locals: { + alarmId: alarm.id.id, + allowAcknowledgment: vm.allowAcknowledgment, + allowClear: vm.allowClear, + showingCallback: onShowingCallback + }, + parent: angular.element($document[0].body), + targetEvent: $event, + fullscreen: true, + skipHide: true, + onShowing: function(scope, element) { + onShowingCallback.onShowing(scope, element); + } + }).then(function (alarm) { + if (alarm) { + vm.subscription.update(); + } + }); + + } + } + + function ackAlarms($event) { + if ($event) { + $event.stopPropagation(); + } + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) { + var title = $translate.instant('alarm.aknowledge-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); + var content = $translate.instant('alarm.aknowledge-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(title) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + var tasks = []; + for (var i=0;i 0) { + var title = $translate.instant('alarm.clear-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); + var content = $translate.instant('alarm.clear-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(title) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + var tasks = []; + for (var i=0;i ( + path.split('.').reduce((acc, part) => acc && acc[part], obj) + ); + + function getAlarmValue(alarm, key) { + var alarmField = types.alarmFields[key.name]; + if (alarmField) { + return getDescendantProp(alarm, alarmField.value); + } else { + return getDescendantProp(alarm, key.name); + } + } + + function updateAlarmSource() { + + vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle); + + vm.stylesInfo = {}; + vm.contentsInfo = {}; + vm.columnWidth = {}; + + for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { + var dataKey = vm.alarmSource.dataKeys[d]; + + var translationId = types.translate.customTranslationsPrefix + dataKey.label; + var translation = $translate.instant(translationId); + if (translation != translationId) { + dataKey.title = translation + ''; + } else { + dataKey.title = dataKey.label; + } + + var keySettings = dataKey.settings; + + var cellStyleFunction = null; + var useCellStyleFunction = false; + + if (keySettings.useCellStyleFunction === true) { + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { + try { + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); + useCellStyleFunction = true; + } catch (e) { + cellStyleFunction = null; + useCellStyleFunction = false; + } + } + } + + vm.stylesInfo[dataKey.label] = { + useCellStyleFunction: useCellStyleFunction, + cellStyleFunction: cellStyleFunction + }; + + var cellContentFunction = null; + var useCellContentFunction = false; + + if (keySettings.useCellContentFunction === true) { + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { + try { + cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction); + useCellContentFunction = true; + } catch (e) { + cellContentFunction = null; + useCellContentFunction = false; + } + } + } + + vm.contentsInfo[dataKey.label] = { + useCellContentFunction: useCellContentFunction, + cellContentFunction: cellContentFunction + }; + + var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; + vm.columnWidth[dataKey.label] = columnWidth; + } + } + +} \ No newline at end of file diff --git a/ui/src/app/widget/lib/alarms-table-widget.scss b/ui/src/app/widget/lib/alarms-table-widget.scss new file mode 100644 index 0000000000..600be46389 --- /dev/null +++ b/ui/src/app/widget/lib/alarms-table-widget.scss @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-has-timewindow { + .tb-alarms-table { + md-toolbar { + min-height: 60px; + max-height: 60px; + &.md-table-toolbar { + .md-toolbar-tools { + max-height: 60px; + } + } + } + } +} + +.tb-alarms-table { + + md-toolbar { + min-height: 39px; + max-height: 39px; + &.md-table-toolbar { + .md-toolbar-tools { + max-height: 39px; + } + } + } + + &.tb-data-table { + table.md-table, table.md-table.md-row-select { + tbody { + tr { + td { + &.tb-action-cell { + min-width: 36px; + max-width: 36px; + width: 36px; + } + } + } + } + } + } +} diff --git a/ui/src/app/widget/lib/alarms-table-widget.tpl.html b/ui/src/app/widget/lib/alarms-table-widget.tpl.html new file mode 100644 index 0000000000..942b2afb1d --- /dev/null +++ b/ui/src/app/widget/lib/alarms-table-widget.tpl.html @@ -0,0 +1,103 @@ + +
+
+ +
+ + search + + {{'alarm.search' | translate}} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ +
+ + + + done + + {{ 'alarm.acknowledge' | translate }} + + + + clear + + {{ 'alarm.clear' | translate }} + + +
+
+ + + + + + + + + + + + + + +
{{ key.title }} 
+ + + more_horiz + + {{ 'alarm.details' | translate }} + + +
+ + alarm.no-alarms-prompt +
+ + +
+ alarm.no-data +
diff --git a/ui/src/app/widget/select-widget-type.tpl.html b/ui/src/app/widget/select-widget-type.tpl.html index e035273793..b7ebcb15f6 100644 --- a/ui/src/app/widget/select-widget-type.tpl.html +++ b/ui/src/app/widget/select-widget-type.tpl.html @@ -53,6 +53,13 @@ {{vm.types.widgetType.rpc.name}} + + error + + {{vm.types.widgetType.alarm.name}} +