Browse Source

Merge remote-tracking branch 'upstream/master' into dao-refactoring-vs

pull/152/head
Volodymyr Babak 9 years ago
parent
commit
f806c392b2
  1. 26
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  2. 9
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  3. 42
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  4. 5
      ui/src/app/alarm/alarm-details-dialog.controller.js
  5. 8
      ui/src/app/alarm/alarm-details-dialog.tpl.html
  6. 7
      ui/src/app/alarm/alarm-row.directive.js
  7. 112
      ui/src/app/api/alarm.service.js
  8. 58
      ui/src/app/api/alias-controller.js
  9. 238
      ui/src/app/api/subscription.js
  10. 3
      ui/src/app/api/widget.service.js
  11. 10
      ui/src/app/asset/assets.tpl.html
  12. 75
      ui/src/app/common/types.constant.js
  13. 69
      ui/src/app/common/utils.service.js
  14. 135
      ui/src/app/components/dashboard.directive.js
  15. 24
      ui/src/app/components/dashboard.tpl.html
  16. 46
      ui/src/app/components/datakey-config.directive.js
  17. 10
      ui/src/app/components/datakey-config.tpl.html
  18. 73
      ui/src/app/components/datasource-entity.directive.js
  19. 2
      ui/src/app/components/datasource-entity.scss
  20. 55
      ui/src/app/components/datasource-entity.tpl.html
  21. 53
      ui/src/app/components/datasource-func.directive.js
  22. 2
      ui/src/app/components/datasource-func.scss
  23. 66
      ui/src/app/components/datasource-func.tpl.html
  24. 13
      ui/src/app/components/datasource.directive.js
  25. 1
      ui/src/app/components/datasource.tpl.html
  26. 8
      ui/src/app/components/grid.directive.js
  27. 2
      ui/src/app/components/timewindow.directive.js
  28. 89
      ui/src/app/components/widget-config.directive.js
  29. 52
      ui/src/app/components/widget-config.tpl.html
  30. 41
      ui/src/app/components/widget.controller.js
  31. 10
      ui/src/app/customer/customers.tpl.html
  32. 4
      ui/src/app/dashboard/add-widget.controller.js
  33. 1
      ui/src/app/dashboard/dashboard-settings.controller.js
  34. 3
      ui/src/app/dashboard/dashboard-settings.tpl.html
  35. 24
      ui/src/app/dashboard/dashboard.controller.js
  36. 19
      ui/src/app/dashboard/dashboard.tpl.html
  37. 1
      ui/src/app/dashboard/edit-widget.directive.js
  38. 2
      ui/src/app/dashboard/edit-widget.tpl.html
  39. 1
      ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
  40. 6
      ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
  41. 7
      ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
  42. 4
      ui/src/app/dashboard/states/default-state-controller.js
  43. 27
      ui/src/app/dashboard/states/entity-state-controller.js
  44. 10
      ui/src/app/device/devices.tpl.html
  45. 1
      ui/src/app/help/help-links.constant.js
  46. 26
      ui/src/app/locale/locale.constant.js
  47. 2
      ui/src/app/locale/translate-handler.js
  48. 10
      ui/src/app/plugin/plugins.tpl.html
  49. 10
      ui/src/app/rule/rules.tpl.html
  50. 10
      ui/src/app/tenant/tenants.tpl.html
  51. 597
      ui/src/app/widget/lib/alarms-table-widget.js
  52. 58
      ui/src/app/widget/lib/alarms-table-widget.scss
  53. 103
      ui/src/app/widget/lib/alarms-table-widget.tpl.html
  54. 7
      ui/src/app/widget/select-widget-type.tpl.html
  55. 34
      ui/src/app/widget/widget-editor.controller.js
  56. 54
      ui/src/scss/main.scss

26
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);
}
}
}

9
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);
}

42
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();

5
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;

8
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' |

7
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,

112
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];
}
}
}

58
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);

238
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++) {

3
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;

10
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}}">

75
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;

69
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;
}
}

135
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;

24
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 }">

46
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);
}

10
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">

73
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);

2
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;

55
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:&apos;...&apos;}}" }'>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>

53
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: '='
},

2
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;

66
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:&apos;...&apos;}}" }'>device.no-key-matching</span>
<span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>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:&apos;...&apos;}}" }'>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>

13
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 = {};
}
};

1
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

8
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);
}

2
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);

89
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);

52
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 }}">

41
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);

10
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}}">

4
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;

1
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%';

3
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

24
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 = [];
}

19
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>

1
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: '='

2
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>

1
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"

6
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() {

7
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 }}"

4
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;
}

27
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();
}
}

10
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}}">

1
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) {

26
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": {
}
}
}

2
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');
}
};

10
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}}">

10
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}}">

10
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}}">

597
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;
}
}
}

58
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;
}
}
}
}
}
}
}

103
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>&nbsp;</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>&nbsp</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>

7
ui/src/app/widget/select-widget-type.tpl.html

@ -53,6 +53,13 @@
</md-icon>
<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"

34
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);
}

54
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 {

Loading…
Cancel
Save