thingsboard-memoizeit
Changes
ui/src/app/api/alarm.service.js 112(+109 -3)
ui/src/app/api/alias-controller.js 58(+35 -23)
ui/src/app/api/subscription.js 238(+189 -49)
ui/src/app/api/widget.service.js 3(+2 -1)
ui/src/app/asset/assets.tpl.html 10(+5 -5)
ui/src/app/common/types.constant.js 75(+68 -7)
ui/src/app/common/utils.service.js 69(+67 -2)
ui/src/app/components/dashboard.directive.js 135(+127 -8)
ui/src/app/components/dashboard.tpl.html 24(+18 -6)
ui/src/app/components/widget.controller.js 41(+32 -9)
ui/src/app/components/widget-config.directive.js 89(+71 -18)
ui/src/app/components/widget-config.tpl.html 52(+49 -3)
ui/src/app/customer/customers.tpl.html 10(+5 -5)
ui/src/app/dashboard/dashboard.controller.js 24(+9 -15)
ui/src/app/dashboard/dashboard.tpl.html 19(+16 -3)
ui/src/app/device/devices.tpl.html 10(+5 -5)
ui/src/app/locale/locale.constant.js 26(+22 -4)
ui/src/app/plugin/plugins.tpl.html 10(+5 -5)
ui/src/app/rule/rules.tpl.html 10(+5 -5)
ui/src/app/tenant/tenants.tpl.html 10(+5 -5)
ui/src/app/widget/lib/alarms-table-widget.js 597(+597 -0)
ui/src/app/widget/lib/alarms-table-widget.tpl.html 103(+103 -0)
ui/src/scss/main.scss 54(+54 -0)
Details
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 19c75e7..baee889 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 3556d51..63fba03 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<TimePageData<AlarmInfo>> 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 94f8fdb..c3c1886 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.entity.AbstractEntityService;
@@ -45,6 +46,7 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -240,6 +242,46 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
});
}
+ @Override
+ public AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus,
+ AlarmStatus alarmStatus) {
+ TimePageLink nextPageLink = new TimePageLink(100);
+ boolean hasNext = true;
+ AlarmSeverity highestSeverity = null;
+ AlarmQuery query;
+ while (hasNext) {
+ query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false);
+ List<AlarmInfo> 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<AlarmInfo> sorted = new ArrayList(alarms);
+ sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity()));
+ AlarmSeverity severity = sorted.get(0).getSeverity();
+ if (severity == AlarmSeverity.CRITICAL) {
+ highestSeverity = severity;
+ break;
+ } else if (highestSeverity == null) {
+ highestSeverity = severity;
+ } else {
+ highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity;
+ }
+ }
+ }
+ return highestSeverity;
+ }
+
private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
log.debug("Deleting Alarm relation: {}", alarmRelation);
relationService.deleteRelation(alarmRelation).get();
diff --git a/ui/src/app/alarm/alarm-details-dialog.controller.js b/ui/src/app/alarm/alarm-details-dialog.controller.js
index 0cc05ef..6c82e20 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 c958201..48f991c 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 @@
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
- <md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeUnack ||
- vm.alarm.status==vm.types.alarmStatus.clearedUnack"
+ <md-button ng-if="vm.allowAcknowledgment && (vm.alarm.status==vm.types.alarmStatus.activeUnack ||
+ vm.alarm.status==vm.types.alarmStatus.clearedUnack)"
class="md-raised md-primary"
ng-disabled="loading"
ng-click="vm.acknowledge()"
style="margin-right:20px;">{{ 'alarm.acknowledge' |
translate }}
</md-button>
- <md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeAck ||
- vm.alarm.status==vm.types.alarmStatus.activeUnack"
+ <md-button ng-if="vm.allowClear && (vm.alarm.status==vm.types.alarmStatus.activeAck ||
+ vm.alarm.status==vm.types.alarmStatus.activeUnack)"
class="md-raised md-primary"
ng-disabled="loading"
ng-click="vm.clear()">{{ 'alarm.clear' |
diff --git a/ui/src/app/alarm/alarm-row.directive.js b/ui/src/app/alarm/alarm-row.directive.js
index 9cb9bed..09c04f9 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,
ui/src/app/api/alarm.service.js 112(+109 -3)
diff --git a/ui/src/app/api/alarm.service.js b/ui/src/app/api/alarm.service.js
index 34e6b59..3b7a735 100644
--- a/ui/src/app/api/alarm.service.js
+++ b/ui/src/app/api/alarm.service.js
@@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', [])
.name;
/*@ngInject*/
-function AlarmService($http, $q, $interval, $filter) {
+function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) {
+
+ var alarmSourceListeners = {};
+
+ var simulatedAlarm = {
+ createdTime: (new Date).getTime(),
+ startTs: (new Date).getTime(),
+ endTs: 0,
+ ackTs: 0,
+ clearTs: 0,
+ originatorName: 'Simulated',
+ originator: {
+ entityType: "DEVICE",
+ id: "1"
+ },
+ type: 'TEMPERATURE',
+ severity: "MAJOR",
+ status: types.alarmStatus.activeUnack,
+ details: {
+ message: "Temperature is high!"
+ }
+ };
+
var service = {
getAlarm: getAlarm,
getAlarmInfo: getAlarmInfo,
@@ -26,8 +48,11 @@ function AlarmService($http, $q, $interval, $filter) {
ackAlarm: ackAlarm,
clearAlarm: clearAlarm,
getAlarms: getAlarms,
+ getHighestAlarmSeverity: getHighestAlarmSeverity,
pollAlarms: pollAlarms,
- cancelPollAlarms: cancelPollAlarms
+ cancelPollAlarms: cancelPollAlarms,
+ subscribeForAlarms: subscribeForAlarms,
+ unsubscribeFromAlarms: unsubscribeFromAlarms
}
return service;
@@ -141,6 +166,23 @@ function AlarmService($http, $q, $interval, $filter) {
return deferred.promise;
}
+ function getHighestAlarmSeverity(entityType, entityId, alarmSearchStatus, alarmStatus, config) {
+ var deferred = $q.defer();
+ var url = '/api/alarm/highestSeverity/' + entityType + '/' + entityId;
+
+ if (alarmSearchStatus) {
+ url += '?searchStatus=' + alarmSearchStatus;
+ } else if (alarmStatus) {
+ url += '?status=' + alarmStatus;
+ }
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
function fetchAlarms(alarmsQuery, pageLink, deferred, alarmsList) {
getAlarms(alarmsQuery.entityType, alarmsQuery.entityId,
pageLink, alarmsQuery.alarmSearchStatus, alarmsQuery.alarmStatus,
@@ -171,12 +213,21 @@ function AlarmService($http, $q, $interval, $filter) {
pageLink = {
limit: alarmsQuery.limit
};
- } else {
+ } else if (alarmsQuery.interval) {
pageLink = {
limit: 100,
startTime: time - alarmsQuery.interval
};
+ } else if (alarmsQuery.startTime) {
+ pageLink = {
+ limit: 100,
+ startTime: alarmsQuery.startTime
+ }
+ if (alarmsQuery.endTime) {
+ pageLink.endTime = alarmsQuery.endTime;
+ }
}
+
fetchAlarms(alarmsQuery, pageLink, deferred);
return deferred.promise;
}
@@ -211,4 +262,59 @@ function AlarmService($http, $q, $interval, $filter) {
}
}
+ function subscribeForAlarms(alarmSourceListener) {
+ alarmSourceListener.id = utils.guid();
+ alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener;
+ var alarmSource = alarmSourceListener.alarmSource;
+ if (alarmSource.type == types.datasourceType.function) {
+ $timeout(function() {
+ alarmSourceListener.alarmsUpdated([simulatedAlarm], false);
+ });
+ } else if (alarmSource.entityType && alarmSource.entityId) {
+ var pollingInterval = alarmSourceListener.alarmsPollingInterval;
+ alarmSourceListener.alarmsQuery = {
+ entityType: alarmSource.entityType,
+ entityId: alarmSource.entityId,
+ alarmSearchStatus: alarmSourceListener.alarmSearchStatus,
+ alarmStatus: null
+ }
+ var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'});
+ if (originatorKeys && originatorKeys.length) {
+ alarmSourceListener.alarmsQuery.fetchOriginator = true;
+ }
+ var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow;
+ if (subscriptionTimewindow.realtimeWindowMs) {
+ alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs;
+ } else {
+ alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs;
+ alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs;
+ }
+ alarmSourceListener.alarmsQuery.onAlarms = function(alarms) {
+ if (subscriptionTimewindow.realtimeWindowMs) {
+ var now = Date.now();
+ if (alarmSourceListener.lastUpdateTs) {
+ var interval = now - alarmSourceListener.lastUpdateTs;
+ alarmSourceListener.alarmsQuery.startTime += interval;
+ } else {
+ alarmSourceListener.lastUpdateTs = now;
+ }
+ }
+ alarmSourceListener.alarmsUpdated(alarms, false);
+ }
+ onPollAlarms(alarmSourceListener.alarmsQuery);
+ alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval,
+ 0, false, alarmSourceListener.alarmsQuery);
+ }
+
+ }
+
+ function unsubscribeFromAlarms(alarmSourceListener) {
+ if (alarmSourceListener && alarmSourceListener.id) {
+ if (alarmSourceListener.pollPromise) {
+ $interval.cancel(alarmSourceListener.pollPromise);
+ alarmSourceListener.pollPromise = null;
+ }
+ delete alarmSourceListeners[alarmSourceListener.id];
+ }
+ }
}
ui/src/app/api/alias-controller.js 58(+35 -23)
diff --git a/ui/src/app/api/alias-controller.js b/ui/src/app/api/alias-controller.js
index 60e8a31..9acd5aa 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);
ui/src/app/api/subscription.js 238(+189 -49)
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
index 1d4eb30..030b852 100644
--- a/ui/src/app/api/subscription.js
+++ b/ui/src/app/api/subscription.js
@@ -64,6 +64,45 @@ export default class Subscription {
deferred.resolve(subscription);
}
);
+ } else if (this.type === this.ctx.types.widgetType.alarm.value) {
+ this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
+ this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
+ this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
+ this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
+ this.alarmSource = options.alarmSource;
+
+ this.alarmSearchStatus = angular.isDefined(options.alarmSearchStatus) ?
+ options.alarmSearchStatus : this.ctx.types.alarmSearchStatus.any;
+ this.alarmsPollingInterval = angular.isDefined(options.alarmsPollingInterval) ?
+ options.alarmsPollingInterval : 5000;
+
+ this.alarmSourceListener = null;
+ this.alarms = [];
+
+ this.originalTimewindow = null;
+ this.timeWindow = {
+ stDiff: this.ctx.stDiff
+ }
+ this.useDashboardTimewindow = options.useDashboardTimewindow;
+
+ if (this.useDashboardTimewindow) {
+ this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
+ } else {
+ this.timeWindowConfig = angular.copy(options.timeWindowConfig);
+ }
+
+ this.subscriptionTimewindow = null;
+
+ this.loadingData = false;
+ this.displayLegend = false;
+ this.initAlarmSubscription().then(
+ function success() {
+ deferred.resolve(subscription);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
@@ -132,6 +171,43 @@ export default class Subscription {
return deferred.promise;
}
+ initAlarmSubscription() {
+ var deferred = this.ctx.$q.defer();
+ if (!this.ctx.aliasController) {
+ this.configureAlarmsData();
+ deferred.resolve();
+ } else {
+ var subscription = this;
+ this.ctx.aliasController.resolveAlarmSource(this.alarmSource).then(
+ function success(alarmSource) {
+ subscription.alarmSource = alarmSource;
+ subscription.configureAlarmsData();
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ configureAlarmsData() {
+ var subscription = this;
+ var registration;
+ if (this.useDashboardTimewindow) {
+ registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
+ if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
+ subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
+ subscription.update();
+ }
+ });
+ this.registrations.push(registration);
+ } else {
+ this.startWatchingTimewindow();
+ }
+ }
+
initDataSubscription() {
var deferred = this.ctx.$q.defer();
if (!this.ctx.aliasController) {
@@ -210,8 +286,7 @@ export default class Subscription {
registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
- subscription.unsubscribe();
- subscription.subscribe();
+ subscription.update();
}
});
this.registrations.push(registration);
@@ -227,8 +302,7 @@ export default class Subscription {
return subscription.timeWindowConfig;
}, function (newTimewindow, prevTimewindow) {
if (!angular.equals(newTimewindow, prevTimewindow)) {
- subscription.unsubscribe();
- subscription.subscribe();
+ subscription.update();
}
}, true);
this.registrations.push(this.timeWindowWatchRegistration);
@@ -393,6 +467,8 @@ export default class Subscription {
onAliasesChanged(aliasIds) {
if (this.type === this.ctx.types.widgetType.rpc.value) {
return this.checkRpcTarget(aliasIds);
+ } else if (this.type === this.ctx.types.widgetType.alarm.value) {
+ return this.checkAlarmSource(aliasIds);
} else {
return this.checkSubscriptions(aliasIds);
}
@@ -429,8 +505,7 @@ export default class Subscription {
this.timeWindowConfig = angular.copy(this.originalTimewindow);
this.originalTimewindow = null;
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
- this.unsubscribe();
- this.subscribe();
+ this.update();
this.startWatchingTimewindow();
}
}
@@ -446,8 +521,7 @@ export default class Subscription {
}
this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs);
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
- this.unsubscribe();
- this.subscribe();
+ this.update();
this.startWatchingTimewindow();
}
}
@@ -516,6 +590,15 @@ export default class Subscription {
}
}
+ alarmsUpdated(alarms, apply) {
+ this.notifyDataLoaded();
+ this.alarms = alarms;
+ if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
+ this.updateTimewindow();
+ }
+ this.onDataUpdated(apply);
+ }
+
updateLegend(dataIndex, data, apply) {
var dataKey = this.legendData.keys[dataIndex].dataKey;
var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;
@@ -536,66 +619,115 @@ export default class Subscription {
this.callbacks.legendDataUpdated(this, apply !== false);
}
+ update() {
+ this.unsubscribe();
+ this.subscribe();
+ }
+
subscribe() {
if (this.type === this.ctx.types.widgetType.rpc.value) {
return;
}
+ if (this.type === this.ctx.types.widgetType.alarm.value) {
+ this.alarmsSubscribe();
+ } else {
+ this.notifyDataLoading();
+ if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
+ this.updateRealtimeSubscription();
+ if (this.subscriptionTimewindow.fixedWindow) {
+ this.onDataUpdated();
+ }
+ }
+ var index = 0;
+ for (var i = 0; i < this.datasources.length; i++) {
+ var datasource = this.datasources[i];
+ if (angular.isFunction(datasource))
+ continue;
+
+ var subscription = this;
+
+ var listener = {
+ subscriptionType: this.type,
+ subscriptionTimewindow: this.subscriptionTimewindow,
+ datasource: datasource,
+ entityType: datasource.entityType,
+ entityId: datasource.entityId,
+ dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
+ subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
+ },
+ updateRealtimeSubscription: function () {
+ this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
+ return this.subscriptionTimewindow;
+ },
+ setRealtimeSubscription: function (subscriptionTimewindow) {
+ subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
+ },
+ datasourceIndex: index
+ };
+
+ for (var a = 0; a < datasource.dataKeys.length; a++) {
+ this.data[index + a].data = [];
+ }
+
+ index += datasource.dataKeys.length;
+
+ this.datasourceListeners.push(listener);
+ this.ctx.datasourceService.subscribeToDatasource(listener);
+ if (datasource.unresolvedStateEntity) {
+ this.notifyDataLoaded();
+ this.onDataUpdated();
+ }
+
+ }
+ }
+ }
+
+ alarmsSubscribe() {
this.notifyDataLoading();
- if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
+ if (this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
- var index = 0;
- for (var i = 0; i < this.datasources.length; i++) {
- var datasource = this.datasources[i];
- if (angular.isFunction(datasource))
- continue;
-
- var subscription = this;
-
- var listener = {
- subscriptionType: this.type,
- subscriptionTimewindow: this.subscriptionTimewindow,
- datasource: datasource,
- entityType: datasource.entityType,
- entityId: datasource.entityId,
- dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
- subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
- },
- updateRealtimeSubscription: function () {
- this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
- return this.subscriptionTimewindow;
- },
- setRealtimeSubscription: function (subscriptionTimewindow) {
- subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
- },
- datasourceIndex: index
- };
-
- for (var a = 0; a < datasource.dataKeys.length; a++) {
- this.data[index + a].data = [];
+ var subscription = this;
+ this.alarmSourceListener = {
+ subscriptionTimewindow: this.subscriptionTimewindow,
+ alarmSource: this.alarmSource,
+ alarmSearchStatus: this.alarmSearchStatus,
+ alarmsPollingInterval: this.alarmsPollingInterval,
+ alarmsUpdated: function(alarms, apply) {
+ subscription.alarmsUpdated(alarms, apply);
}
+ }
+ this.alarms = [];
- index += datasource.dataKeys.length;
+ this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener);
- this.datasourceListeners.push(listener);
- this.ctx.datasourceService.subscribeToDatasource(listener);
- if (datasource.unresolvedStateEntity) {
- this.notifyDataLoaded();
- this.onDataUpdated();
- }
+ if (this.alarmSource.unresolvedStateEntity) {
+ this.notifyDataLoaded();
+ this.onDataUpdated();
}
}
unsubscribe() {
if (this.type !== this.ctx.types.widgetType.rpc.value) {
- for (var i = 0; i < this.datasourceListeners.length; i++) {
- var listener = this.datasourceListeners[i];
- this.ctx.datasourceService.unsubscribeFromDatasource(listener);
+ if (this.type == this.ctx.types.widgetType.alarm.value) {
+ this.alarmsUnsubscribe();
+ } else {
+ for (var i = 0; i < this.datasourceListeners.length; i++) {
+ var listener = this.datasourceListeners[i];
+ this.ctx.datasourceService.unsubscribeFromDatasource(listener);
+ }
+ this.datasourceListeners = [];
}
- this.datasourceListeners = [];
+ }
+ }
+
+ alarmsUnsubscribe() {
+ if (this.alarmSourceListener) {
+ this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener);
+ this.alarmSourceListener = null;
}
}
@@ -607,6 +739,14 @@ export default class Subscription {
}
}
+ checkAlarmSource(aliasIds) {
+ if (this.alarmSource && this.alarmSource.entityAliasId) {
+ return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1;
+ } else {
+ return false;
+ }
+ }
+
checkSubscriptions(aliasIds) {
var subscriptionsChanged = false;
for (var i = 0; i < this.datasourceListeners.length; i++) {
ui/src/app/api/widget.service.js 3(+2 -1)
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 21c38e2..1c5e5f4 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;
ui/src/app/asset/assets.tpl.html 10(+5 -5)
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
index 8370d3d..c4c0267 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)"></tb-asset>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.asset}}"
@@ -39,7 +39,7 @@
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.asset}}"
@@ -48,19 +48,19 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.asset"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'asset.events' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'asset.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.asset"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.asset}}">
ui/src/app/common/types.constant.js 75(+68 -7)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index b281ad1..81ce0fa 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', [])
name: "aggregation.none"
}
},
+ alarmFields: {
+ createdTime: {
+ value: "createdTime",
+ name: "alarm.created-time",
+ time: true
+ },
+ startTime: {
+ value: "startTs",
+ name: "alarm.start-time",
+ time: true
+ },
+ endTime: {
+ value: "endTs",
+ name: "alarm.end-time",
+ time: true
+ },
+ ackTime: {
+ value: "ackTs",
+ name: "alarm.ack-time",
+ time: true
+ },
+ clearTime: {
+ value: "clearTs",
+ name: "alarm.clear-time",
+ time: true
+ },
+ originator: {
+ value: "originatorName",
+ name: "alarm.originator"
+ },
+ originatorType: {
+ value: "originator.entityType",
+ name: "alarm.originator-type"
+ },
+ type: {
+ value: "type",
+ name: "alarm.type"
+ },
+ severity: {
+ value: "severity",
+ name: "alarm.severity"
+ },
+ status: {
+ value: "status",
+ name: "alarm.status"
+ }
+ },
alarmStatus: {
activeUnack: "ACTIVE_UNACK",
activeAck: "ACTIVE_ACK",
@@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', [])
alarmSeverity: {
"CRITICAL": {
name: "alarm.severity-critical",
- class: "tb-critical"
+ class: "tb-critical",
+ color: "red"
},
"MAJOR": {
name: "alarm.severity-major",
- class: "tb-major"
+ class: "tb-major",
+ color: "orange"
},
"MINOR": {
name: "alarm.severity-minor",
- class: "tb-minor"
+ class: "tb-minor",
+ color: "#ffca3d"
},
"WARNING": {
name: "alarm.severity-warning",
- class: "tb-warning"
+ class: "tb-warning",
+ color: "#abab00"
},
"INDETERMINATE": {
name: "alarm.severity-indeterminate",
- class: "tb-indeterminate"
+ class: "tb-indeterminate",
+ color: "green"
}
},
aliasFilterType: {
@@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', [])
dataKeyType: {
timeseries: "timeseries",
attribute: "attribute",
- function: "function"
+ function: "function",
+ alarm: "alarm"
},
componentType: {
filter: "FILTER",
@@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', [])
alias: "basic_gpio_control"
}
},
+ alarm: {
+ value: "alarm",
+ name: "widget.alarm",
+ template: {
+ bundleAlias: "alarm_widgets",
+ alias: "alarms_table"
+ }
+ },
static: {
value: "static",
name: "widget.static",
@@ -333,7 +394,7 @@ export default angular.module('thingsboard.types', [])
cards: "cards"
},
translate: {
- dashboardStatePrefix: "dashboardState.state."
+ customTranslationsPrefix: "custom."
}
}
).name;
ui/src/app/common/utils.service.js 69(+67 -2)
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index 8d2e565..a3433c2 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<defaultAlarmFields.length;i++) {
+ var name = defaultAlarmFields[i];
+ var dataKey = {
+ name: name,
+ type: types.dataKeyType.alarm,
+ label: $translate.instant(types.alarmFields[name].name)+'',
+ color: getMaterialColor(i),
+ settings: {},
+ _hash: Math.random()
+ };
+ defaultAlarmDataKeys.push(dataKey);
+ }
+
var service = {
getDefaultDatasource: getDefaultDatasource,
getDefaultDatasourceJson: getDefaultDatasourceJson,
+ getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
getMaterialColor: getMaterialColor,
getPredefinedFunctionBody: getPredefinedFunctionBody,
getPredefinedFunctionsList: getPredefinedFunctionsList,
@@ -109,7 +134,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
cleanCopy: cleanCopy,
isLocalUrl: isLocalUrl,
validateDatasources: validateDatasources,
- createKey: createKey
+ createKey: createKey,
+ createLabelFromDatasource: createLabelFromDatasource,
+ insertVariable: insertVariable
}
return service;
@@ -212,6 +239,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
return angular.toJson(getDefaultDatasource(dataKeySchema));
}
+ function getDefaultAlarmDataKeys() {
+ return angular.copy(defaultAlarmDataKeys);
+ }
+
function isDescriptorSchemaNotEmpty(descriptor) {
if (descriptor && descriptor.schema && descriptor.schema.properties) {
for(var prop in descriptor.schema.properties) {
@@ -357,4 +388,38 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
return dataKey;
}
+ function createLabelFromDatasource(datasource, pattern) {
+ var label = angular.copy(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);
+ }
+ 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;
+ }
+
}
ui/src/app/components/dashboard.directive.js 135(+127 -8)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index a88e656..1c2c654 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();
}
@@ -177,10 +180,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
vm.widgetBackgroundColor = widgetBackgroundColor;
vm.widgetPadding = widgetPadding;
vm.showWidgetTitle = showWidgetTitle;
+ vm.showWidgetTitlePanel = showWidgetTitlePanel;
+ vm.showWidgetActions = showWidgetActions;
vm.widgetTitleStyle = widgetTitleStyle;
+ vm.widgetTitle = widgetTitle;
+ vm.widgetActions = widgetActions;
vm.dropWidgetShadow = dropWidgetShadow;
vm.enableWidgetFullscreen = enableWidgetFullscreen;
vm.hasTimewindow = hasTimewindow;
+ vm.hasAggregation = hasAggregation;
vm.editWidget = editWidget;
vm.exportWidget = exportWidget;
vm.removeWidget = removeWidget;
@@ -267,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;
@@ -283,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() {
@@ -293,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();
});
@@ -329,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;
@@ -341,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 () {
@@ -356,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]) {
@@ -646,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;
@@ -669,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 {
@@ -746,6 +816,24 @@ 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 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;
@@ -754,6 +842,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;
@@ -771,7 +886,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}
function hasTimewindow(widget) {
- if (widget.type === types.widgetType.timeseries.value) {
+ if (widget.type === types.widgetType.timeseries.value || widget.type === types.widgetType.alarm.value) {
return angular.isDefined(widget.config.useDashboardTimewindow) ?
!widget.config.useDashboardTimewindow : false;
} else {
@@ -779,6 +894,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}
}
+ function hasAggregation(widget) {
+ return widget.type === types.widgetType.timeseries.value;
+ }
+
function adoptMaxRows() {
if (vm.widgets) {
var maxRows = vm.gridsterOpts.maxRows;
ui/src/app/components/dashboard.tpl.html 24(+18 -6)
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 8d77a6f..478e2e5 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -23,7 +23,7 @@
</md-content>
<md-menu md-position-mode="target target" tb-mousepoint-menu>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
- <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%;">
+ <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
<div id="gridster-child" gridster="vm.gridsterOpts">
<ul>
<li gridster-item="vm.widgetItemMap" class="tb-noselect" ng-repeat="widget in vm.widgets">
@@ -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)}">
- <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
- <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
- <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
+ <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitlePanel(widget)">
+ <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{vm.widgetTitle(widget)}}</span>
+ <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
- <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()">
+ <div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
+ <md-button ng-repeat="action in vm.widgetActions(widget)"
+ aria-label="{{ action.name | translate }}"
+ ng-show="!vm.isEdit && action.show"
+ ng-click="action.onAction($event)"
+ class="md-icon-button">
+ <md-tooltip md-direction="top">
+ {{ action.name | translate }}
+ </md-tooltip>
+ <ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
+ </md-button>
<md-button id="expand-button"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
@@ -92,6 +103,7 @@
aliasController: vm.aliasController,
stateController: vm.stateController,
isEdit: vm.isEdit,
+ isMobile: vm.isMobileSize,
stDiff: vm.stDiff,
dashboardTimewindow: vm.dashboardTimewindow,
dashboardTimewindowApi: vm.dashboardTimewindowApi }">
diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js
index 90810af..d19617f 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 755c4f4..bb7a4b9 100644
--- a/ui/src/app/components/datakey-config.tpl.html
+++ b/ui/src/app/components/datakey-config.tpl.html
@@ -16,7 +16,9 @@
-->
<md-content class="md-padding" layout="column">
- <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries || model.type === types.dataKeyType.attribute"
+ <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries ||
+ model.type === types.dataKeyType.attribute ||
+ model.type === types.dataKeyType.alarm"
style="padding-bottom: 8px;"
ng-required="true"
md-no-cache="true"
@@ -27,8 +29,8 @@
md-items="item in keysSearch(keySearchText)"
md-item-text="item"
md-min-length="0"
- placeholder="Key name"
- md-floating-label="Key">
+ placeholder="{{ 'entity.key-name' | translate }}"
+ md-floating-label="{{ 'entity.key' | translate }}">
<span md-highlight-text="keySearchText" md-highlight-flags="^i">{{item}}</span>
</md-autocomplete>
<div layout="row" layout-align="start center">
@@ -48,7 +50,7 @@
md-color-history="false">
</div>
</div>
- <div layout="row" layout-align="start center">
+ <div layout="row" layout-align="start center" ng-if="model.type !== types.dataKeyType.alarm">
<md-input-container flex>
<label translate>datakey.units</label>
<input name="units" ng-model="model.units">
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js
index eb9eafc..b5327e2 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 0e91d98..0633f5f 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})">
</tb-datasource-func>
<tb-datasource-entity flex
diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js
index da43274..e6ec8f8 100644
--- a/ui/src/app/components/datasource-entity.directive.js
+++ b/ui/src/app/components/datasource-entity.directive.js
@@ -45,12 +45,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
scope.ngModelCtrl = ngModelCtrl;
scope.types = types;
+ scope.alarmFields = [];
+ for (var alarmField in types.alarmFields) {
+ scope.alarmFields.push(alarmField);
+ }
+
scope.selectedTimeseriesDataKey = null;
scope.timeseriesDataKeySearchText = null;
scope.selectedAttributeDataKey = null;
scope.attributeDataKeySearchText = null;
+ scope.selectedAlarmDataKey = null;
+ scope.alarmDataKeySearchText = null;
+
scope.updateValidity = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
@@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
});
scope.$watch('timeseriesDataKeys', function () {
- if (ngModelCtrl.$viewValue) {
- var dataKeys = [];
- dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
- dataKeys = dataKeys.concat(scope.attributeDataKeys);
- ngModelCtrl.$viewValue.dataKeys = dataKeys;
- scope.updateValidity();
- }
+ updateDataKeys();
}, true);
scope.$watch('attributeDataKeys', function () {
+ updateDataKeys();
+ }, true);
+
+ scope.$watch('alarmDataKeys', function () {
+ updateDataKeys();
+ }, true);
+
+ function updateDataKeys() {
if (ngModelCtrl.$viewValue) {
var dataKeys = [];
dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
dataKeys = dataKeys.concat(scope.attributeDataKeys);
+ dataKeys = dataKeys.concat(scope.alarmDataKeys);
ngModelCtrl.$viewValue.dataKeys = dataKeys;
scope.updateValidity();
}
- }, true);
+ }
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
@@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
}
var timeseriesDataKeys = [];
var attributeDataKeys = [];
+ var alarmDataKeys = [];
for (var d in ngModelCtrl.$viewValue.dataKeys) {
var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
if (dataKey.type === types.dataKeyType.timeseries) {
timeseriesDataKeys.push(dataKey);
} else if (dataKey.type === types.dataKeyType.attribute) {
attributeDataKeys.push(dataKey);
+ } else if (dataKey.type === types.dataKeyType.alarm) {
+ alarmDataKeys.push(dataKey);
}
}
scope.timeseriesDataKeys = timeseriesDataKeys;
scope.attributeDataKeys = attributeDataKeys;
+ scope.alarmDataKeys = alarmDataKeys;
}
};
@@ -135,6 +150,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
}
+ if (!scope.alarmDataKeySearchText || scope.alarmDataKeySearchText === '') {
+ scope.alarmDataKeySearchText = scope.alarmDataKeySearchText === '' ? null : '';
+ }
};
scope.transformTimeseriesDataKeyChip = function (chip) {
@@ -145,6 +163,10 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
};
+ scope.transformAlarmDataKeyChip = function (chip) {
+ return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm});
+ };
+
scope.showColorPicker = function (event, dataKey) {
$mdColorPicker.show({
value: dataKey.color,
@@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
scope.timeseriesDataKeys[index] = dataKey;
} else if (dataKey.type === types.dataKeyType.attribute) {
scope.attributeDataKeys[index] = dataKey;
+ } else if (dataKey.type === types.dataKeyType.alarm) {
+ scope.alarmDataKeys[index] = dataKey;
}
ngModelCtrl.$setDirty();
}, function () {
@@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
};
scope.dataKeysSearch = function (searchText, type) {
- if (scope.entityAlias) {
- var deferred = $q.defer();
- scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})
- .then(function (dataKeys) {
- deferred.resolve(dataKeys);
- }, function (e) {
- deferred.reject(e);
- });
- return deferred.promise;
+ if (scope.widgetType == types.widgetType.alarm.value) {
+ var dataKeys = searchText ? scope.alarmFields.filter(
+ scope.createFilterForDataKey(searchText)) : scope.alarmFields;
+ return dataKeys;
} else {
- return $q.when([]);
+ if (scope.entityAlias) {
+ var deferred = $q.defer();
+ scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})
+ .then(function (dataKeys) {
+ deferred.resolve(dataKeys);
+ }, function (e) {
+ deferred.reject(e);
+ });
+ return deferred.promise;
+ } else {
+ return $q.when([]);
+ }
}
};
+ scope.createFilterForDataKey = function (query) {
+ var lowercaseQuery = angular.lowercase(query);
+ return function filterFn(dataKey) {
+ return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0);
+ };
+ };
+
scope.createKey = function (event, chipsId) {
var chipsChild = $(chipsId, element)[0].firstElementChild;
var el = angular.element(chipsChild);
diff --git a/ui/src/app/components/datasource-entity.scss b/ui/src/app/components/datasource-entity.scss
index 7a87fc7..b9e892b 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 2fb4608..9876491 100644
--- a/ui/src/app/components/datasource-entity.tpl.html
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -24,7 +24,7 @@
</tb-entity-alias-select>
<section flex layout='column'>
<section flex layout='column' layout-align="center" style="padding-left: 4px;">
- <md-chips flex
+ <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
id="timeseries_datakey_chips"
ng-required="true"
ng-model="timeseriesDataKeys" md-autocomplete-snap
@@ -128,10 +128,63 @@
</div>
</md-chip-template>
</md-chips>
+ <md-chips flex ng-if="widgetType == types.widgetType.alarm.value"
+ id="alarm_datakey_chips"
+ ng-required="true"
+ ng-model="alarmDataKeys" md-autocomplete-snap
+ md-transform-chip="transformAlarmDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="alarm_datakey"
+ md-selected-item="selectedAlarmDataKey"
+ md-search-text="alarmDataKeySearchText"
+ md-items="item in dataKeysSearch(alarmDataKeySearchText, types.dataKeyType.alarm)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{'datakey.alarm' | translate }}"
+ md-menu-class="tb-alarm-datakey-autocomplete">
+ <span md-highlight-text="alarmDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(alarmDataKeySearchText)">
+ <span translate>entity.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(alarmDataKeySearchText)">
+ <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
</section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
<div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
+ <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
</div>
</section>
</section>
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index fe54696..2a2ae82 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<ngModelCtrl.$viewValue.dataKeys.length;d++) {
+ var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
+ if (dataKey.type === types.dataKeyType.function) {
+ funcDataKeys.push(dataKey);
+ } else if (dataKey.type === types.dataKeyType.alarm) {
+ alarmDataKeys.push(dataKey);
+ }
+ }
}
scope.funcDataKeys = funcDataKeys;
+ scope.alarmDataKeys = alarmDataKeys;
scope.datasourceName = ngModelCtrl.$viewValue.name;
}
};
- scope.transformDataKeyChip = function (chip) {
+ scope.transformFuncDataKeyChip = function (chip) {
return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
};
+ scope.transformAlarmDataKeyChip = function (chip) {
+ return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm});
+ };
+
scope.showColorPicker = function (event, dataKey) {
$mdColorPicker.show({
value: dataKey.color,
@@ -129,7 +160,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
dataKey: angular.copy(dataKey),
dataKeySettingsSchema: scope.datakeySettingsSchema,
entityAlias: null,
- entityAliases: null
+ aliasController: null
},
parent: angular.element($document[0].body),
fullscreen: true,
@@ -140,7 +171,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
w.triggerHandler('resize');
}
}).then(function (dataKey) {
- scope.funcDataKeys[index] = dataKey;
+ if (dataKey.type === types.dataKeyType.function) {
+ scope.funcDataKeys[index] = dataKey;
+ } else if (dataKey.type === types.dataKeyType.alarm) {
+ scope.alarmDataKeys[index] = dataKey;
+ }
ngModelCtrl.$setDirty();
}, function () {
});
@@ -151,8 +186,9 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
}
scope.dataKeysSearch = function (dataKeySearchText) {
- var dataKeys = dataKeySearchText ? scope.functionTypes.filter(
- scope.createFilterForDataKey(dataKeySearchText)) : scope.functionTypes;
+ var targetKeys = scope.widgetType == types.widgetType.alarm.value ? scope.alarmFields : scope.functionTypes;
+ var dataKeys = dataKeySearchText ? targetKeys.filter(
+ scope.createFilterForDataKey(dataKeySearchText)) : targetKeys;
return dataKeys;
};
@@ -180,6 +216,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
restrict: "E",
require: "^ngModel",
scope: {
+ widgetType: '=',
generateDataKey: '&',
datakeySettingsSchema: '='
},
diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss
index 8739ed8..fdda0e4 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 e8e08f7..7fa4aac 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -17,18 +17,19 @@
-->
<section class="tb-datasource-func" flex layout='column'
layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
- <md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;">
+ <md-input-container ng-if="widgetType != types.widgetType.alarm.value"
+ class="tb-datasource-name" md-no-float style="min-width: 200px;">
<input name="datasourceName"
placeholder="{{ 'datasource.name' | translate }}"
ng-model="datasourceName"
aria-label="{{ 'datasource.name' | translate }}">
</md-input-container>
<section flex layout='column' style="padding-left: 4px;">
- <md-chips flex
+ <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
id="function_datakey_chips"
ng-required="true"
ng-model="funcDataKeys" md-autocomplete-snap
- md-transform-chip="transformDataKeyChip($chip)"
+ md-transform-chip="transformFuncDataKeyChip($chip)"
md-require-match="false">
<md-autocomplete
md-no-cache="false"
@@ -47,9 +48,9 @@
<span translate>device.no-keys-found</span>
</div>
<div ng-if="textIsNotEmpty(dataKeySearchText)">
- <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span>
+ <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span>
<span>
- <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
+ <a translate ng-click="createKey($event, '#function_datakey_chips')">entity.create-new-key</a>
</span>
</div>
</div>
@@ -75,8 +76,61 @@
</div>
</md-chip-template>
</md-chips>
+ <md-chips flex ng-if="widgetType == types.widgetType.alarm.value"
+ id="alarm_datakey_chips"
+ ng-required="true"
+ ng-model="alarmDataKeys" md-autocomplete-snap
+ md-transform-chip="transformAlarmDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="alarm_datakey"
+ md-selected-item="selectedAlarmDataKey"
+ md-search-text="alarmDataKeySearchText"
+ md-items="item in dataKeysSearch(alarmDataKeySearchText, types.dataKeyType.alarm)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{'datakey.alarm' | translate }}"
+ md-menu-class="tb-alarm-datakey-autocomplete">
+ <span md-highlight-text="alarmDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(alarmDataKeySearchText)">
+ <span translate>entity.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(alarmDataKeySearchText)">
+ <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
- <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
+ <div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div>
+ <div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
</div>
</section>
</section>
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index 296456a..422501b 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/timewindow.directive.js b/ui/src/app/components/timewindow.directive.js
index 1642a34..3206d47 100644
--- a/ui/src/app/components/timewindow.directive.js
+++ b/ui/src/app/components/timewindow.directive.js
@@ -64,7 +64,7 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
scope.historyOnly = angular.isDefined(attrs.historyOnly);
- scope.aggregation = angular.isDefined(attrs.aggregation);
+ scope.aggregation = scope.$eval(attrs.aggregation);
scope.isToolbar = angular.isDefined(attrs.isToolbar);
ui/src/app/components/widget.controller.js 41(+32 -9)
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index 08885b8..8d91b2d 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -21,7 +21,7 @@ import Subscription from '../api/subscription';
/*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
- datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
+ datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow,
dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
var vm = this;
@@ -44,13 +44,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var widgetContext = {
inited: false,
- $scope: $scope,
$container: null,
$containerParent: null,
width: 0,
height: 0,
+ hideTitlePanel: false,
isEdit: isEdit,
- isMobile: false,
+ isMobile: isMobile,
widgetConfig: widget.config,
settings: widget.config.settings,
units: widget.config.units || '',
@@ -113,6 +113,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
timeService: timeService,
deviceService: deviceService,
datasourceService: datasourceService,
+ alarmService: alarmService,
utils: utils,
widgetUtils: widgetContext.utils,
dashboardTimewindowApi: dashboardTimewindowApi,
@@ -121,6 +122,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
aliasController: aliasController
};
+ widget.$ctx = function() {
+ return widgetContext;
+ }
+
var widgetTypeInstance;
vm.useCustomDatasources = false;
@@ -285,9 +290,18 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var deferred = $q.defer();
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
options = {
- type: widget.type,
- datasources: angular.copy(widget.config.datasources)
- };
+ type: widget.type
+ }
+ if (widget.type == types.widgetType.alarm.value) {
+ options.alarmSource = angular.copy(widget.config.alarmSource);
+ options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ?
+ widget.config.alarmSearchStatus : types.alarmSearchStatus.any;
+ options.alarmsPollingInterval = angular.isDefined(widget.config.alarmsPollingInterval) ?
+ widget.config.alarmsPollingInterval * 1000 : 5000;
+ } else {
+ options.datasources = angular.copy(widget.config.datasources)
+ }
+
defaultComponentsOptions(options);
createSubscription(options).then(
@@ -320,7 +334,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.executingRpcRequest = subscription.executingRpcRequest;
},
onRpcSuccess: function(subscription) {
- $scope.executingRpcRequest = subscription.executingRpcRequest;
+ $scope.executingRpcRequest = subscription.executingRpcRequest;
$scope.rpcErrorText = subscription.rpcErrorText;
$scope.rpcRejection = subscription.rpcRejection;
},
@@ -436,7 +450,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
widgetContext.$container = $('#container', containerElement);
widgetContext.$containerParent = $(containerElement);
- $compile($element.contents())($scope);
+ if (widgetSizeDetected) {
+ widgetContext.$container.css('height', widgetContext.height + 'px');
+ widgetContext.$container.css('width', widgetContext.width + 'px');
+ }
+
+ widgetContext.$scope = $scope.$new();
+
+ $compile($element.contents())(widgetContext.$scope);
addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
}
@@ -444,6 +465,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
function destroyWidgetElement() {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$element.html('');
+ if (widgetContext.$scope) {
+ widgetContext.$scope.$destroy();
+ }
widgetContext.$container = null;
widgetContext.$containerParent = null;
}
@@ -594,7 +618,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
function gridsterItemInitialized(item) {
if (item && item.gridster) {
- widgetContext.isMobile = item.gridster.isMobile;
gridsterItemInited = true;
onInit();
// gridsterItemElement = $(item.$element);
ui/src/app/components/widget-config.directive.js 89(+71 -18)
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index d5b4239..3849cd3 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -43,7 +43,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar
.name;
/*@ngInject*/
-function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, utils) {
+function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout, types, utils) {
var linker = function (scope, element, attrs, ngModelCtrl) {
@@ -87,6 +87,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
value: null
}
+ scope.alarmSource = {
+ value: null
+ }
+
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var config = ngModelCtrl.$viewValue.config;
@@ -113,7 +117,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.showLegend = angular.isDefined(config.showLegend) ?
config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
scope.legendConfig = config.legendConfig;
- if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
+ if (scope.widgetType !== types.widgetType.rpc.value &&
+ scope.widgetType !== types.widgetType.alarm.value &&
+ scope.widgetType !== types.widgetType.static.value
&& scope.isDataEnabled) {
if (scope.datasources) {
scope.datasources.splice(0, scope.datasources.length);
@@ -137,6 +143,16 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
} else {
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 {
+ scope.alarmSource.value = null;
+ }
}
scope.settings = config.settings;
@@ -175,6 +191,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0;
ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
+ } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
+ valid = config && config.alarmSource;
+ ngModelCtrl.$setValidity('alarmSource', valid);
} else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
valid = config && config.datasources && config.datasources.length > 0;
ngModelCtrl.$setValidity('datasources', valid);
@@ -190,7 +209,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
};
scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' +
- 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () {
+ 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' +
+ 'alarmSearchStatus + alarmsPollingInterval + showLegend', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
if (value.config) {
@@ -210,6 +230,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
config.units = scope.units;
config.decimals = scope.decimals;
config.useDashboardTimewindow = scope.useDashboardTimewindow;
+ config.alarmSearchStatus = scope.alarmSearchStatus;
+ config.alarmsPollingInterval = scope.alarmsPollingInterval;
config.showLegend = scope.showLegend;
}
if (value.layout) {
@@ -253,7 +275,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true);
scope.$watch('datasources', function () {
- if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value
+ if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config
+ && scope.widgetType !== types.widgetType.rpc.value
+ && scope.widgetType !== types.widgetType.alarm.value
&& scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
var config = value.config;
@@ -286,6 +310,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}
});
+ scope.$watch('alarmSource.value', function () {
+ if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
+ var value = ngModelCtrl.$viewValue;
+ var config = value.config;
+ if (scope.alarmSource.value) {
+ config.alarmSource = scope.alarmSource.value;
+ } else {
+ config.alarmSource = null;
+ }
+ ngModelCtrl.$setViewValue(value);
+ scope.updateValidity();
+ }
+ });
+
scope.addDatasource = function () {
var newDatasource;
if (scope.functionsOnly) {
@@ -320,10 +358,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
return chip;
}
+ var label = chip;
+ if (type === types.dataKeyType.alarm) {
+ var alarmField = types.alarmFields[chip];
+ if (alarmField) {
+ label = $translate.instant(alarmField.name)+'';
+ }
+ }
+ label = scope.genNextLabel(label);
+
var result = {
name: chip,
type: type,
- label: scope.genNextLabel(chip),
+ label: label,
color: scope.genNextColor(),
settings: {},
_hash: Math.random()
@@ -351,15 +398,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
var matches = false;
do {
matches = false;
- if (value.config.datasources) {
- for (var d=0;d<value.config.datasources.length;d++) {
- var datasource = value.config.datasources[d];
- for (var k=0;k<datasource.dataKeys.length;k++) {
- var dataKey = datasource.dataKeys[k];
- if (dataKey.label === label) {
- i++;
- label = name + ' ' + i;
- matches = true;
+ var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
+ if (datasources) {
+ for (var d=0;d<datasources.length;d++) {
+ var datasource = datasources[d];
+ if (datasource && datasource.dataKeys) {
+ for (var k = 0; k < datasource.dataKeys.length; k++) {
+ var dataKey = datasource.dataKeys[k];
+ if (dataKey.label === label) {
+ i++;
+ label = name + ' ' + i;
+ matches = true;
+ }
}
}
}
@@ -371,10 +421,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.genNextColor = function () {
var i = 0;
var value = ngModelCtrl.$viewValue;
- if (value.config.datasources) {
- for (var d=0;d<value.config.datasources.length;d++) {
- var datasource = value.config.datasources[d];
- i += datasource.dataKeys.length;
+ var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
+ if (datasources) {
+ for (var d=0;d<datasources.length;d++) {
+ var datasource = datasources[d];
+ if (datasource && datasource.dataKeys) {
+ i += datasource.dataKeys.length;
+ }
}
}
return utils.getMaterialColor(i);
ui/src/app/components/widget-config.tpl.html 52(+49 -3)
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 0500e91..4bfe55c 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -20,18 +20,46 @@
<md-tab label="{{ 'widget-config.data' | translate }}"
ng-show="widgetType !== types.widgetType.static.value">
<md-content class="md-padding" layout="column">
- <div ng-show="widgetType === types.widgetType.timeseries.value" layout='column' layout-align="center"
+ <div ng-show="widgetType === types.widgetType.timeseries.value || widgetType === types.widgetType.alarm.value" layout='column' layout-align="center"
layout-gt-sm='row' layout-align-gt-sm="start center">
<md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}"
ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }}
</md-checkbox>
<section flex layout="row" layout-align="start center" style="margin-bottom: 16px;">
<span ng-class="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
- <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation flex ng-model="timewindow"></tb-timewindow>
+ <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation="{{ widgetType === types.widgetType.timeseries.value }}"
+ flex ng-model="timewindow"></tb-timewindow>
</section>
</div>
+ <div ng-show="widgetType === types.widgetType.alarm.value" layout='column' layout-align="center"
+ layout-gt-sm='row' layout-align-gt-sm="start center">
+ <md-input-container class="md-block" flex>
+ <label translate>alarm.alarm-status</label>
+ <md-select ng-model="alarmSearchStatus" style="padding-bottom: 24px;">
+ <md-option ng-repeat="searchStatus in types.alarmSearchStatus" ng-value="searchStatus">
+ {{ ('alarm.search-status.' + searchStatus) | translate }}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>alarm.polling-interval</label>
+ <input ng-required="widgetType === types.widgetType.alarm.value"
+ type="number"
+ step="1"
+ min="1"
+ name="alarmsPollingInterval"
+ ng-model="alarmsPollingInterval"/>
+ <div ng-messages="theForm.alarmsPollingInterval.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>alarm.polling-interval-required</div>
+ <div ng-message="min" translate>alarm.min-polling-interval-message</div>
+ </div>
+ </md-input-container>
+ </div>
<v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
- ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled">
+ ng-show="widgetType !== types.widgetType.rpc.value
+ && widgetType !== types.widgetType.alarm.value
+ && widgetType !== types.widgetType.static.value
+ && isDataEnabled">
<v-pane id="datasources-pane" expanded="true">
<v-pane-header>
{{ 'widget-config.datasources' | translate }}
@@ -112,6 +140,24 @@
</v-pane-content>
</v-pane>
</v-accordion>
+ <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
+ ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
+ <v-pane id="alarm-source-pane" expanded="true">
+ <v-pane-header>
+ {{ 'widget-config.alarm-source' | translate }}
+ </v-pane-header>
+ <v-pane-content style="padding: 0 5px;">
+ <tb-datasource flex
+ ng-model="alarmSource.value"
+ widget-type="widgetType"
+ functions-only="functionsOnly"
+ alias-controller="aliasController"
+ datakey-settings-schema="datakeySettingsSchema"
+ generate-data-key="generateDataKey(chip,type)"
+ on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"></tb-datasource>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
</md-content>
</md-tab>
<md-tab label="{{ 'widget-config.settings' | translate }}">
ui/src/app/customer/customers.tpl.html 10(+5 -5)
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index a6521f0..da0a2e9 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)"></tb-customer>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.customer}}"
@@ -39,7 +39,7 @@
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.customer}}"
@@ -48,19 +48,19 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.customer"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'customer.events' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'customer.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.customer"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.customer}}">
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index 5372b62..fd2ada7 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;
ui/src/app/dashboard/dashboard.controller.js 24(+9 -15)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 65eb833..6b9b0b3 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 = [];
}
ui/src/app/dashboard/dashboard.tpl.html 19(+16 -3)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 202268f..96ce711 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -52,7 +52,7 @@
<tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
is-toolbar
direction="left"
- tooltip-direction="bottom" aggregation
+ tooltip-direction="bottom" aggregation="true"
ng-model="vm.dashboardCtx.dashboardTimewindow">
</tb-timewindow>
<tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
@@ -179,6 +179,7 @@
<tb-edit-widget
dashboard="vm.dashboard"
alias-controller="vm.dashboardCtx.aliasController"
+ widget-edit-mode="vm.widgetEditMode"
widget="vm.editingWidget"
widget-layout="vm.editingWidgetLayout"
the-form="vm.widgetForm">
@@ -205,7 +206,8 @@
</header-pane>
<div ng-if="vm.isAddingWidget">
<md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||
- vm.rpcWidgetTypes.length > 0 || vm.staticWidgetTypes.length > 0"
+ vm.rpcWidgetTypes.length > 0 || vm.alarmWidgetTypes.length > 0 ||
+ vm.staticWidgetTypes.length > 0"
flex
class="tb-absolute-fill" md-border-bottom>
<md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
@@ -238,6 +240,16 @@
on-widget-clicked="vm.addWidgetFromType(event, widget)">
</tb-dashboard>
</md-tab>
+ <md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
+ <tb-dashboard
+ widgets="vm.alarmWidgetTypes"
+ is-edit="false"
+ is-mobile="true"
+ is-edit-action-enabled="false"
+ is-remove-action-enabled="false"
+ on-widget-clicked="vm.addWidgetFromType(event, widget)">
+ </tb-dashboard>
+ </md-tab>
<md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard
widgets="vm.staticWidgetTypes"
@@ -250,7 +262,8 @@
</md-tab>
</md-tabs>
<span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&
- vm.rpcWidgetTypes.length === 0 && vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
+ vm.rpcWidgetTypes.length === 0 && vm.alarmWidgetTypes.length === 0 &&
+ vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
layout-align="center center"
style="text-transform: uppercase; display: flex;"
class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index ac98709..c5743c9 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 f3ff704..427ca19 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -121,6 +121,9 @@
</div>
</md-input-container>
</div>
+ <md-checkbox flex aria-label="{{ 'dashboard.autofill-height' | translate }}"
+ ng-model="vm.gridSettings.autoFillHeight">{{ 'dashboard.autofill-height' | translate }}
+ </md-checkbox>
<div flex
ng-required="false"
md-color-picker
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 6532914..d0c651a 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 a9ff0b6..4b4382e 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"></tb-widget-config>
diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
index 4a4b25e..3df1327 100644
--- a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
@@ -49,6 +49,7 @@
state-controller="vm.dashboardCtx.stateController"
dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow"
is-edit="vm.isEdit"
+ autofill-height="vm.layoutCtx.gridSettings.autoFillHeight && !vm.isEdit"
is-mobile="vm.isMobile"
is-mobile-disabled="vm.widgetEditMode"
is-edit-action-enabled="vm.isEdit"
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
index 86eb9ec..8af5a77 100644
--- a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
@@ -53,12 +53,6 @@ export default function DashboardStateDialogController($scope, $mdDialog, $filte
if (!vm.stateIdTouched && vm.isAdd) {
vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_");
}
- var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true);
- if (result && result.length && result[0].id !== vm.prevStateId) {
- $scope.theForm.name.$setValidity('stateExists', false);
- } else {
- $scope.theForm.name.$setValidity('stateExists', true);
- }
}
function checkStateId() {
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
index fa45d7a..385de94 100644
--- a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
@@ -37,18 +37,15 @@
<input name="name" required ng-model="vm.state.name">
<div ng-messages="theForm.name.$error">
<div ng-message="required" translate>dashboard.state-name-required</div>
- <div ng-message="stateExists" translate>dashboard.state-name-exists</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>dashboard.state-id</label>
- <input name="stateId" ng-model="vm.state.id"
- ng-change="vm.stateIdTouched = true"
- ng-pattern="/^[a-zA-Z0-9_]*$/">
+ <input name="stateId" required ng-model="vm.state.id"
+ ng-change="vm.stateIdTouched = true">
<div ng-messages="theForm.stateId.$error">
<div ng-message="required" translate>dashboard.state-id-required</div>
<div ng-message="stateExists" translate>dashboard.state-id-exists</div>
- <div ng-message="pattern" translate>dashboard.invalid-state-id-format</div>
</div>
</md-input-container>
<md-checkbox flex aria-label="{{ 'dashboard.is-root-state' | translate }}"
diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js
index 76ea9b8..4844b4c 100644
--- a/ui/src/app/dashboard/states/default-state-controller.js
+++ b/ui/src/app/dashboard/states/default-state-controller.js
@@ -97,10 +97,10 @@ export default function DefaultStateController($scope, $location, $state, $state
function getStateName(id, state) {
var result = '';
- var translationId = types.translate.dashboardStatePrefix + id;
+ var translationId = types.translate.customTranslationsPrefix + state.name;
var translation = $translate.instant(translationId);
if (translation != translationId) {
- result = translation;
+ result = translation + '';
} else {
result = state.name;
}
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
index 51cf67d..6e7685e 100644
--- a/ui/src/app/dashboard/states/entity-state-controller.js
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -17,7 +17,7 @@
import './entity-state-controller.scss';
/*@ngInject*/
-export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, types, dashboardUtils, entityService) {
+export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, utils, types, dashboardUtils, entityService) {
var vm = this;
@@ -106,18 +106,17 @@ export default function EntityStateController($scope, $location, $state, $stateP
function getStateName(index) {
var result = '';
if (vm.stateObject[index]) {
+ var stateName = vm.states[vm.stateObject[index].id].name;
+ var translationId = types.translate.customTranslationsPrefix + stateName;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ stateName = translation + '';
+ }
var params = vm.stateObject[index].params;
if (params && params.entityName) {
- result = params.entityName;
+ result = utils.insertVariable(stateName, 'entityName', params.entityName);
} else {
- var id = vm.stateObject[index].id;
- var translationId = types.translate.dashboardStatePrefix + id;
- var translation = $translate.instant(translationId);
- if (translation != translationId) {
- result = translation;
- } else {
- result = vm.states[vm.stateObject[index].id].name;
- }
+ result = stateName;
}
}
return result;
@@ -243,11 +242,9 @@ export default function EntityStateController($scope, $location, $state, $stateP
}
function gotoState(stateId, update, openRightLayout) {
- if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
- vm.dashboardCtrl.openDashboardState(stateId, openRightLayout);
- if (update) {
- updateLocation();
- }
+ vm.dashboardCtrl.openDashboardState(stateId, openRightLayout);
+ if (update) {
+ updateLocation();
}
}
ui/src/app/device/devices.tpl.html 10(+5 -5)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 1e467be..b8394c7 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -32,7 +32,7 @@
on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.device}}"
@@ -40,7 +40,7 @@
default-attribute-scope="{{vm.types.attributesScope.client.value}}">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.device}}"
@@ -49,19 +49,19 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.device"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'device.events' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'device.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.device"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.device}}">
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
index 22f9d75..faed304 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) {
ui/src/app/locale/locale.constant.js 26(+22 -4)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index a54eba4..b213746 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', [])
"type": "Type",
"severity": "Severity",
"originator": "Originator",
+ "originator-type": "Originator type",
"details": "Details",
"status": "Status",
"alarm-details": "Alarm details",
@@ -144,7 +145,17 @@ export default angular.module('thingsboard.locale', [])
"severity-warning": "Warning",
"severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge",
- "clear": "Clear"
+ "clear": "Clear",
+ "search": "Search alarms",
+ "selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected",
+ "no-data": "No data to display",
+ "polling-interval": "Alarms polling interval (sec)",
+ "polling-interval-required": "Alarms polling interval is required.",
+ "min-polling-interval-message": "At least 1 sec polling interval is allowed.",
+ "aknowledge-alarms-title": "Acknowledge { count, select, 1 {1 alarm} other {# alarms} }",
+ "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, select, 1 {1 alarm} other {# alarms} }?",
+ "clear-alarms-title": "Clear { count, select, 1 {1 alarm} other {# alarms} }",
+ "clear-alarms-text": "Are you sure you want to clear { count, select, 1 {1 alarm} other {# alarms} }?"
},
"alias": {
"add": "Add alias",
@@ -420,6 +431,7 @@ export default angular.module('thingsboard.locale', [])
"vertical-margin-required": "Vertical margin value is required.",
"min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
+ "autofill-height": "Auto fill layout height",
"display-title": "Display dashboard title",
"toolbar-always-open": "Keep toolbar opened",
"title-color": "Title color",
@@ -461,11 +473,9 @@ export default angular.module('thingsboard.locale', [])
"state": "Dashboard state",
"state-name": "Name",
"state-name-required": "Dashboard state name is required.",
- "state-name-exists": "Dashboard state with the same name is already exists.",
"state-id": "State Id",
"state-id-required": "Dashboard state id is required.",
"state-id-exists": "Dashboard state with the same id is already exists.",
- "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.",
"is-root-state": "Root state",
"delete-state-title": "Delete dashboard state",
"delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?",
@@ -486,8 +496,10 @@ export default angular.module('thingsboard.locale', [])
"configuration": "Data key configuration",
"timeseries": "Timeseries",
"attributes": "Attributes",
+ "alarm": "Alarm fields",
"timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
+ "alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types",
"function-types-required": "Function types are required."
},
@@ -637,6 +649,8 @@ export default angular.module('thingsboard.locale', [])
"no-aliases-found": "No aliases found.",
"no-alias-matching": "'{{alias}}' not found.",
"create-new-alias": "Create a new one!",
+ "key": "Key",
+ "key-name": "Key name",
"no-keys-found": "No keys found.",
"no-key-matching": "'{{key}}' not found.",
"create-new-key": "Create a new one!",
@@ -1046,6 +1060,7 @@ export default angular.module('thingsboard.locale', [])
"timeseries": "Time series",
"latest-values": "Latest values",
"rpc": "Control widget",
+ "alarm": "Alarm widget",
"static": "Static widget",
"select-widget-type": "Select widget type",
"missing-widget-title-error": "Widget title must be specified!",
@@ -1133,7 +1148,8 @@ export default angular.module('thingsboard.locale', [])
"datasource-parameters": "Parameters",
"remove-datasource": "Remove datasource",
"add-datasource": "Add datasource",
- "target-device": "Target device"
+ "target-device": "Target device",
+ "alarm-source": "Alarm source"
},
"widget-type": {
"import": "Import widget type",
@@ -1150,6 +1166,8 @@ export default angular.module('thingsboard.locale', [])
"zh_CN": "Chinese",
"ru_RU": "Russian",
"es_ES": "Spanish"
+ },
+ "custom": {
}
}
}
diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js
index d227041..7410062 100644
--- a/ui/src/app/locale/translate-handler.js
+++ b/ui/src/app/locale/translate-handler.js
@@ -18,7 +18,7 @@
export default function ThingsboardMissingTranslateHandler($log, types) {
return function (translationId) {
- if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) {
+ if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) {
$log.warn('Translation for ' + translationId + ' doesn\'t exist');
}
};
ui/src/app/plugin/plugins.tpl.html 10(+5 -5)
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index 73b0adc..b3563e9 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)"></tb-plugin>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'attribute.attributes' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}"
@@ -39,7 +39,7 @@
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}"
@@ -48,19 +48,19 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'alarm.alarms' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'plugin.events' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'plugin.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.lcEvent.value}}">
</tb-event-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'relation.relations' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}">
ui/src/app/rule/rules.tpl.html 10(+5 -5)
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index 336fe63..064728b 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)"></tb-rule>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'attribute.attributes' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}"
@@ -39,7 +39,7 @@
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}"
@@ -48,19 +48,19 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'alarm.alarms' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'rule.events' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rule.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.lcEvent.value}}">
</tb-event-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'relation.relations' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}">
ui/src/app/tenant/tenants.tpl.html 10(+5 -5)
diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html
index e407291..94a827f 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)"></tb-tenant>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.tenant}}"
@@ -37,7 +37,7 @@
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.tenant}}"
@@ -46,19 +46,19 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.tenant"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'tenant.events' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'tenant.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.tenant"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.types.id.nullUid"
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
- <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.tenant}}">
ui/src/app/widget/lib/alarms-table-widget.js 597(+597 -0)
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 0000000..5a65af2
--- /dev/null
+++ b/ui/src/app/widget/lib/alarms-table-widget.js
@@ -0,0 +1,597 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './alarms-table-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html';
+import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import tinycolor from 'tinycolor2';
+import cssjs from '../../../vendor/css.js/css';
+
+export default angular.module('thingsboard.widgets.alarmsTableWidget', [])
+ .directive('tbAlarmsTableWidget', AlarmsTableWidget)
+ .name;
+
+/*@ngInject*/
+function AlarmsTableWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ tableId: '=',
+ ctx: '='
+ },
+ controller: AlarmsTableWidgetController,
+ controllerAs: 'vm',
+ templateUrl: alarmsTableWidgetTemplate
+ };
+}
+
+/*@ngInject*/
+function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, alarmService, utils, types) {
+ var vm = this;
+
+ vm.stylesInfo = {};
+ vm.contentsInfo = {};
+ vm.columnWidth = {};
+
+ vm.showData = true;
+ vm.hasData = false;
+
+ vm.alarms = [];
+ vm.alarmsCount = 0;
+ vm.selectedAlarms = []
+
+ vm.alarmSource = null;
+ vm.allAlarms = null;
+
+ vm.currentAlarm = null;
+
+ vm.enableSelection = true;
+ vm.displayDetails = true;
+ vm.allowAcknowledgment = true;
+ vm.allowClear = true;
+ vm.displayPagination = true;
+ vm.defaultPageSize = 10;
+ vm.defaultSortOrder = '-'+types.alarmFields.createdTime.value;
+
+ vm.query = {
+ order: vm.defaultSortOrder,
+ limit: vm.defaultPageSize,
+ page: 1,
+ search: null
+ };
+
+ vm.searchAction = {
+ name: 'action.search',
+ show: true,
+ onAction: function() {
+ vm.enterFilterMode();
+ },
+ icon: 'search'
+ };
+
+ vm.enterFilterMode = enterFilterMode;
+ vm.exitFilterMode = exitFilterMode;
+ vm.onReorder = onReorder;
+ vm.onPaginate = onPaginate;
+ vm.onRowClick = onRowClick;
+ vm.isCurrent = isCurrent;
+ vm.openAlarmDetails = openAlarmDetails;
+ vm.ackAlarms = ackAlarms;
+ vm.clearAlarms = clearAlarms;
+
+ vm.cellStyle = cellStyle;
+ vm.cellContent = cellContent;
+
+ $scope.$watch('vm.ctx', function() {
+ if (vm.ctx) {
+ vm.settings = vm.ctx.settings;
+ vm.widgetConfig = vm.ctx.widgetConfig;
+ vm.subscription = vm.ctx.defaultSubscription;
+ vm.alarmSource = vm.subscription.alarmSource;
+ initializeConfig();
+ updateAlarmSource();
+ }
+ });
+
+ $scope.$watch("vm.query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+ updateAlarms();
+ }
+ });
+
+ $scope.$on('alarms-table-data-updated', function(event, tableId) {
+ if (vm.tableId == tableId) {
+ if (vm.subscription) {
+ vm.allAlarms = vm.subscription.alarms;
+ updateAlarms(true);
+ $scope.$digest();
+ }
+ }
+ });
+
+ $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
+ vm.isGtXs = isGtXs;
+ });
+
+ $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
+ vm.isGtMd = isGtMd;
+ if (vm.isGtMd) {
+ vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
+ } else {
+ vm.limitOptions = null;
+ }
+ });
+
+ $scope.$watch('vm.selectedAlarms.length', function (newLength) {
+ var selectionMode = newLength ? true : false;
+ if (vm.ctx) {
+ if (selectionMode) {
+ vm.ctx.hideTitlePanel = true;
+ } else if (vm.query.search == null) {
+ vm.ctx.hideTitlePanel = false;
+ }
+ }
+ });
+
+ function initializeConfig() {
+
+ vm.ctx.widgetActions = [ vm.searchAction ];
+
+ if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
+ var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ vm.alarmsTitle = translation + '';
+ } else {
+ vm.alarmsTitle = vm.settings.alarmsTitle;
+ }
+ } else {
+ vm.alarmsTitle = $translate.instant('alarm.alarms');
+ }
+
+ vm.ctx.widgetTitle = vm.alarmsTitle;
+
+ vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true;
+ vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
+ vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true;
+ vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true;
+ vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true;
+ if (!vm.allowAcknowledgment && !vm.allowClear) {
+ vm.enableSelection = false;
+ }
+
+ vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
+
+ var pageSize = vm.settings.defaultPageSize;
+ if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) {
+ vm.defaultPageSize = pageSize;
+ }
+
+ if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) {
+ vm.defaultSortOrder = vm.settings.defaultSortOrder;
+ }
+
+ vm.query.order = vm.defaultSortOrder;
+ vm.query.limit = vm.defaultPageSize;
+ if (vm.isGtMd) {
+ vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
+ } else {
+ vm.limitOptions = null;
+ }
+
+ var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
+ var defaultColor = tinycolor(origColor);
+ var mdDark = defaultColor.setAlpha(0.87).toRgbString();
+ var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
+ var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
+ //var mdDarkIcon = mdDarkSecondary;
+ var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
+
+ var cssString = 'table.md-table th.md-column {\n'+
+ 'color: ' + mdDarkSecondary + ';\n'+
+ '}\n'+
+ 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+
+ 'border-color: ' + mdDarkSecondary + ';\n'+
+ '}\n'+
+ 'table.md-table th.md-column md-icon.md-sort-icon {\n'+
+ 'color: ' + mdDarkDisabled + ';\n'+
+ '}\n'+
+ 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
+ 'color: ' + mdDark + ';\n'+
+ '}\n'+
+ 'table.md-table td.md-cell {\n'+
+ 'color: ' + mdDark + ';\n'+
+ 'border-top: 1px '+mdDarkDivider+' solid;\n'+
+ '}\n'+
+ 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
+ 'border-color: ' + mdDarkSecondary + ';\n'+
+ '}\n'+
+ 'table.md-table td.md-cell.md-placeholder {\n'+
+ 'color: ' + mdDarkDisabled + ';\n'+
+ '}\n'+
+ 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
+ 'color: ' + mdDarkSecondary + ';\n'+
+ '}\n'+
+ '.md-table-pagination {\n'+
+ 'color: ' + mdDarkSecondary + ';\n'+
+ 'border-top: 1px '+mdDarkDivider+' solid;\n'+
+ '}\n'+
+ '.md-table-pagination .buttons md-icon {\n'+
+ 'color: ' + mdDarkSecondary + ';\n'+
+ '}\n'+
+ '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
+ 'color: ' + mdDarkSecondary + ';\n'+
+ '}';
+
+ var cssParser = new cssjs();
+ cssParser.testMode = false;
+ var namespace = 'ts-table-' + hashCode(cssString);
+ cssParser.cssPreviewNamespace = namespace;
+ cssParser.createStyleElement(namespace, cssString);
+ $element.addClass(namespace);
+
+ function hashCode(str) {
+ var hash = 0;
+ var i, char;
+ if (str.length === 0) return hash;
+ for (i = 0; i < str.length; i++) {
+ char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash;
+ }
+ return hash;
+ }
+ }
+
+ function enterFilterMode () {
+ vm.query.search = '';
+ vm.ctx.hideTitlePanel = true;
+ }
+
+ function exitFilterMode () {
+ vm.query.search = null;
+ updateAlarms();
+ vm.ctx.hideTitlePanel = false;
+ }
+
+ function onReorder () {
+ updateAlarms();
+ }
+
+ function onPaginate () {
+ updateAlarms();
+ }
+
+ function onRowClick($event, alarm) {
+ if (vm.currentAlarm != alarm) {
+ vm.currentAlarm = alarm;
+ }
+ }
+
+ function isCurrent(alarm) {
+ return (vm.currentAlarm && alarm && vm.currentAlarm.id && alarm.id) &&
+ (vm.currentAlarm.id.id === alarm.id.id);
+ }
+
+ function openAlarmDetails($event, alarm) {
+ if (alarm && alarm.id) {
+ var onShowingCallback = {
+ onShowing: function(){}
+ }
+ $mdDialog.show({
+ controller: 'AlarmDetailsDialogController',
+ controllerAs: 'vm',
+ templateUrl: alarmDetailsDialogTemplate,
+ locals: {
+ alarmId: alarm.id.id,
+ allowAcknowledgment: vm.allowAcknowledgment,
+ allowClear: vm.allowClear,
+ showingCallback: onShowingCallback
+ },
+ parent: angular.element($document[0].body),
+ targetEvent: $event,
+ fullscreen: true,
+ skipHide: true,
+ onShowing: function(scope, element) {
+ onShowingCallback.onShowing(scope, element);
+ }
+ }).then(function (alarm) {
+ if (alarm) {
+ vm.subscription.update();
+ }
+ });
+
+ }
+ }
+
+ function ackAlarms($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.selectedAlarms && vm.selectedAlarms.length > 0) {
+ var title = $translate.instant('alarm.aknowledge-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat');
+ var content = $translate.instant('alarm.aknowledge-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat');
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(title)
+ .htmlContent(content)
+ .ariaLabel(title)
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ var tasks = [];
+ for (var i=0;i<vm.selectedAlarms.length;i++) {
+ var alarm = vm.selectedAlarms[i];
+ if (alarm.id) {
+ tasks.push(alarmService.ackAlarm(alarm.id.id));
+ }
+ }
+ if (tasks.length) {
+ $q.all(tasks).then(function () {
+ vm.selectedAlarms = [];
+ vm.subscription.update();
+ });
+ }
+
+ });
+ }
+ }
+
+ function clearAlarms($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.selectedAlarms && vm.selectedAlarms.length > 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.selectedAlarms.length;i++) {
+ var alarm = vm.selectedAlarms[i];
+ if (alarm.id) {
+ tasks.push(alarmService.clearAlarm(alarm.id.id));
+ }
+ }
+ if (tasks.length) {
+ $q.all(tasks).then(function () {
+ vm.selectedAlarms = [];
+ vm.subscription.update();
+ });
+ }
+
+ });
+ }
+ }
+
+
+ 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;
+
+ if (vm.displayPagination) {
+ var startIndex = vm.query.limit * (vm.query.page - 1);
+ vm.alarms = result.slice(startIndex, startIndex + vm.query.limit);
+ } else {
+ vm.alarms = result;
+ }
+
+ 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);
+ }
+ }
+ if (!style.width) {
+ var columnWidth = vm.columnWidth[key.label];
+ style.width = columnWidth;
+ }
+ 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 value;
+ }
+ } 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 {};
+ }
+ } 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() {
+
+ vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle);
+
+ vm.stylesInfo = {};
+ vm.contentsInfo = {};
+ vm.columnWidth = {};
+
+ for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
+ var dataKey = vm.alarmSource.dataKeys[d];
+
+ var translationId = types.translate.customTranslationsPrefix + dataKey.label;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ dataKey.title = translation + '';
+ } else {
+ dataKey.title = dataKey.label;
+ }
+
+ var keySettings = dataKey.settings;
+
+ var cellStyleFunction = null;
+ var useCellStyleFunction = false;
+
+ if (keySettings.useCellStyleFunction === true) {
+ if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
+ try {
+ cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
+ useCellStyleFunction = true;
+ } catch (e) {
+ cellStyleFunction = null;
+ useCellStyleFunction = false;
+ }
+ }
+ }
+
+ vm.stylesInfo[dataKey.label] = {
+ useCellStyleFunction: useCellStyleFunction,
+ cellStyleFunction: cellStyleFunction
+ };
+
+ var cellContentFunction = null;
+ var useCellContentFunction = false;
+
+ if (keySettings.useCellContentFunction === true) {
+ if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
+ try {
+ cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction);
+ useCellContentFunction = true;
+ } catch (e) {
+ cellContentFunction = null;
+ useCellContentFunction = false;
+ }
+ }
+ }
+
+ vm.contentsInfo[dataKey.label] = {
+ useCellContentFunction: useCellContentFunction,
+ cellContentFunction: cellContentFunction
+ };
+
+ var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
+ vm.columnWidth[dataKey.label] = columnWidth;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/alarms-table-widget.scss b/ui/src/app/widget/lib/alarms-table-widget.scss
new file mode 100644
index 0000000..600be46
--- /dev/null
+++ b/ui/src/app/widget/lib/alarms-table-widget.scss
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-has-timewindow {
+ .tb-alarms-table {
+ md-toolbar {
+ min-height: 60px;
+ max-height: 60px;
+ &.md-table-toolbar {
+ .md-toolbar-tools {
+ max-height: 60px;
+ }
+ }
+ }
+ }
+}
+
+.tb-alarms-table {
+
+ md-toolbar {
+ min-height: 39px;
+ max-height: 39px;
+ &.md-table-toolbar {
+ .md-toolbar-tools {
+ max-height: 39px;
+ }
+ }
+ }
+
+ &.tb-data-table {
+ table.md-table, table.md-table.md-row-select {
+ tbody {
+ tr {
+ td {
+ &.tb-action-cell {
+ min-width: 36px;
+ max-width: 36px;
+ width: 36px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
ui/src/app/widget/lib/alarms-table-widget.tpl.html 103(+103 -0)
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 0000000..942b2af
--- /dev/null
+++ b/ui/src/app/widget/lib/alarms-table-widget.tpl.html
@@ -0,0 +1,103 @@
+<!--
+
+ 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.
+
+-->
+<div class="tb-absolute-fill tb-alarms-table tb-data-table" layout="column">
+ <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
+ <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedAlarms.length &&
+ vm.query.search != null">
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{'alarm.search' | translate}}
+ </md-tooltip>
+ </md-button>
+ <md-input-container flex>
+ <label> </label>
+ <input ng-model="vm.query.search" placeholder="{{'alarm.search' | translate}}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedAlarms.length">
+ <div class="md-toolbar-tools">
+ <span translate="alarm.selected-alarms"
+ translate-values="{count: vm.selectedAlarms.length}"
+ translate-interpolation="messageformat"></span>
+ <span flex></span>
+ <md-button ng-if="vm.allowAcknowledgment" class="md-icon-button" ng-click="vm.ackAlarms($event)">
+ <md-icon>done</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'alarm.acknowledge' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-if="vm.allowClear" class="md-icon-button" ng-click="vm.clearAlarms($event)">
+ <md-icon>clear</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'alarm.clear' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-table-container flex>
+ <table md-table md-row-select="vm.enableSelection" multiple="" ng-model="vm.selectedAlarms">
+ <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+ <tr md-row>
+ <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.title }}</span></th>
+ <th md-column ng-if="vm.displayDetails"><span> </span></th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr ng-show="vm.alarms.length" md-row md-select="alarm"
+ md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms"
+ ng-click="vm.onRowClick($event, alarm)" ng-class="{'tb-current': vm.isCurrent(alarm)}">
+ <td md-cell flex ng-repeat="key in vm.alarmSource.dataKeys"
+ ng-style="vm.cellStyle(alarm, key)"
+ ng-bind-html="vm.cellContent(alarm, key)">
+ </td>
+ <td md-cell ng-if="vm.displayDetails" class="tb-action-cell">
+ <md-button class="md-icon-button" aria-label="{{ 'alarm.details' | translate }}"
+ ng-click="vm.openAlarmDetails($event, alarm)">
+ <md-icon aria-label="{{ 'alarm.details' | translate }}" class="material-icons">more_horiz</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'alarm.details' | translate }}
+ </md-tooltip>
+ </md-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <md-divider></md-divider>
+ <span ng-show="!vm.alarms.length"
+ layout-align="center center"
+ class="no-data-found" translate>alarm.no-alarms-prompt</span>
+ </md-table-container>
+ <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions"
+ md-page="vm.query.page" md-total="{{vm.alarmsCount}}"
+ md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd">
+ </md-table-pagination>
+ </div>
+ <span ng-show="!vm.showData"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="tb-absolute-fill" translate>alarm.no-data</span>
+</div>
diff --git a/ui/src/app/widget/select-widget-type.tpl.html b/ui/src/app/widget/select-widget-type.tpl.html
index e035273..b7ebcb1 100644
--- a/ui/src/app/widget/select-widget-type.tpl.html
+++ b/ui/src/app/widget/select-widget-type.tpl.html
@@ -54,6 +54,13 @@
<span translate>{{vm.types.widgetType.rpc.name}}</span>
</md-button>
<md-button class="tb-card-button md-raised md-primary" layout="column"
+ ng-click="vm.typeSelected(vm.types.widgetType.alarm.value)">
+ <md-icon class="material-icons tb-md-96"
+ aria-label="{{ vm.types.widgetType.alarm.name | translate }}">error
+ </md-icon>
+ <span translate>{{vm.types.widgetType.alarm.name}}</span>
+ </md-button>
+ <md-button class="tb-card-button md-raised md-primary" layout="column"
ng-click="vm.typeSelected(vm.types.widgetType.static.value)">
<md-icon class="material-icons tb-md-96"
aria-label="{{ vm.types.widgetType.static.name | translate }}">font_download
diff --git a/ui/src/app/widget/widget-editor.controller.js b/ui/src/app/widget/widget-editor.controller.js
index 24f8fc7..7eb6b98 100644
--- a/ui/src/app/widget/widget-editor.controller.js
+++ b/ui/src/app/widget/widget-editor.controller.js
@@ -329,10 +329,14 @@ export default function WidgetEditorController(widgetService, userService, types
$scope.$watch('vm.widget.type', function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) {
var config = angular.fromJson(vm.widget.defaultConfig);
- if (vm.widget.type !== types.widgetType.rpc.value) {
+ if (vm.widget.type !== types.widgetType.rpc.value
+ && vm.widget.type !== types.widgetType.alarm.value) {
if (config.targetDeviceAliases) {
delete config.targetDeviceAliases;
}
+ if (config.alarmSource) {
+ delete config.alarmSource;
+ }
if (!config.datasources) {
config.datasources = [];
}
@@ -346,22 +350,38 @@ export default function WidgetEditorController(widgetService, userService, types
for (var i = 0; i < config.datasources.length; i++) {
var datasource = config.datasources[i];
datasource.type = vm.widget.type;
- if (vm.widget.type !== types.widgetType.timeseries.value && datasource.intervalSec) {
- delete datasource.intervalSec;
- } else if (vm.widget.type === types.widgetType.timeseries.value && !datasource.intervalSec) {
- datasource.intervalSec = 60;
- }
}
- } else {
+ } else if (vm.widget.type == types.widgetType.rpc.value) {
if (config.datasources) {
delete config.datasources;
}
+ if (config.alarmSource) {
+ delete config.alarmSource;
+ }
if (config.timewindow) {
delete config.timewindow;
}
if (!config.targetDeviceAliases) {
config.targetDeviceAliases = [];
}
+ } else { // alarm
+ if (config.datasources) {
+ delete config.datasources;
+ }
+ if (config.targetDeviceAliases) {
+ delete config.targetDeviceAliases;
+ }
+ if (!config.alarmSource) {
+ config.alarmSource = {};
+ config.alarmSource.type = vm.widget.type
+ }
+ if (!config.timewindow) {
+ config.timewindow = {
+ realtime: {
+ timewindowMs: 24 * 60 * 60 * 1000
+ }
+ };
+ }
}
vm.widget.defaultConfig = angular.toJson(config);
}
ui/src/scss/main.scss 54(+54 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index ab720c5..c47f177 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -281,7 +281,61 @@ 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;
+ &: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: rgba(221, 221, 221, 0.65) !important;
+ }
+ }
+ }
+ }
tr {
td {
&.tb-action-cell {