From 531f42450f94c1e70d34e4c30fca51248fdbc6df Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 14 Jun 2017 13:43:18 +0300 Subject: [PATCH] TB-63: Alarms widget initial implementation. --- ui/src/app/api/alarm.service.js | 94 ++++- ui/src/app/api/alias-controller.js | 58 +-- ui/src/app/api/subscription.js | 214 ++++++++-- ui/src/app/api/widget.service.js | 3 +- ui/src/app/common/types.constant.js | 73 +++- ui/src/app/common/utils.service.js | 54 ++- ui/src/app/components/dashboard.directive.js | 7 +- ui/src/app/components/dashboard.tpl.html | 2 +- .../components/datasource-entity.directive.js | 73 +++- .../app/components/datasource-entity.tpl.html | 52 ++- .../components/datasource-func.directive.js | 53 ++- .../app/components/datasource-func.tpl.html | 59 ++- ui/src/app/components/datasource.directive.js | 13 +- ui/src/app/components/datasource.tpl.html | 1 + ui/src/app/components/timewindow.directive.js | 2 +- .../app/components/widget-config.directive.js | 80 +++- ui/src/app/components/widget-config.tpl.html | 28 +- ui/src/app/components/widget.controller.js | 29 +- ui/src/app/dashboard/add-widget.controller.js | 4 + ui/src/app/dashboard/dashboard.controller.js | 24 +- ui/src/app/dashboard/dashboard.tpl.html | 19 +- ui/src/app/dashboard/edit-widget.directive.js | 1 + ui/src/app/dashboard/edit-widget.tpl.html | 2 +- ui/src/app/help/help-links.constant.js | 1 + ui/src/app/locale/locale.constant.js | 12 +- ui/src/app/widget/lib/alarms-table-widget.js | 390 ++++++++++++++++++ .../app/widget/lib/alarms-table-widget.scss | 34 ++ .../widget/lib/alarms-table-widget.tpl.html | 116 ++++++ ui/src/app/widget/select-widget-type.tpl.html | 7 + ui/src/app/widget/widget-editor.controller.js | 34 +- 30 files changed, 1370 insertions(+), 169 deletions(-) create mode 100644 ui/src/app/widget/lib/alarms-table-widget.js create mode 100644 ui/src/app/widget/lib/alarms-table-widget.scss create mode 100644 ui/src/app/widget/lib/alarms-table-widget.tpl.html diff --git a/ui/src/app/api/alarm.service.js b/ui/src/app/api/alarm.service.js index 34e6b59e20..c2ee07a8ef 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, @@ -27,7 +49,9 @@ function AlarmService($http, $q, $interval, $filter) { clearAlarm: clearAlarm, getAlarms: getAlarms, pollAlarms: pollAlarms, - cancelPollAlarms: cancelPollAlarms + cancelPollAlarms: cancelPollAlarms, + subscribeForAlarms: subscribeForAlarms, + unsubscribeFromAlarms: unsubscribeFromAlarms } return service; @@ -171,12 +195,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 +244,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 { + var pollingInterval = 5000; //TODO: + alarmSourceListener.alarmsQuery = { + entityType: alarmSource.entityType, + entityId: alarmSource.entityId, + alarmSearchStatus: null, //TODO: + 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..16e4cc34ac 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -64,6 +64,39 @@ 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.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 +165,44 @@ 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.unsubscribe(); + subscription.subscribe(); + } + }); + this.registrations.push(registration); + } else { + this.startWatchingTimewindow(); + } + } + initDataSubscription() { var deferred = this.ctx.$q.defer(); if (!this.ctx.aliasController) { @@ -393,6 +464,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); } @@ -516,6 +589,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; @@ -540,62 +622,104 @@ export default class Subscription { 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, + 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 +731,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/common/types.constant.js b/ui/src/app/common/types.constant.js index b281ad1043..9610360642 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", diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 8d2e565f4a..ebded08f63 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
{{widget.config.title}} - +
- + + + {{item}} + +
+
+ entity.no-keys-found +
+
+ entity.no-key-matching +
+
+
+
+ +
+
+
+
+
+
+ {{$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 - +
- + + + {{item}} + +
+
+ entity.no-keys-found +
+
+ entity.no-key-matching +
+
+
+
+ +
+
+
+
+
+
+ {{$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); @@ -253,7 +268,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 +303,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 +351,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 +391,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 - +
+ ng-show="widgetType !== types.widgetType.rpc.value + && widgetType !== types.widgetType.alarm.value + && widgetType !== types.widgetType.static.value + && isDataEnabled"> {{ 'widget-config.datasources' | translate }} @@ -112,6 +116,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..2f76396f41 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, stDiff, dashboardTimewindow, dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { var vm = this; @@ -44,7 +44,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q var widgetContext = { inited: false, - $scope: $scope, $container: null, $containerParent: null, width: 0, @@ -113,6 +112,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, @@ -285,9 +285,14 @@ 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); + } else { + options.datasources = angular.copy(widget.config.datasources) + } + defaultComponentsOptions(options); createSubscription(options).then( @@ -320,7 +325,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 +441,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 +456,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; } 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.controller.js b/ui/src/app/dashboard/dashboard.controller.js index 65eb833dd4..6b9b0b39c5 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -47,6 +47,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget vm.latestWidgetTypes = []; vm.timeseriesWidgetTypes = []; vm.rpcWidgetTypes = []; + vm.alarmWidgetTypes = []; vm.staticWidgetTypes = []; vm.widgetEditMode = $state.$current.data.widgetEditMode; vm.iframeMode = $rootScope.iframeMode; @@ -263,6 +264,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget vm.latestWidgetTypes = []; vm.timeseriesWidgetTypes = []; vm.rpcWidgetTypes = []; + vm.alarmWidgetTypes = []; vm.staticWidgetTypes = []; if (vm.widgetsBundle) { var bundleAlias = vm.widgetsBundle.alias; @@ -308,6 +310,8 @@ export default function DashboardController(types, utils, dashboardUtils, widget vm.latestWidgetTypes.push(widget); } else if (widgetTypeInfo.type === types.widgetType.rpc.value) { vm.rpcWidgetTypes.push(widget); + } else if (widgetTypeInfo.type === types.widgetType.alarm.value) { + vm.alarmWidgetTypes.push(widget); } else if (widgetTypeInfo.type === types.widgetType.static.value) { vm.staticWidgetTypes.push(widget); } @@ -358,21 +362,6 @@ export default function DashboardController(types, utils, dashboardUtils, widget vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils, types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases); - - /* entityService.processEntityAliases(vm.dashboard.configuration.entityAliases) - .then( - function(resolution) { - if (resolution.error && !isTenantAdmin()) { - vm.configurationError = true; - showAliasesResolutionError(resolution.error); - } else { - vm.dashboardConfiguration = vm.dashboard.configuration; - vm.dashboardCtx.dashboard = vm.dashboard; - vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo; - vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; - } - } - );*/ }, function fail() { vm.configurationError = true; }); @@ -744,6 +733,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget link = 'widgetsConfigRpc'; break; } + case types.widgetType.alarm.value: { + link = 'widgetsConfigAlarm'; + break; + } case types.widgetType.static.value: { link = 'widgetsConfigStatic'; break; @@ -851,6 +844,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget vm.timeseriesWidgetTypes = []; vm.latestWidgetTypes = []; vm.rpcWidgetTypes = []; + vm.alarmWidgetTypes = []; vm.staticWidgetTypes = []; } diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index 202268fa5e..96ce711273 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -52,7 +52,7 @@ @@ -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/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..85fe2b0026 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,10 @@ 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" }, "alias": { "add": "Add alias", @@ -486,8 +490,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." }, @@ -1046,6 +1052,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 +1140,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", 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..9a3edcf23d --- /dev/null +++ b/ui/src/app/widget/lib/alarms-table-widget.js @@ -0,0 +1,390 @@ +/* + * 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'; + +/* 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: '=', + config: '=', + subscription: '=' + }, + controller: AlarmsTableWidgetController, + controllerAs: 'vm', + templateUrl: alarmsTableWidgetTemplate + }; +} + +/*@ngInject*/ +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUtil, $translate, utils, types) { + var vm = this; + + vm.stylesInfo = {}; + vm.contentsInfo = {}; + + vm.showData = true; + vm.hasData = false; + + vm.alarms = []; + vm.alarmsCount = 0; + vm.selectedAlarms = [] + + vm.alarmSource = null; + vm.allAlarms = null; + + vm.currentAlarm = null; + + vm.query = { + order: '-'+types.alarmFields.createdTime.value, + limit: 10, + page: 1, + search: null + }; + + vm.alarmsTitle = $translate.instant('alarm.alarms'); + + vm.enterFilterMode = enterFilterMode; + vm.exitFilterMode = exitFilterMode; + vm.onReorder = onReorder; + vm.onPaginate = onPaginate; + + vm.cellStyle = cellStyle; + vm.cellContent = cellContent; + + $scope.$watch('vm.config', function() { + if (vm.config) { + vm.settings = vm.config.settings; + vm.widgetConfig = vm.config.widgetConfig; + initializeConfig(); + } + }); + + $scope.$watch("vm.query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { + updateAlarms(); + } + }); + + $scope.$watch('vm.subscription', function() { + if (vm.subscription) { + vm.alarmSource = vm.subscription.alarmSource; + updateAlarmSource(); + } + }); + + $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 = [5, 10, 15]; + } else { + vm.limitOptions = null; + } + }); + + function initializeConfig() { + + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { + vm.alarmsTitle = vm.settings.alarmsTitle; + } + //TODO: + + 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-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-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 = ''; + } + + function exitFilterMode () { + vm.query.search = null; + updateAlarms(); + } + + function onReorder () { + updateAlarms(); + } + + function onPaginate () { + updateAlarms(); + } + + function updateAlarms(preserveSelections) { + if (!preserveSelections) { + vm.selectedAlarms = []; + } + var result = $filter('orderBy')(vm.allAlarms, vm.query.order); + if (vm.query.search != null) { + result = $filter('filter')(result, {$: vm.query.search}); + } + vm.alarmsCount = result.length; + var startIndex = vm.query.limit * (vm.query.page - 1); + vm.alarms = result.slice(startIndex, startIndex + vm.query.limit); + if (preserveSelections) { + var newSelectedAlarms = []; + if (vm.selectedAlarms && vm.selectedAlarms.length) { + var i = vm.selectedAlarms.length; + while (i--) { + var selectedAlarm = vm.selectedAlarms[i]; + if (selectedAlarm.id) { + result = $filter('filter')(vm.alarms, {id: {id: selectedAlarm.id.id} }); + if (result && result.length) { + newSelectedAlarms.push(result[0]); + } + } + } + } + vm.selectedAlarms = newSelectedAlarms; + } + } + + function cellStyle(alarm, key) { + var style = {}; + if (alarm && key) { + var styleInfo = vm.stylesInfo[key.label]; + var value = getAlarmValue(alarm, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } else { + style = defaultStyle(key, value); + } + } + return style; + } + + function cellContent(alarm, key) { + var strContent = ''; + if (alarm && key) { + var contentInfo = vm.contentsInfo[key.label]; + var value = getAlarmValue(alarm, key); + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + if (angular.isDefined(value)) { + strContent = '' + value; + } + var content = strContent; + try { + content = contentInfo.cellContentFunction(value, alarm, $filter); + } catch (e) { + content = strContent; + } + } else { + content = defaultContent(key, value); + } + return content; + } else { + return strContent; + } + } + + function defaultContent(key, value) { + if (angular.isDefined(value)) { + var alarmField = types.alarmFields[key.name]; + if (alarmField) { + if (alarmField.time) { + return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss'); + } else if (alarmField.value == types.alarmFields.severity.value) { + return $translate.instant(types.alarmSeverity[value].name); + } else if (alarmField.value == types.alarmFields.status.value) { + return $translate.instant('alarm.display-status.'+value); + } else if (alarmField.value == types.alarmFields.originatorType.value) { + return $translate.instant(types.entityTypeTranslations[value].type); + } + else { + return value; + } + } else { + return ''; + } + } else { + return ''; + } + } + function defaultStyle(key, value) { + if (angular.isDefined(value)) { + var alarmField = types.alarmFields[key.name]; + if (alarmField) { + if (alarmField.value == types.alarmFields.severity.value) { + return { + fontWeight: 'bold', + color: types.alarmSeverity[value].color + }; + } else { + return {}; + } + } + } else { + return {}; + } + } + + const getDescendantProp = (obj, path) => ( + 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() { + + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { + vm.alarmsTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.settings.alarmsTitle); + } + + vm.stylesInfo = {}; + vm.contentsInfo = {}; + + for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { + var dataKey = vm.alarmSource.dataKeys[d]; + 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 + }; + } + } + +} \ 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..afe50f098b --- /dev/null +++ b/ui/src/app/widget/lib/alarms-table-widget.scss @@ -0,0 +1,34 @@ +/** + * 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-alarms-table { + margin-top: 15px; + &.tb-data-table { + table.md-table { + tbody { + tr { + td { + &.ag-action-cell { + min-width: 40px; + max-width: 40px; + width: 40px; + } + } + } + } + } + } +} 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..f1732b03b3 --- /dev/null +++ b/ui/src/app/widget/lib/alarms-table-widget.tpl.html @@ -0,0 +1,116 @@ + +
+
+ +
+ {{ vm.alarmsTitle }} + + + search + + {{ 'action.search' | translate }} + + +
+
+ +
+ + search + + {{'alarm.search' | translate}} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ +
+ + + + done + + {{ 'alarm.acknowledge' | translate }} + + + + clear + + {{ 'alarm.clear' | translate }} + + +
+
+ + + + + + + + + + + + + + +
{{ key.label }} 
+ + + 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}} +