Browse Source

Merge pull request #168 from thingsboard/feature/TB-63

TB-63: Alarms widget initial implementation.
pull/175/head
Igor Kulikov 9 years ago
committed by GitHub
parent
commit
6056af8e09
  1. 94
      ui/src/app/api/alarm.service.js
  2. 58
      ui/src/app/api/alias-controller.js
  3. 214
      ui/src/app/api/subscription.js
  4. 3
      ui/src/app/api/widget.service.js
  5. 73
      ui/src/app/common/types.constant.js
  6. 54
      ui/src/app/common/utils.service.js
  7. 7
      ui/src/app/components/dashboard.directive.js
  8. 2
      ui/src/app/components/dashboard.tpl.html
  9. 73
      ui/src/app/components/datasource-entity.directive.js
  10. 52
      ui/src/app/components/datasource-entity.tpl.html
  11. 53
      ui/src/app/components/datasource-func.directive.js
  12. 59
      ui/src/app/components/datasource-func.tpl.html
  13. 13
      ui/src/app/components/datasource.directive.js
  14. 1
      ui/src/app/components/datasource.tpl.html
  15. 2
      ui/src/app/components/timewindow.directive.js
  16. 80
      ui/src/app/components/widget-config.directive.js
  17. 28
      ui/src/app/components/widget-config.tpl.html
  18. 29
      ui/src/app/components/widget.controller.js
  19. 4
      ui/src/app/dashboard/add-widget.controller.js
  20. 24
      ui/src/app/dashboard/dashboard.controller.js
  21. 19
      ui/src/app/dashboard/dashboard.tpl.html
  22. 1
      ui/src/app/dashboard/edit-widget.directive.js
  23. 2
      ui/src/app/dashboard/edit-widget.tpl.html
  24. 1
      ui/src/app/help/help-links.constant.js
  25. 12
      ui/src/app/locale/locale.constant.js
  26. 390
      ui/src/app/widget/lib/alarms-table-widget.js
  27. 34
      ui/src/app/widget/lib/alarms-table-widget.scss
  28. 116
      ui/src/app/widget/lib/alarms-table-widget.tpl.html
  29. 7
      ui/src/app/widget/select-widget-type.tpl.html
  30. 34
      ui/src/app/widget/widget-editor.controller.js

94
ui/src/app/api/alarm.service.js

@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', [])
.name;
/*@ngInject*/
function AlarmService($http, $q, $interval, $filter) {
function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) {
var alarmSourceListeners = {};
var simulatedAlarm = {
createdTime: (new Date).getTime(),
startTs: (new Date).getTime(),
endTs: 0,
ackTs: 0,
clearTs: 0,
originatorName: 'Simulated',
originator: {
entityType: "DEVICE",
id: "1"
},
type: 'TEMPERATURE',
severity: "MAJOR",
status: types.alarmStatus.activeUnack,
details: {
message: "Temperature is high!"
}
};
var service = {
getAlarm: getAlarm,
getAlarmInfo: getAlarmInfo,
@ -27,7 +49,9 @@ function AlarmService($http, $q, $interval, $filter) {
clearAlarm: clearAlarm,
getAlarms: getAlarms,
pollAlarms: pollAlarms,
cancelPollAlarms: cancelPollAlarms
cancelPollAlarms: cancelPollAlarms,
subscribeForAlarms: subscribeForAlarms,
unsubscribeFromAlarms: unsubscribeFromAlarms
}
return service;
@ -171,12 +195,21 @@ function AlarmService($http, $q, $interval, $filter) {
pageLink = {
limit: alarmsQuery.limit
};
} else {
} else if (alarmsQuery.interval) {
pageLink = {
limit: 100,
startTime: time - alarmsQuery.interval
};
} else if (alarmsQuery.startTime) {
pageLink = {
limit: 100,
startTime: alarmsQuery.startTime
}
if (alarmsQuery.endTime) {
pageLink.endTime = alarmsQuery.endTime;
}
}
fetchAlarms(alarmsQuery, pageLink, deferred);
return deferred.promise;
}
@ -211,4 +244,59 @@ function AlarmService($http, $q, $interval, $filter) {
}
}
function subscribeForAlarms(alarmSourceListener) {
alarmSourceListener.id = utils.guid();
alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener;
var alarmSource = alarmSourceListener.alarmSource;
if (alarmSource.type == types.datasourceType.function) {
$timeout(function() {
alarmSourceListener.alarmsUpdated([simulatedAlarm], false);
});
} else {
var pollingInterval = 5000; //TODO:
alarmSourceListener.alarmsQuery = {
entityType: alarmSource.entityType,
entityId: alarmSource.entityId,
alarmSearchStatus: null, //TODO:
alarmStatus: null
}
var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'});
if (originatorKeys && originatorKeys.length) {
alarmSourceListener.alarmsQuery.fetchOriginator = true;
}
var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow;
if (subscriptionTimewindow.realtimeWindowMs) {
alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs;
} else {
alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs;
alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs;
}
alarmSourceListener.alarmsQuery.onAlarms = function(alarms) {
if (subscriptionTimewindow.realtimeWindowMs) {
var now = Date.now();
if (alarmSourceListener.lastUpdateTs) {
var interval = now - alarmSourceListener.lastUpdateTs;
alarmSourceListener.alarmsQuery.startTime += interval;
} else {
alarmSourceListener.lastUpdateTs = now;
}
}
alarmSourceListener.alarmsUpdated(alarms, false);
}
onPollAlarms(alarmSourceListener.alarmsQuery);
alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval,
0, false, alarmSourceListener.alarmsQuery);
}
}
function unsubscribeFromAlarms(alarmSourceListener) {
if (alarmSourceListener && alarmSourceListener.id) {
if (alarmSourceListener.pollPromise) {
$interval.cancel(alarmSourceListener.pollPromise);
alarmSourceListener.pollPromise = null;
}
delete alarmSourceListeners[alarmSourceListener.id];
}
}
}

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

214
ui/src/app/api/subscription.js

@ -64,6 +64,39 @@ export default class Subscription {
deferred.resolve(subscription);
}
);
} else if (this.type === this.ctx.types.widgetType.alarm.value) {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
this.alarmSource = options.alarmSource;
this.alarmSourceListener = null;
this.alarms = [];
this.originalTimewindow = null;
this.timeWindow = {
stDiff: this.ctx.stDiff
}
this.useDashboardTimewindow = options.useDashboardTimewindow;
if (this.useDashboardTimewindow) {
this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
} else {
this.timeWindowConfig = angular.copy(options.timeWindowConfig);
}
this.subscriptionTimewindow = null;
this.loadingData = false;
this.displayLegend = false;
this.initAlarmSubscription().then(
function success() {
deferred.resolve(subscription);
},
function fail() {
deferred.reject();
}
);
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
@ -132,6 +165,44 @@ export default class Subscription {
return deferred.promise;
}
initAlarmSubscription() {
var deferred = this.ctx.$q.defer();
if (!this.ctx.aliasController) {
this.configureAlarmsData();
deferred.resolve();
} else {
var subscription = this;
this.ctx.aliasController.resolveAlarmSource(this.alarmSource).then(
function success(alarmSource) {
subscription.alarmSource = alarmSource;
subscription.configureAlarmsData();
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
}
return deferred.promise;
}
configureAlarmsData() {
var subscription = this;
var registration;
if (this.useDashboardTimewindow) {
registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
subscription.unsubscribe();
subscription.subscribe();
}
});
this.registrations.push(registration);
} else {
this.startWatchingTimewindow();
}
}
initDataSubscription() {
var deferred = this.ctx.$q.defer();
if (!this.ctx.aliasController) {
@ -393,6 +464,8 @@ export default class Subscription {
onAliasesChanged(aliasIds) {
if (this.type === this.ctx.types.widgetType.rpc.value) {
return this.checkRpcTarget(aliasIds);
} else if (this.type === this.ctx.types.widgetType.alarm.value) {
return this.checkAlarmSource(aliasIds);
} else {
return this.checkSubscriptions(aliasIds);
}
@ -516,6 +589,15 @@ export default class Subscription {
}
}
alarmsUpdated(alarms, apply) {
this.notifyDataLoaded();
this.alarms = alarms;
if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
this.updateTimewindow();
}
this.onDataUpdated(apply);
}
updateLegend(dataIndex, data, apply) {
var dataKey = this.legendData.keys[dataIndex].dataKey;
var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;
@ -540,62 +622,104 @@ export default class Subscription {
if (this.type === this.ctx.types.widgetType.rpc.value) {
return;
}
if (this.type === this.ctx.types.widgetType.alarm.value) {
this.alarmsSubscribe();
} else {
this.notifyDataLoading();
if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
var index = 0;
for (var i = 0; i < this.datasources.length; i++) {
var datasource = this.datasources[i];
if (angular.isFunction(datasource))
continue;
var subscription = this;
var listener = {
subscriptionType: this.type,
subscriptionTimewindow: this.subscriptionTimewindow,
datasource: datasource,
entityType: datasource.entityType,
entityId: datasource.entityId,
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
},
updateRealtimeSubscription: function () {
this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: function (subscriptionTimewindow) {
subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
},
datasourceIndex: index
};
for (var a = 0; a < datasource.dataKeys.length; a++) {
this.data[index + a].data = [];
}
index += datasource.dataKeys.length;
this.datasourceListeners.push(listener);
this.ctx.datasourceService.subscribeToDatasource(listener);
if (datasource.unresolvedStateEntity) {
this.notifyDataLoaded();
this.onDataUpdated();
}
}
}
}
alarmsSubscribe() {
this.notifyDataLoading();
if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
if (this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
var index = 0;
for (var i = 0; i < this.datasources.length; i++) {
var datasource = this.datasources[i];
if (angular.isFunction(datasource))
continue;
var subscription = this;
var listener = {
subscriptionType: this.type,
subscriptionTimewindow: this.subscriptionTimewindow,
datasource: datasource,
entityType: datasource.entityType,
entityId: datasource.entityId,
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
},
updateRealtimeSubscription: function () {
this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: function (subscriptionTimewindow) {
subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
},
datasourceIndex: index
};
for (var a = 0; a < datasource.dataKeys.length; a++) {
this.data[index + a].data = [];
var subscription = this;
this.alarmSourceListener = {
subscriptionTimewindow: this.subscriptionTimewindow,
alarmSource: this.alarmSource,
alarmsUpdated: function(alarms, apply) {
subscription.alarmsUpdated(alarms, apply);
}
}
this.alarms = [];
index += datasource.dataKeys.length;
this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener);
this.datasourceListeners.push(listener);
this.ctx.datasourceService.subscribeToDatasource(listener);
if (datasource.unresolvedStateEntity) {
this.notifyDataLoaded();
this.onDataUpdated();
}
if (this.alarmSource.unresolvedStateEntity) {
this.notifyDataLoaded();
this.onDataUpdated();
}
}
unsubscribe() {
if (this.type !== this.ctx.types.widgetType.rpc.value) {
for (var i = 0; i < this.datasourceListeners.length; i++) {
var listener = this.datasourceListeners[i];
this.ctx.datasourceService.unsubscribeFromDatasource(listener);
if (this.type == this.ctx.types.widgetType.alarm.value) {
this.alarmsUnsubscribe();
} else {
for (var i = 0; i < this.datasourceListeners.length; i++) {
var listener = this.datasourceListeners[i];
this.ctx.datasourceService.unsubscribeFromDatasource(listener);
}
this.datasourceListeners = [];
}
this.datasourceListeners = [];
}
}
alarmsUnsubscribe() {
if (this.alarmSourceListener) {
this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener);
this.alarmSourceListener = null;
}
}
@ -607,6 +731,14 @@ export default class Subscription {
}
}
checkAlarmSource(aliasIds) {
if (this.alarmSource && this.alarmSource.entityAliasId) {
return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1;
} else {
return false;
}
}
checkSubscriptions(aliasIds) {
var subscriptionsChanged = false;
for (var i = 0; i < this.datasourceListeners.length; i++) {

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;

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

54
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,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
cleanCopy: cleanCopy,
isLocalUrl: isLocalUrl,
validateDatasources: validateDatasources,
createKey: createKey
createKey: createKey,
createLabelFromDatasource: createLabelFromDatasource
}
return service;
@ -212,6 +238,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 +387,24 @@ 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;
}
}

7
ui/src/app/components/dashboard.directive.js

@ -181,6 +181,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
vm.dropWidgetShadow = dropWidgetShadow;
vm.enableWidgetFullscreen = enableWidgetFullscreen;
vm.hasTimewindow = hasTimewindow;
vm.hasAggregation = hasAggregation;
vm.editWidget = editWidget;
vm.exportWidget = exportWidget;
vm.removeWidget = removeWidget;
@ -771,7 +772,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 +780,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;

2
ui/src/app/components/dashboard.tpl.html

@ -47,7 +47,7 @@
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>
<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()">
<md-button id="expand-button"

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

52
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,60 @@
</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="true">
<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>
</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: '='
},

59
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"
@ -75,8 +76,58 @@
</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="true">
<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>
</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

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

80
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,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
} else {
scope.targetDeviceAlias.value = null;
}
} else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
if (config.alarmSource) {
scope.alarmSource.value = config.alarmSource;
} else {
scope.alarmSource.value = null;
}
}
scope.settings = config.settings;
@ -175,6 +187,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);
@ -253,7 +268,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true);
scope.$watch('datasources', function () {
if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value
if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config
&& scope.widgetType !== types.widgetType.rpc.value
&& scope.widgetType !== types.widgetType.alarm.value
&& scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
var config = value.config;
@ -286,6 +303,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}
});
scope.$watch('alarmSource.value', function () {
if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
var config = value.config;
if (scope.alarmSource.value) {
config.alarmSource = scope.alarmSource.value;
} else {
config.alarmSource = null;
}
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
});
scope.addDatasource = function () {
var newDatasource;
if (scope.functionsOnly) {
@ -320,10 +351,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
return chip;
}
var label = chip;
if (type === types.dataKeyType.alarm) {
var alarmField = types.alarmFields[chip];
if (alarmField) {
label = $translate.instant(alarmField.name)+'';
}
}
label = scope.genNextLabel(label);
var result = {
name: chip,
type: type,
label: scope.genNextLabel(chip),
label: label,
color: scope.genNextColor(),
settings: {},
_hash: Math.random()
@ -351,15 +391,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
var matches = false;
do {
matches = false;
if (value.config.datasources) {
for (var d=0;d<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 +414,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);

28
ui/src/app/components/widget-config.tpl.html

@ -20,18 +20,22 @@
<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>
<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 +116,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 }}">

29
ui/src/app/components/widget.controller.js

@ -21,7 +21,7 @@ import Subscription from '../api/subscription';
/*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
var vm = this;
@ -44,7 +44,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var widgetContext = {
inited: false,
$scope: $scope,
$container: null,
$containerParent: null,
width: 0,
@ -113,6 +112,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
timeService: timeService,
deviceService: deviceService,
datasourceService: datasourceService,
alarmService: alarmService,
utils: utils,
widgetUtils: widgetContext.utils,
dashboardTimewindowApi: dashboardTimewindowApi,
@ -285,9 +285,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var deferred = $q.defer();
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
options = {
type: widget.type,
datasources: angular.copy(widget.config.datasources)
};
type: widget.type
}
if (widget.type == types.widgetType.alarm.value) {
options.alarmSource = angular.copy(widget.config.alarmSource);
} else {
options.datasources = angular.copy(widget.config.datasources)
}
defaultComponentsOptions(options);
createSubscription(options).then(
@ -320,7 +325,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.executingRpcRequest = subscription.executingRpcRequest;
},
onRpcSuccess: function(subscription) {
$scope.executingRpcRequest = subscription.executingRpcRequest;
$scope.executingRpcRequest = subscription.executingRpcRequest;
$scope.rpcErrorText = subscription.rpcErrorText;
$scope.rpcRejection = subscription.rpcRejection;
},
@ -436,7 +441,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
widgetContext.$container = $('#container', containerElement);
widgetContext.$containerParent = $(containerElement);
$compile($element.contents())($scope);
if (widgetSizeDetected) {
widgetContext.$container.css('height', widgetContext.height + 'px');
widgetContext.$container.css('width', widgetContext.width + 'px');
}
widgetContext.$scope = $scope.$new();
$compile($element.contents())(widgetContext.$scope);
addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
}
@ -444,6 +456,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
function destroyWidgetElement() {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$element.html('');
if (widgetContext.$scope) {
widgetContext.$scope.$destroy();
}
widgetContext.$container = null;
widgetContext.$containerParent = null;
}

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;

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

12
ui/src/app/locale/locale.constant.js

@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', [])
"type": "Type",
"severity": "Severity",
"originator": "Originator",
"originator-type": "Originator type",
"details": "Details",
"status": "Status",
"alarm-details": "Alarm details",
@ -144,7 +145,10 @@ export default angular.module('thingsboard.locale', [])
"severity-warning": "Warning",
"severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge",
"clear": "Clear"
"clear": "Clear",
"search": "Search alarms",
"selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected",
"no-data": "No data to display"
},
"alias": {
"add": "Add alias",
@ -486,8 +490,10 @@ export default angular.module('thingsboard.locale', [])
"configuration": "Data key configuration",
"timeseries": "Timeseries",
"attributes": "Attributes",
"alarm": "Alarm fields",
"timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
"alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types",
"function-types-required": "Function types are required."
},
@ -1046,6 +1052,7 @@ export default angular.module('thingsboard.locale', [])
"timeseries": "Time series",
"latest-values": "Latest values",
"rpc": "Control widget",
"alarm": "Alarm widget",
"static": "Static widget",
"select-widget-type": "Select widget type",
"missing-widget-title-error": "Widget title must be specified!",
@ -1133,7 +1140,8 @@ export default angular.module('thingsboard.locale', [])
"datasource-parameters": "Parameters",
"remove-datasource": "Remove datasource",
"add-datasource": "Add datasource",
"target-device": "Target device"
"target-device": "Target device",
"alarm-source": "Alarm source"
},
"widget-type": {
"import": "Import widget type",

390
ui/src/app/widget/lib/alarms-table-widget.js

@ -0,0 +1,390 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './alarms-table-widget.scss';
/* eslint-disable import/no-unresolved, import/default */
import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import tinycolor from 'tinycolor2';
import cssjs from '../../../vendor/css.js/css';
export default angular.module('thingsboard.widgets.alarmsTableWidget', [])
.directive('tbAlarmsTableWidget', AlarmsTableWidget)
.name;
/*@ngInject*/
function AlarmsTableWidget() {
return {
restrict: "E",
scope: true,
bindToController: {
tableId: '=',
config: '=',
subscription: '='
},
controller: AlarmsTableWidgetController,
controllerAs: 'vm',
templateUrl: alarmsTableWidgetTemplate
};
}
/*@ngInject*/
function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdUtil, $translate, utils, types) {
var vm = this;
vm.stylesInfo = {};
vm.contentsInfo = {};
vm.showData = true;
vm.hasData = false;
vm.alarms = [];
vm.alarmsCount = 0;
vm.selectedAlarms = []
vm.alarmSource = null;
vm.allAlarms = null;
vm.currentAlarm = null;
vm.query = {
order: '-'+types.alarmFields.createdTime.value,
limit: 10,
page: 1,
search: null
};
vm.alarmsTitle = $translate.instant('alarm.alarms');
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.onReorder = onReorder;
vm.onPaginate = onPaginate;
vm.cellStyle = cellStyle;
vm.cellContent = cellContent;
$scope.$watch('vm.config', function() {
if (vm.config) {
vm.settings = vm.config.settings;
vm.widgetConfig = vm.config.widgetConfig;
initializeConfig();
}
});
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateAlarms();
}
});
$scope.$watch('vm.subscription', function() {
if (vm.subscription) {
vm.alarmSource = vm.subscription.alarmSource;
updateAlarmSource();
}
});
$scope.$on('alarms-table-data-updated', function(event, tableId) {
if (vm.tableId == tableId) {
if (vm.subscription) {
vm.allAlarms = vm.subscription.alarms;
updateAlarms(true);
$scope.$digest();
}
}
});
$scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
vm.isGtXs = isGtXs;
});
$scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
vm.isGtMd = isGtMd;
if (vm.isGtMd) {
vm.limitOptions = [5, 10, 15];
} else {
vm.limitOptions = null;
}
});
function initializeConfig() {
if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
vm.alarmsTitle = vm.settings.alarmsTitle;
}
//TODO:
var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
var defaultColor = tinycolor(origColor);
var mdDark = defaultColor.setAlpha(0.87).toRgbString();
var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
//var mdDarkIcon = mdDarkSecondary;
var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
var cssString = 'table.md-table th.md-column {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'table.md-table th.md-column md-icon.md-sort-icon {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'table.md-table td.md-cell {\n'+
'color: ' + mdDark + ';\n'+
'border-top: 1px '+mdDarkDivider+' solid;\n'+
'}\n'+
'table.md-table td.md-cell.md-placeholder {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.md-table-pagination {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'border-top: 1px '+mdDarkDivider+' solid;\n'+
'}\n'+
'.md-table-pagination .buttons md-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}';
var cssParser = new cssjs();
cssParser.testMode = false;
var namespace = 'ts-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$element.addClass(namespace);
function hashCode(str) {
var hash = 0;
var i, char;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
}
function enterFilterMode () {
vm.query.search = '';
}
function exitFilterMode () {
vm.query.search = null;
updateAlarms();
}
function onReorder () {
updateAlarms();
}
function onPaginate () {
updateAlarms();
}
function updateAlarms(preserveSelections) {
if (!preserveSelections) {
vm.selectedAlarms = [];
}
var result = $filter('orderBy')(vm.allAlarms, vm.query.order);
if (vm.query.search != null) {
result = $filter('filter')(result, {$: vm.query.search});
}
vm.alarmsCount = result.length;
var startIndex = vm.query.limit * (vm.query.page - 1);
vm.alarms = result.slice(startIndex, startIndex + vm.query.limit);
if (preserveSelections) {
var newSelectedAlarms = [];
if (vm.selectedAlarms && vm.selectedAlarms.length) {
var i = vm.selectedAlarms.length;
while (i--) {
var selectedAlarm = vm.selectedAlarms[i];
if (selectedAlarm.id) {
result = $filter('filter')(vm.alarms, {id: {id: selectedAlarm.id.id} });
if (result && result.length) {
newSelectedAlarms.push(result[0]);
}
}
}
}
vm.selectedAlarms = newSelectedAlarms;
}
}
function cellStyle(alarm, key) {
var style = {};
if (alarm && key) {
var styleInfo = vm.stylesInfo[key.label];
var value = getAlarmValue(alarm, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
style = styleInfo.cellStyleFunction(value);
} catch (e) {
style = {};
}
} else {
style = defaultStyle(key, value);
}
}
return style;
}
function cellContent(alarm, key) {
var strContent = '';
if (alarm && key) {
var contentInfo = vm.contentsInfo[key.label];
var value = getAlarmValue(alarm, key);
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
if (angular.isDefined(value)) {
strContent = '' + value;
}
var content = strContent;
try {
content = contentInfo.cellContentFunction(value, alarm, $filter);
} catch (e) {
content = strContent;
}
} else {
content = defaultContent(key, value);
}
return content;
} else {
return strContent;
}
}
function defaultContent(key, value) {
if (angular.isDefined(value)) {
var alarmField = types.alarmFields[key.name];
if (alarmField) {
if (alarmField.time) {
return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss');
} else if (alarmField.value == types.alarmFields.severity.value) {
return $translate.instant(types.alarmSeverity[value].name);
} else if (alarmField.value == types.alarmFields.status.value) {
return $translate.instant('alarm.display-status.'+value);
} else if (alarmField.value == types.alarmFields.originatorType.value) {
return $translate.instant(types.entityTypeTranslations[value].type);
}
else {
return value;
}
} else {
return '';
}
} else {
return '';
}
}
function defaultStyle(key, value) {
if (angular.isDefined(value)) {
var alarmField = types.alarmFields[key.name];
if (alarmField) {
if (alarmField.value == types.alarmFields.severity.value) {
return {
fontWeight: 'bold',
color: types.alarmSeverity[value].color
};
} else {
return {};
}
}
} else {
return {};
}
}
const getDescendantProp = (obj, path) => (
path.split('.').reduce((acc, part) => acc && acc[part], obj)
);
function getAlarmValue(alarm, key) {
var alarmField = types.alarmFields[key.name];
if (alarmField) {
return getDescendantProp(alarm, alarmField.value);
} else {
return getDescendantProp(alarm, key.name);
}
}
function updateAlarmSource() {
if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
vm.alarmsTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.settings.alarmsTitle);
}
vm.stylesInfo = {};
vm.contentsInfo = {};
for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
var dataKey = vm.alarmSource.dataKeys[d];
var keySettings = dataKey.settings;
var cellStyleFunction = null;
var useCellStyleFunction = false;
if (keySettings.useCellStyleFunction === true) {
if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
try {
cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
useCellStyleFunction = true;
} catch (e) {
cellStyleFunction = null;
useCellStyleFunction = false;
}
}
}
vm.stylesInfo[dataKey.label] = {
useCellStyleFunction: useCellStyleFunction,
cellStyleFunction: cellStyleFunction
};
var cellContentFunction = null;
var useCellContentFunction = false;
if (keySettings.useCellContentFunction === true) {
if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
try {
cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction);
useCellContentFunction = true;
} catch (e) {
cellContentFunction = null;
useCellContentFunction = false;
}
}
}
vm.contentsInfo[dataKey.label] = {
useCellContentFunction: useCellContentFunction,
cellContentFunction: cellContentFunction
};
}
}
}

34
ui/src/app/widget/lib/alarms-table-widget.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-alarms-table {
margin-top: 15px;
&.tb-data-table {
table.md-table {
tbody {
tr {
td {
&.ag-action-cell {
min-width: 40px;
max-width: 40px;
width: 40px;
}
}
}
}
}
}
}

116
ui/src/app/widget/lib/alarms-table-widget.tpl.html

@ -0,0 +1,116 @@
<!--
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">
<span>{{ vm.alarmsTitle }}</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
<md-icon>search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<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 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 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 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.label }}</span></th>
<th md-column><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 ng-repeat="key in vm.alarmSource.dataKeys"
ng-style="vm.cellStyle(alarm, key)"
ng-bind-html="vm.cellContent(alarm, key)">
</td>
<td md-cell 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 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);
}

Loading…
Cancel
Save