From 531f42450f94c1e70d34e4c30fca51248fdbc6df Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 14 Jun 2017 13:43:18 +0300 Subject: [PATCH 1/5] 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}} + Date: Wed, 14 Jun 2017 20:23:13 +0300 Subject: [PATCH 2/5] TB-63: Alarms widget implementation --- .../alarm/alarm-details-dialog.controller.js | 5 +- .../app/alarm/alarm-details-dialog.tpl.html | 8 +- ui/src/app/alarm/alarm-row.directive.js | 7 +- ui/src/app/api/alarm.service.js | 6 +- ui/src/app/api/subscription.js | 28 ++- ui/src/app/common/types.constant.js | 3 +- .../components/datakey-config.directive.js | 46 ++++- ui/src/app/components/datakey-config.tpl.html | 10 +- ui/src/app/components/datasource-entity.scss | 2 +- .../app/components/datasource-entity.tpl.html | 5 +- ui/src/app/components/datasource-func.scss | 2 +- .../app/components/datasource-func.tpl.html | 9 +- .../app/components/widget-config.directive.js | 9 +- ui/src/app/components/widget-config.tpl.html | 24 +++ ui/src/app/components/widget.controller.js | 4 + .../states/entity-state-controller.js | 8 +- ui/src/app/locale/locale.constant.js | 11 +- ui/src/app/locale/translate-handler.js | 3 +- ui/src/app/widget/lib/alarms-table-widget.js | 195 +++++++++++++++++- .../app/widget/lib/alarms-table-widget.scss | 10 +- .../widget/lib/alarms-table-widget.tpl.html | 18 +- ui/src/scss/main.scss | 48 +++++ 22 files changed, 388 insertions(+), 73 deletions(-) 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 c2ee07a8ef..db4ceadde1 100644 --- a/ui/src/app/api/alarm.service.js +++ b/ui/src/app/api/alarm.service.js @@ -252,12 +252,12 @@ function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) { $timeout(function() { alarmSourceListener.alarmsUpdated([simulatedAlarm], false); }); - } else { - var pollingInterval = 5000; //TODO: + } else if (alarmSource.entityType && alarmSource.entityId) { + var pollingInterval = alarmSourceListener.alarmsPollingInterval; alarmSourceListener.alarmsQuery = { entityType: alarmSource.entityType, entityId: alarmSource.entityId, - alarmSearchStatus: null, //TODO: + alarmSearchStatus: alarmSourceListener.alarmSearchStatus, alarmStatus: null } var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'}); diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index 16e4cc34ac..030b8521ee 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -70,6 +70,12 @@ export default class Subscription { 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 = []; @@ -193,8 +199,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); @@ -281,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); @@ -298,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); @@ -502,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(); } } @@ -519,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(); } } @@ -618,6 +619,11 @@ 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; @@ -688,6 +694,8 @@ export default class Subscription { this.alarmSourceListener = { subscriptionTimewindow: this.subscriptionTimewindow, alarmSource: this.alarmSource, + alarmSearchStatus: this.alarmSearchStatus, + alarmsPollingInterval: this.alarmsPollingInterval, alarmsUpdated: function(alarms, apply) { subscription.alarmsUpdated(alarms, apply); } diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 9610360642..6a4cbccc2f 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -394,7 +394,8 @@ export default angular.module('thingsboard.types', []) cards: "cards" }, translate: { - dashboardStatePrefix: "dashboardState.state." + dashboardStatePrefix: "dashboardState.state.", + keyLabelPrefix: "key.label." } } ).name; 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.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 d5cd782290..9876491086 100644 --- a/ui/src/app/components/datasource-entity.tpl.html +++ b/ui/src/app/components/datasource-entity.tpl.html @@ -133,7 +133,7 @@ ng-required="true" ng-model="alarmDataKeys" md-autocomplete-snap md-transform-chip="transformAlarmDataKeyChip($chip)" - md-require-match="true"> + md-require-match="false">
entity.no-key-matching + + entity.create-new-key +
diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss index 8739ed8fee..fdda0e47da 100644 --- a/ui/src/app/components/datasource-func.scss +++ b/ui/src/app/components/datasource-func.scss @@ -26,7 +26,7 @@ } } - .tb-func-datakey-autocomplete { + .tb-func-datakey-autocomplete, .tb-alarm-datakey-autocomplete { .tb-not-found { display: block; line-height: 1.5; diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html index 1ce1108aa5..7fa4aac008 100644 --- a/ui/src/app/components/datasource-func.tpl.html +++ b/ui/src/app/components/datasource-func.tpl.html @@ -48,9 +48,9 @@ device.no-keys-found
- device.no-key-matching + entity.no-key-matching - device.create-new-key + entity.create-new-key
@@ -81,7 +81,7 @@ ng-required="true" ng-model="alarmDataKeys" md-autocomplete-snap md-transform-chip="transformAlarmDataKeyChip($chip)" - md-require-match="true"> + md-require-match="false">
entity.no-key-matching + + entity.create-new-key +
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index e0b4800582..3849cd3c11 100644 --- a/ui/src/app/components/widget-config.directive.js +++ b/ui/src/app/components/widget-config.directive.js @@ -144,6 +144,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout scope.targetDeviceAlias.value = null; } } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { + scope.alarmSearchStatus = angular.isDefined(config.alarmSearchStatus) ? + config.alarmSearchStatus : types.alarmSearchStatus.any; + scope.alarmsPollingInterval = angular.isDefined(config.alarmsPollingInterval) ? + config.alarmsPollingInterval : 5; if (config.alarmSource) { scope.alarmSource.value = config.alarmSource; } else { @@ -205,7 +209,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout }; 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) { @@ -225,6 +230,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout 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) { diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html index 386f33a62f..4bfe55c0ef 100644 --- a/ui/src/app/components/widget-config.tpl.html +++ b/ui/src/app/components/widget-config.tpl.html @@ -31,6 +31,30 @@ flex ng-model="timewindow"> +
+ + + + + {{ ('alarm.search-status.' + searchStatus) | translate }} + + + + + + +
+
alarm.polling-interval-required
+
alarm.min-polling-interval-message
+
+
+
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); @@ -207,6 +248,115 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUti 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 {{ vm.alarmsTitle }} - + search {{ 'action.search' | translate }} @@ -57,13 +57,13 @@ translate-values="{count: vm.selectedAlarms.length}" translate-interpolation="messageformat"> - + done {{ 'alarm.acknowledge' | translate }} - + clear {{ 'alarm.clear' | translate }} @@ -72,22 +72,22 @@ - +
- - + + - -
{{ key.label }} {{ key.title }} 
+ more_horiz @@ -104,7 +104,7 @@ layout-align="center center" class="no-data-found" translate>alarm.no-alarms-prompt - diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index ab720c5e5a..5b5f92947f 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -281,7 +281,55 @@ pre.tb-highlight { display: flex; } table.md-table { + &.md-row-select td.md-cell, + &.md-row-select th.md-column { + &:first-child { + width: 20px; + padding: 0 0 0 12px; + } + + &:nth-child(2) { + padding: 0 12px; + } + + &:nth-child(n+3):nth-last-child(n+2) { + padding: 0 28px 0 0; + } + } + + &:not(.md-row-select) td.md-cell, + &:not(.md-row-select) th.md-column { + &:first-child { + padding: 0 12px; + } + + &:nth-child(n+2):nth-last-child(n+2) { + padding: 0 28px 0 0; + } + } + + td.md-cell, + th.md-column { + + &:last-child { + padding: 0 12px 0 0; + } + + } + } + + table.md-table, table.md-table.md-row-select { tbody { + &.md-body { + tr { + &.md-row:not([disabled]) { + outline: none; + &.tb-current, &.tb-current:hover{ + background-color: #dddddd !important; + } + } + } + } tr { td { &.tb-action-cell { From 8a37cf0d0de1a762805844f48e7c2436240a88ee Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 15 Jun 2017 13:45:40 +0300 Subject: [PATCH 3/5] TB-63: Improve alarm widget layout. --- ui/src/app/common/types.constant.js | 2 +- ui/src/app/components/dashboard.directive.js | 39 +++++++++++ ui/src/app/components/dashboard.tpl.html | 19 +++-- ui/src/app/components/widget.controller.js | 5 ++ ui/src/app/locale/locale.constant.js | 5 ++ ui/src/app/locale/translate-handler.js | 2 +- ui/src/app/widget/lib/alarms-table-widget.js | 70 +++++++++++++------ .../app/widget/lib/alarms-table-widget.scss | 26 ++++++- .../widget/lib/alarms-table-widget.tpl.html | 13 ---- 9 files changed, 139 insertions(+), 42 deletions(-) diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 6a4cbccc2f..ad09c820ed 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -395,7 +395,7 @@ export default angular.module('thingsboard.types', []) }, translate: { dashboardStatePrefix: "dashboardState.state.", - keyLabelPrefix: "key.label." + customTranslationsPrefix: "custom." } } ).name; diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index c6b4349858..f21df59927 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -177,7 +177,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ vm.widgetBackgroundColor = widgetBackgroundColor; vm.widgetPadding = widgetPadding; vm.showWidgetTitle = showWidgetTitle; + vm.showWidgetTitlePanel = showWidgetTitlePanel; vm.widgetTitleStyle = widgetTitleStyle; + vm.widgetTitle = widgetTitle; + vm.widgetActions = widgetActions; vm.dropWidgetShadow = dropWidgetShadow; vm.enableWidgetFullscreen = enableWidgetFullscreen; vm.hasTimewindow = hasTimewindow; @@ -747,6 +750,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } } + function showWidgetTitlePanel(widget) { + var ctx = widgetContext(widget); + if (ctx && ctx.hideTitlePanel) { + return false; + } else { + return showWidgetTitle(widget) || hasTimewindow(widget); + } + } + function widgetTitleStyle(widget) { if (angular.isDefined(widget.config.titleStyle)) { return widget.config.titleStyle; @@ -755,6 +767,33 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } } + function widgetTitle(widget) { + var ctx = widgetContext(widget); + if (ctx && ctx.widgetTitle + && ctx.widgetTitle.length) { + return ctx.widgetTitle; + } else { + return widget.config.title; + } + } + + function widgetActions(widget) { + var ctx = widgetContext(widget); + if (ctx && ctx.widgetActions && ctx.widgetActions.length) { + return ctx.widgetActions; + } else { + return []; + } + } + + function widgetContext(widget) { + var context; + if (widget.$ctx) { + context = widget.$ctx(); + } + return context; + } + function dropWidgetShadow(widget) { if (angular.isDefined(widget.config.dropShadow)) { return widget.config.dropShadow; diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html index 08bdd42963..da069b0ad3 100644 --- a/ui/src/app/components/dashboard.tpl.html +++ b/ui/src/app/components/dashboard.tpl.html @@ -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 }} + + +
- -
- {{ vm.alarmsTitle }} - - - search - - {{ 'action.search' | translate }} - - -
-
From 69a0b74c6b066a37f480b27db1e9b48d9d264c94 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 15 Jun 2017 18:29:28 +0300 Subject: [PATCH 4/5] TB-63: Improve alarms widget. Introduce auto-fill height setting for dashboard layout. --- ui/src/app/asset/assets.tpl.html | 10 +-- ui/src/app/common/types.constant.js | 1 - ui/src/app/common/utils.service.js | 17 +++- ui/src/app/components/dashboard.directive.js | 89 +++++++++++++++++-- ui/src/app/components/dashboard.tpl.html | 5 +- ui/src/app/components/grid.directive.js | 8 +- ui/src/app/components/widget.controller.js | 5 +- ui/src/app/customer/customers.tpl.html | 10 +-- .../dashboard-settings.controller.js | 1 + .../app/dashboard/dashboard-settings.tpl.html | 3 + .../layouts/dashboard-layout.tpl.html | 1 + .../dashboard-state-dialog.controller.js | 6 -- .../states/dashboard-state-dialog.tpl.html | 7 +- .../states/default-state-controller.js | 4 +- .../states/entity-state-controller.js | 19 ++-- ui/src/app/device/devices.tpl.html | 10 +-- ui/src/app/locale/locale.constant.js | 6 +- ui/src/app/locale/translate-handler.js | 3 +- ui/src/app/plugin/plugins.tpl.html | 10 +-- ui/src/app/rule/rules.tpl.html | 10 +-- ui/src/app/tenant/tenants.tpl.html | 10 +-- ui/src/app/widget/lib/alarms-table-widget.js | 6 ++ ui/src/scss/main.scss | 8 +- 23 files changed, 173 insertions(+), 76 deletions(-) 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 ad09c820ed..81ce0fa4f9 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -394,7 +394,6 @@ export default angular.module('thingsboard.types', []) cards: "cards" }, translate: { - dashboardStatePrefix: "dashboardState.state.", customTranslationsPrefix: "custom." } } diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index ebded08f63..a3433c255f 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -135,7 +135,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) { isLocalUrl: isLocalUrl, validateDatasources: validateDatasources, createKey: createKey, - createLabelFromDatasource: createLabelFromDatasource + createLabelFromDatasource: createLabelFromDatasource, + insertVariable: insertVariable } return service; @@ -407,4 +408,18 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) { return label; } + function insertVariable(pattern, name, value) { + var result = angular.copy(pattern); + var match = varsRegex.exec(pattern); + while (match !== null) { + var variable = match[0]; + var variableName = match[1]; + if (variableName === name) { + result = result.split(variable).join(value); + } + match = varsRegex.exec(pattern); + } + return result; + } + } diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index f21df59927..1c2c6543af 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -58,6 +58,7 @@ function Dashboard() { columns: '=', margins: '=', isEdit: '=', + autofillHeight: '=', isMobile: '=', isMobileDisabled: '=?', isEditActionEnabled: '=', @@ -102,6 +103,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; + vm.isMobileSize = false; + if (!('dashboardTimewindow' in vm)) { vm.dashboardTimewindow = timeService.defaultTimewindow(); } @@ -178,6 +181,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ vm.widgetPadding = widgetPadding; vm.showWidgetTitle = showWidgetTitle; vm.showWidgetTitlePanel = showWidgetTitlePanel; + vm.showWidgetActions = showWidgetActions; vm.widgetTitleStyle = widgetTitleStyle; vm.widgetTitle = widgetTitle; vm.widgetActions = widgetActions; @@ -271,12 +275,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ function updateMobileOpts() { var isMobileDisabled = vm.isMobileDisabled === true; - var isMobile = vm.isMobile === true && !isMobileDisabled; + var isMobile = vm.isMobile === true && !isMobileDisabled || vm.autofillHeight; var mobileBreakPoint = isMobileDisabled ? 0 : (isMobile ? 20000 : 960); + if (!isMobile && !isMobileDisabled) { isMobile = !$mdMedia('gt-sm'); } - var rowHeight = isMobile ? 70 : 'match'; + + var rowHeight = detectRowSize(isMobile); + if (vm.gridsterOpts.isMobile != isMobile) { vm.gridsterOpts.isMobile = isMobile; vm.gridsterOpts.mobileModeEnabled = isMobile; @@ -287,6 +294,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ if (vm.gridsterOpts.rowHeight != rowHeight) { vm.gridsterOpts.rowHeight = rowHeight; } + + vm.isMobileSize = checkIsMobileSize(); + } + + function checkIsMobileSize() { + var isMobileDisabled = vm.isMobileDisabled === true; + var isMobileSize = vm.isMobile === true && !isMobileDisabled; + if (!isMobileSize && !isMobileDisabled) { + isMobileSize = !$mdMedia('gt-sm'); + } + return isMobileSize; } $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() { @@ -297,6 +315,34 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ updateMobileOpts(); }); + $scope.$watch('vm.autofillHeight', function () { + if (vm.autofillHeight) { + //if (gridsterParent.height()) { + // updateMobileOpts(); + //} else { + if ($scope.parentHeighWatcher) { + $scope.parentHeighWatcher(); + } + if (gridsterParent.height()) { + updateMobileOpts(); + } + $scope.parentHeighWatcher = $scope.$watch(function() { return gridsterParent.height(); }, + function(newHeight) { + if (newHeight) { + updateMobileOpts(); + } + } + ); + } else { + if ($scope.parentHeighWatcher) { + $scope.parentHeighWatcher(); + $scope.parentHeighWatcher = null; + } + + updateMobileOpts(); + } + }); + $scope.$watch('vm.isMobileDisabled', function () { updateMobileOpts(); }); @@ -333,6 +379,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ $scope.$broadcast('toggleDashboardEditMode', vm.isEdit); }); + $scope.$watch('vm.isMobileSize', function (newVal, prevVal) { + if (!angular.equals(newVal, prevVal)) { + $scope.$broadcast('mobileModeChanged', vm.isMobileSize); + } + }); + $scope.$on('gridster-resized', function (event, sizes, theGridster) { if (checkIsLocalGridsterElement(theGridster)) { vm.gridster = theGridster; @@ -345,13 +397,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ $scope.$on('gridster-mobile-changed', function (event, theGridster) { if (checkIsLocalGridsterElement(theGridster)) { vm.gridster = theGridster; - var rowHeight = vm.gridster.isMobile ? 70 : 'match'; + var rowHeight = detectRowSize(vm.gridster.isMobile); if (vm.gridsterOpts.rowHeight != rowHeight) { vm.gridsterOpts.rowHeight = rowHeight; updateGridsterParams(); } - - $scope.$broadcast('mobileModeChanged', vm.gridster.isMobile); + vm.isMobileSize = checkIsMobileSize(); //TODO: widgets visibility /*$timeout(function () { @@ -360,6 +411,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } }); + function detectRowSize(isMobile) { + var rowHeight = isMobile ? 70 : 'match'; + if (vm.autofillHeight) { + var viewportHeight = gridsterParent.height(); + var totalRows = 0; + for (var i = 0; i < vm.widgets.length; i++) { + var w = vm.widgets[i]; + var sizeY = widgetSizeY(w); + totalRows += sizeY; + } + rowHeight = (viewportHeight - (vm.gridsterOpts.margins[1])) / totalRows; + } + return rowHeight; + } + function widgetOrder(widget) { var order; if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { @@ -650,7 +716,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } function widgetSizeY(widget) { - if (vm.gridsterOpts.isMobile) { + if (vm.gridsterOpts.isMobile && !vm.autofillHeight) { var mobileHeight; if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { mobileHeight = vm.widgetLayouts[widget.id].mobileHeight; @@ -673,7 +739,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } function setWidgetSizeY(widget, sizeY) { - if (!vm.gridsterOpts.isMobile) { + if (!vm.gridsterOpts.isMobile && !vm.autofillHeight) { if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { vm.widgetLayouts[widget.id].sizeY = sizeY; } else { @@ -759,6 +825,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } } + function showWidgetActions(widget) { + var ctx = widgetContext(widget); + if (ctx && ctx.hideTitlePanel) { + return false; + } else { + return true; + } + } + function widgetTitleStyle(widget) { if (angular.isDefined(widget.config.titleStyle)) { return widget.config.titleStyle; diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html index da069b0ad3..478e2e562a 100644 --- a/ui/src/app/components/dashboard.tpl.html +++ b/ui/src/app/components/dashboard.tpl.html @@ -23,7 +23,7 @@ -
+
  • @@ -50,7 +50,7 @@ {{vm.widgetTitle(widget)}}
-
+
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js index 296456a9c5..422501bb0f 100644 --- a/ui/src/app/components/grid.directive.js +++ b/ui/src/app/components/grid.directive.js @@ -124,7 +124,7 @@ function Grid() { } /*@ngInject*/ -function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache) { +function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache, $window) { var vm = this; @@ -155,6 +155,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra vm.refreshList = refreshList; vm.saveItem = saveItem; vm.toggleItemSelection = toggleItemSelection; + vm.triggerResize = triggerResize; $scope.$watch(function () { return $mdMedia('xs') || $mdMedia('sm'); @@ -600,6 +601,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra } } + function triggerResize() { + var w = angular.element($window); + w.triggerHandler('resize'); + } + function moveToTop() { moveToIndex(0, true); } diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index 63ad0ed89c..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, alarmService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, + datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow, dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { var vm = this; @@ -50,7 +50,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q height: 0, hideTitlePanel: false, isEdit: isEdit, - isMobile: false, + isMobile: isMobile, widgetConfig: widget.config, settings: widget.config.settings, units: widget.config.units || '', @@ -618,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/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 }} +
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/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index f35434c833..b21374600e 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -431,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", @@ -472,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}}'?", @@ -1169,9 +1168,6 @@ export default angular.module('thingsboard.locale', []) "es_ES": "Spanish" }, "custom": { - "alarms": { - "title": "Super ${entityName}" - } } } } diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js index 20af2584cc..7410062a57 100644 --- a/ui/src/app/locale/translate-handler.js +++ b/ui/src/app/locale/translate-handler.js @@ -18,8 +18,7 @@ export default function ThingsboardMissingTranslateHandler($log, types) { return function (translationId) { - if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix) && - !translationId.startsWith(types.translate.customTranslationsPrefix)) { + 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 index ebe6b0304f..5a65af20b0 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.js +++ b/ui/src/app/widget/lib/alarms-table-widget.js @@ -210,6 +210,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia 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'+ @@ -220,6 +223,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia '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'+ diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index 5b5f92947f..c47f177430 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -324,8 +324,14 @@ pre.tb-highlight { tr { &.md-row:not([disabled]) { outline: none; + &:hover { + background-color: rgba(221, 221, 221, 0.3) !important; + } + &.md-selected { + background-color: rgba(221, 221, 221, 0.5) !important; + } &.tb-current, &.tb-current:hover{ - background-color: #dddddd !important; + background-color: rgba(221, 221, 221, 0.65) !important; } } } From 87683218b556558fbb81e2faf0898de5e55fb879 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 15 Jun 2017 20:13:47 +0300 Subject: [PATCH 5/5] TB-63: Find highest alarm severity for specified entity. --- .../server/controller/AlarmController.java | 26 ++++++++++++ .../server/dao/alarm/AlarmService.java | 9 ++-- .../server/dao/alarm/BaseAlarmService.java | 42 +++++++++++++++++++ ui/src/app/api/alarm.service.js | 18 ++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) 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 58f7316a75..06a9b343db 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 @@ -27,6 +27,7 @@ import org.springframework.util.StringUtils; 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; @@ -46,6 +47,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; @@ -242,6 +244,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/api/alarm.service.js b/ui/src/app/api/alarm.service.js index db4ceadde1..3b7a735be2 100644 --- a/ui/src/app/api/alarm.service.js +++ b/ui/src/app/api/alarm.service.js @@ -48,6 +48,7 @@ function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) { ackAlarm: ackAlarm, clearAlarm: clearAlarm, getAlarms: getAlarms, + getHighestAlarmSeverity: getHighestAlarmSeverity, pollAlarms: pollAlarms, cancelPollAlarms: cancelPollAlarms, subscribeForAlarms: subscribeForAlarms, @@ -165,6 +166,23 @@ function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) { 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,