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; .name;
/*@ngInject*/ /*@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 = { var service = {
getAlarm: getAlarm, getAlarm: getAlarm,
getAlarmInfo: getAlarmInfo, getAlarmInfo: getAlarmInfo,
@ -27,7 +49,9 @@ function AlarmService($http, $q, $interval, $filter) {
clearAlarm: clearAlarm, clearAlarm: clearAlarm,
getAlarms: getAlarms, getAlarms: getAlarms,
pollAlarms: pollAlarms, pollAlarms: pollAlarms,
cancelPollAlarms: cancelPollAlarms cancelPollAlarms: cancelPollAlarms,
subscribeForAlarms: subscribeForAlarms,
unsubscribeFromAlarms: unsubscribeFromAlarms
} }
return service; return service;
@ -171,12 +195,21 @@ function AlarmService($http, $q, $interval, $filter) {
pageLink = { pageLink = {
limit: alarmsQuery.limit limit: alarmsQuery.limit
}; };
} else { } else if (alarmsQuery.interval) {
pageLink = { pageLink = {
limit: 100, limit: 100,
startTime: time - alarmsQuery.interval 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); fetchAlarms(alarmsQuery, pageLink, deferred);
return deferred.promise; 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. * limitations under the License.
*/ */
const varsRegex = /\$\{([^\}]*)\}/g;
export default class AliasController { export default class AliasController {
constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) { 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(); var deferred = this.$q.defer();
if (datasource.type === this.types.datasourceType.entity) { if (datasource.type === this.types.datasourceType.entity) {
if (datasource.entityAliasId) { if (datasource.entityAliasId) {
this.getAliasInfo(datasource.entityAliasId).then( this.getAliasInfo(datasource.entityAliasId).then(
function success(aliasInfo) { function success(aliasInfo) {
datasource.aliasName = aliasInfo.alias; datasource.aliasName = aliasInfo.alias;
if (aliasInfo.resolveMultiple) { if (aliasInfo.resolveMultiple && !isSingle) {
var newDatasource; var newDatasource;
var resolvedEntities = aliasInfo.resolvedEntities; var resolvedEntities = aliasInfo.resolvedEntities;
if (resolvedEntities && resolvedEntities.length) { if (resolvedEntities && resolvedEntities.length) {
@ -178,30 +176,44 @@ export default class AliasController {
return deferred.promise; 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) { resolveDatasources(datasources) {
var aliasCtrl = this;
function updateDataKeyLabel(dataKey, datasource) { function updateDataKeyLabel(dataKey, datasource) {
if (!dataKey.pattern) { if (!dataKey.pattern) {
dataKey.pattern = angular.copy(dataKey.label); dataKey.pattern = angular.copy(dataKey.label);
} }
var pattern = dataKey.pattern; dataKey.label = aliasCtrl.utils.createLabelFromDatasource(datasource, 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;
} }
function updateDatasourceKeyLabels(datasource) { function updateDatasourceKeyLabels(datasource) {
@ -213,7 +225,7 @@ export default class AliasController {
var deferred = this.$q.defer(); var deferred = this.$q.defer();
var newDatasources = angular.copy(datasources); var newDatasources = angular.copy(datasources);
var datasorceResolveTasks = []; var datasorceResolveTasks = [];
var aliasCtrl = this;
newDatasources.forEach(function (datasource) { newDatasources.forEach(function (datasource) {
var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource); var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource);
datasorceResolveTasks.push(resolveDatasourceTask); datasorceResolveTasks.push(resolveDatasourceTask);

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

@ -64,6 +64,39 @@ export default class Subscription {
deferred.resolve(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 { } else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
@ -132,6 +165,44 @@ export default class Subscription {
return deferred.promise; 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() { initDataSubscription() {
var deferred = this.ctx.$q.defer(); var deferred = this.ctx.$q.defer();
if (!this.ctx.aliasController) { if (!this.ctx.aliasController) {
@ -393,6 +464,8 @@ export default class Subscription {
onAliasesChanged(aliasIds) { onAliasesChanged(aliasIds) {
if (this.type === this.ctx.types.widgetType.rpc.value) { if (this.type === this.ctx.types.widgetType.rpc.value) {
return this.checkRpcTarget(aliasIds); return this.checkRpcTarget(aliasIds);
} else if (this.type === this.ctx.types.widgetType.alarm.value) {
return this.checkAlarmSource(aliasIds);
} else { } else {
return this.checkSubscriptions(aliasIds); 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) { updateLegend(dataIndex, data, apply) {
var dataKey = this.legendData.keys[dataIndex].dataKey; var dataKey = this.legendData.keys[dataIndex].dataKey;
var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; 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) { if (this.type === this.ctx.types.widgetType.rpc.value) {
return; 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(); this.notifyDataLoading();
if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { if (this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) { if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated(); this.onDataUpdated();
} }
} }
var index = 0; var subscription = this;
for (var i = 0; i < this.datasources.length; i++) { this.alarmSourceListener = {
var datasource = this.datasources[i]; subscriptionTimewindow: this.subscriptionTimewindow,
if (angular.isFunction(datasource)) alarmSource: this.alarmSource,
continue; alarmsUpdated: function(alarms, apply) {
subscription.alarmsUpdated(alarms, apply);
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 = [];
} }
}
this.alarms = [];
index += datasource.dataKeys.length; this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener);
this.datasourceListeners.push(listener); if (this.alarmSource.unresolvedStateEntity) {
this.ctx.datasourceService.subscribeToDatasource(listener); this.notifyDataLoaded();
if (datasource.unresolvedStateEntity) { this.onDataUpdated();
this.notifyDataLoaded();
this.onDataUpdated();
}
} }
} }
unsubscribe() { unsubscribe() {
if (this.type !== this.ctx.types.widgetType.rpc.value) { if (this.type !== this.ctx.types.widgetType.rpc.value) {
for (var i = 0; i < this.datasourceListeners.length; i++) { if (this.type == this.ctx.types.widgetType.alarm.value) {
var listener = this.datasourceListeners[i]; this.alarmsUnsubscribe();
this.ctx.datasourceService.unsubscribeFromDatasource(listener); } 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) { checkSubscriptions(aliasIds) {
var subscriptionsChanged = false; var subscriptionsChanged = false;
for (var i = 0; i < this.datasourceListeners.length; i++) { 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 thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import TbFlot from '../widget/lib/flot-widget'; import TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@ -33,7 +34,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service'; import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardTypes, thingsboardUtils]) thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils])
.factory('widgetService', WidgetService) .factory('widgetService', WidgetService)
.name; .name;

73
ui/src/app/common/types.constant.js

@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', [])
name: "aggregation.none" 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: { alarmStatus: {
activeUnack: "ACTIVE_UNACK", activeUnack: "ACTIVE_UNACK",
activeAck: "ACTIVE_ACK", activeAck: "ACTIVE_ACK",
@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', [])
alarmSeverity: { alarmSeverity: {
"CRITICAL": { "CRITICAL": {
name: "alarm.severity-critical", name: "alarm.severity-critical",
class: "tb-critical" class: "tb-critical",
color: "red"
}, },
"MAJOR": { "MAJOR": {
name: "alarm.severity-major", name: "alarm.severity-major",
class: "tb-major" class: "tb-major",
color: "orange"
}, },
"MINOR": { "MINOR": {
name: "alarm.severity-minor", name: "alarm.severity-minor",
class: "tb-minor" class: "tb-minor",
color: "#ffca3d"
}, },
"WARNING": { "WARNING": {
name: "alarm.severity-warning", name: "alarm.severity-warning",
class: "tb-warning" class: "tb-warning",
color: "#abab00"
}, },
"INDETERMINATE": { "INDETERMINATE": {
name: "alarm.severity-indeterminate", name: "alarm.severity-indeterminate",
class: "tb-indeterminate" class: "tb-indeterminate",
color: "green"
} }
}, },
aliasFilterType: { aliasFilterType: {
@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', [])
dataKeyType: { dataKeyType: {
timeseries: "timeseries", timeseries: "timeseries",
attribute: "attribute", attribute: "attribute",
function: "function" function: "function",
alarm: "alarm"
}, },
componentType: { componentType: {
filter: "FILTER", filter: "FILTER",
@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', [])
alias: "basic_gpio_control" alias: "basic_gpio_control"
} }
}, },
alarm: {
value: "alarm",
name: "widget.alarm",
template: {
bundleAlias: "alarm_widgets",
alias: "alarms_table"
}
},
static: { static: {
value: "static", value: "static",
name: "widget.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) .factory('utils', Utils)
.name; .name;
const varsRegex = /\$\{([^\}]*)\}/g;
/*@ngInject*/ /*@ngInject*/
function Utils($mdColorPalette, $rootScope, $window, types) { function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
var predefinedFunctions = {}, var predefinedFunctions = {},
predefinedFunctionsList = [], predefinedFunctionsList = [],
@ -93,9 +95,32 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
dataKeys: [angular.copy(defaultDataKey)] 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 = { var service = {
getDefaultDatasource: getDefaultDatasource, getDefaultDatasource: getDefaultDatasource,
getDefaultDatasourceJson: getDefaultDatasourceJson, getDefaultDatasourceJson: getDefaultDatasourceJson,
getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
getMaterialColor: getMaterialColor, getMaterialColor: getMaterialColor,
getPredefinedFunctionBody: getPredefinedFunctionBody, getPredefinedFunctionBody: getPredefinedFunctionBody,
getPredefinedFunctionsList: getPredefinedFunctionsList, getPredefinedFunctionsList: getPredefinedFunctionsList,
@ -109,7 +134,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
cleanCopy: cleanCopy, cleanCopy: cleanCopy,
isLocalUrl: isLocalUrl, isLocalUrl: isLocalUrl,
validateDatasources: validateDatasources, validateDatasources: validateDatasources,
createKey: createKey createKey: createKey,
createLabelFromDatasource: createLabelFromDatasource
} }
return service; return service;
@ -212,6 +238,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
return angular.toJson(getDefaultDatasource(dataKeySchema)); return angular.toJson(getDefaultDatasource(dataKeySchema));
} }
function getDefaultAlarmDataKeys() {
return angular.copy(defaultAlarmDataKeys);
}
function isDescriptorSchemaNotEmpty(descriptor) { function isDescriptorSchemaNotEmpty(descriptor) {
if (descriptor && descriptor.schema && descriptor.schema.properties) { if (descriptor && descriptor.schema && descriptor.schema.properties) {
for(var prop in descriptor.schema.properties) { for(var prop in descriptor.schema.properties) {
@ -357,4 +387,24 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
return dataKey; 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.dropWidgetShadow = dropWidgetShadow;
vm.enableWidgetFullscreen = enableWidgetFullscreen; vm.enableWidgetFullscreen = enableWidgetFullscreen;
vm.hasTimewindow = hasTimewindow; vm.hasTimewindow = hasTimewindow;
vm.hasAggregation = hasAggregation;
vm.editWidget = editWidget; vm.editWidget = editWidget;
vm.exportWidget = exportWidget; vm.exportWidget = exportWidget;
vm.removeWidget = removeWidget; vm.removeWidget = removeWidget;
@ -771,7 +772,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
} }
function hasTimewindow(widget) { 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) ? return angular.isDefined(widget.config.useDashboardTimewindow) ?
!widget.config.useDashboardTimewindow : false; !widget.config.useDashboardTimewindow : false;
} else { } else {
@ -779,6 +780,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
} }
} }
function hasAggregation(widget) {
return widget.type === types.widgetType.timeseries.value;
}
function adoptMaxRows() { function adoptMaxRows() {
if (vm.widgets) { if (vm.widgets) {
var maxRows = vm.gridsterOpts.maxRows; var maxRows = vm.gridsterOpts.maxRows;

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

@ -47,7 +47,7 @@
padding: vm.widgetPadding(widget)}"> padding: vm.widgetPadding(widget)}">
<div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(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> <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>
<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" tb-mousedown="$event.stopPropagation()">
<md-button id="expand-button" <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.ngModelCtrl = ngModelCtrl;
scope.types = types; scope.types = types;
scope.alarmFields = [];
for (var alarmField in types.alarmFields) {
scope.alarmFields.push(alarmField);
}
scope.selectedTimeseriesDataKey = null; scope.selectedTimeseriesDataKey = null;
scope.timeseriesDataKeySearchText = null; scope.timeseriesDataKeySearchText = null;
scope.selectedAttributeDataKey = null; scope.selectedAttributeDataKey = null;
scope.attributeDataKeySearchText = null; scope.attributeDataKeySearchText = null;
scope.selectedAlarmDataKey = null;
scope.alarmDataKeySearchText = null;
scope.updateValidity = function () { scope.updateValidity = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
}); });
scope.$watch('timeseriesDataKeys', function () { scope.$watch('timeseriesDataKeys', function () {
if (ngModelCtrl.$viewValue) { updateDataKeys();
var dataKeys = [];
dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
dataKeys = dataKeys.concat(scope.attributeDataKeys);
ngModelCtrl.$viewValue.dataKeys = dataKeys;
scope.updateValidity();
}
}, true); }, true);
scope.$watch('attributeDataKeys', function () { scope.$watch('attributeDataKeys', function () {
updateDataKeys();
}, true);
scope.$watch('alarmDataKeys', function () {
updateDataKeys();
}, true);
function updateDataKeys() {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var dataKeys = []; var dataKeys = [];
dataKeys = dataKeys.concat(scope.timeseriesDataKeys); dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
dataKeys = dataKeys.concat(scope.attributeDataKeys); dataKeys = dataKeys.concat(scope.attributeDataKeys);
dataKeys = dataKeys.concat(scope.alarmDataKeys);
ngModelCtrl.$viewValue.dataKeys = dataKeys; ngModelCtrl.$viewValue.dataKeys = dataKeys;
scope.updateValidity(); scope.updateValidity();
} }
}, true); }
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
} }
var timeseriesDataKeys = []; var timeseriesDataKeys = [];
var attributeDataKeys = []; var attributeDataKeys = [];
var alarmDataKeys = [];
for (var d in ngModelCtrl.$viewValue.dataKeys) { for (var d in ngModelCtrl.$viewValue.dataKeys) {
var dataKey = ngModelCtrl.$viewValue.dataKeys[d]; var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
if (dataKey.type === types.dataKeyType.timeseries) { if (dataKey.type === types.dataKeyType.timeseries) {
timeseriesDataKeys.push(dataKey); timeseriesDataKeys.push(dataKey);
} else if (dataKey.type === types.dataKeyType.attribute) { } else if (dataKey.type === types.dataKeyType.attribute) {
attributeDataKeys.push(dataKey); attributeDataKeys.push(dataKey);
} else if (dataKey.type === types.dataKeyType.alarm) {
alarmDataKeys.push(dataKey);
} }
} }
scope.timeseriesDataKeys = timeseriesDataKeys; scope.timeseriesDataKeys = timeseriesDataKeys;
scope.attributeDataKeys = attributeDataKeys; scope.attributeDataKeys = attributeDataKeys;
scope.alarmDataKeys = alarmDataKeys;
} }
}; };
@ -135,6 +150,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') { if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : ''; scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
} }
if (!scope.alarmDataKeySearchText || scope.alarmDataKeySearchText === '') {
scope.alarmDataKeySearchText = scope.alarmDataKeySearchText === '' ? null : '';
}
}; };
scope.transformTimeseriesDataKeyChip = function (chip) { 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}); 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) { scope.showColorPicker = function (event, dataKey) {
$mdColorPicker.show({ $mdColorPicker.show({
value: dataKey.color, value: dataKey.color,
@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
scope.timeseriesDataKeys[index] = dataKey; scope.timeseriesDataKeys[index] = dataKey;
} else if (dataKey.type === types.dataKeyType.attribute) { } else if (dataKey.type === types.dataKeyType.attribute) {
scope.attributeDataKeys[index] = dataKey; scope.attributeDataKeys[index] = dataKey;
} else if (dataKey.type === types.dataKeyType.alarm) {
scope.alarmDataKeys[index] = dataKey;
} }
ngModelCtrl.$setDirty(); ngModelCtrl.$setDirty();
}, function () { }, function () {
@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
}; };
scope.dataKeysSearch = function (searchText, type) { scope.dataKeysSearch = function (searchText, type) {
if (scope.entityAlias) { if (scope.widgetType == types.widgetType.alarm.value) {
var deferred = $q.defer(); var dataKeys = searchText ? scope.alarmFields.filter(
scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type}) scope.createFilterForDataKey(searchText)) : scope.alarmFields;
.then(function (dataKeys) { return dataKeys;
deferred.resolve(dataKeys);
}, function (e) {
deferred.reject(e);
});
return deferred.promise;
} else { } 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) { scope.createKey = function (event, chipsId) {
var chipsChild = $(chipsId, element)[0].firstElementChild; var chipsChild = $(chipsId, element)[0].firstElementChild;
var el = angular.element(chipsChild); var el = angular.element(chipsChild);

52
ui/src/app/components/datasource-entity.tpl.html

@ -24,7 +24,7 @@
</tb-entity-alias-select> </tb-entity-alias-select>
<section flex layout='column'> <section flex layout='column'>
<section flex layout='column' layout-align="center" style="padding-left: 4px;"> <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" id="timeseries_datakey_chips"
ng-required="true" ng-required="true"
ng-model="timeseriesDataKeys" md-autocomplete-snap ng-model="timeseriesDataKeys" md-autocomplete-snap
@ -128,10 +128,60 @@
</div> </div>
</md-chip-template> </md-chip-template>
</md-chips> </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> </section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> <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.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.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> </div>
</section> </section>
</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); element.html(template);
scope.ngModelCtrl = ngModelCtrl; scope.ngModelCtrl = ngModelCtrl;
scope.types = types;
scope.functionTypes = utils.getPredefinedFunctionsList(); scope.functionTypes = utils.getPredefinedFunctionsList();
scope.alarmFields = [];
for (var alarmField in types.alarmFields) {
scope.alarmFields.push(alarmField);
}
scope.selectedDataKey = null; scope.selectedDataKey = null;
scope.dataKeySearchText = null; scope.dataKeySearchText = null;
scope.selectedAlarmDataKey = null;
scope.alarmDataKeySearchText = null;
scope.updateValidity = function () { scope.updateValidity = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
var dataValid = angular.isDefined(value) && value != null; var dataValid = angular.isDefined(value) && value != null;
ngModelCtrl.$setValidity('deviceData', dataValid); ngModelCtrl.$setValidity('deviceData', dataValid);
if (dataValid) { if (dataValid) {
ngModelCtrl.$setValidity('funcTypes', ngModelCtrl.$setValidity('datasourceKeys',
angular.isDefined(value.dataKeys) && angular.isDefined(value.dataKeys) &&
value.dataKeys != null && value.dataKeys != null &&
value.dataKeys.length > 0); value.dataKeys.length > 0);
@ -63,13 +72,22 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
}; };
scope.$watch('funcDataKeys', function () { scope.$watch('funcDataKeys', function () {
updateDataKeys();
}, true);
scope.$watch('alarmDataKeys', function () {
updateDataKeys();
}, true);
function updateDataKeys() {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var dataKeys = []; var dataKeys = [];
dataKeys = dataKeys.concat(scope.funcDataKeys); dataKeys = dataKeys.concat(scope.funcDataKeys);
dataKeys = dataKeys.concat(scope.alarmDataKeys);
ngModelCtrl.$viewValue.dataKeys = dataKeys; ngModelCtrl.$viewValue.dataKeys = dataKeys;
scope.updateValidity(); scope.updateValidity();
} }
}, true); }
scope.$watch('datasourceName', function () { scope.$watch('datasourceName', function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
@ -81,18 +99,31 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var funcDataKeys = []; var funcDataKeys = [];
var alarmDataKeys = [];
if (ngModelCtrl.$viewValue.dataKeys) { 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.funcDataKeys = funcDataKeys;
scope.alarmDataKeys = alarmDataKeys;
scope.datasourceName = ngModelCtrl.$viewValue.name; scope.datasourceName = ngModelCtrl.$viewValue.name;
} }
}; };
scope.transformDataKeyChip = function (chip) { scope.transformFuncDataKeyChip = function (chip) {
return scope.generateDataKey({chip: chip, type: types.dataKeyType.function}); 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) { scope.showColorPicker = function (event, dataKey) {
$mdColorPicker.show({ $mdColorPicker.show({
value: dataKey.color, value: dataKey.color,
@ -129,7 +160,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
dataKey: angular.copy(dataKey), dataKey: angular.copy(dataKey),
dataKeySettingsSchema: scope.datakeySettingsSchema, dataKeySettingsSchema: scope.datakeySettingsSchema,
entityAlias: null, entityAlias: null,
entityAliases: null aliasController: null
}, },
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
fullscreen: true, fullscreen: true,
@ -140,7 +171,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
w.triggerHandler('resize'); w.triggerHandler('resize');
} }
}).then(function (dataKey) { }).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(); ngModelCtrl.$setDirty();
}, function () { }, function () {
}); });
@ -151,8 +186,9 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
} }
scope.dataKeysSearch = function (dataKeySearchText) { scope.dataKeysSearch = function (dataKeySearchText) {
var dataKeys = dataKeySearchText ? scope.functionTypes.filter( var targetKeys = scope.widgetType == types.widgetType.alarm.value ? scope.alarmFields : scope.functionTypes;
scope.createFilterForDataKey(dataKeySearchText)) : scope.functionTypes; var dataKeys = dataKeySearchText ? targetKeys.filter(
scope.createFilterForDataKey(dataKeySearchText)) : targetKeys;
return dataKeys; return dataKeys;
}; };
@ -180,6 +216,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
restrict: "E", restrict: "E",
require: "^ngModel", require: "^ngModel",
scope: { scope: {
widgetType: '=',
generateDataKey: '&', generateDataKey: '&',
datakeySettingsSchema: '=' datakeySettingsSchema: '='
}, },

59
ui/src/app/components/datasource-func.tpl.html

@ -17,18 +17,19 @@
--> -->
<section class="tb-datasource-func" flex layout='column' <section class="tb-datasource-func" flex layout='column'
layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center"> 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" <input name="datasourceName"
placeholder="{{ 'datasource.name' | translate }}" placeholder="{{ 'datasource.name' | translate }}"
ng-model="datasourceName" ng-model="datasourceName"
aria-label="{{ 'datasource.name' | translate }}"> aria-label="{{ 'datasource.name' | translate }}">
</md-input-container> </md-input-container>
<section flex layout='column' style="padding-left: 4px;"> <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" id="function_datakey_chips"
ng-required="true" ng-required="true"
ng-model="funcDataKeys" md-autocomplete-snap ng-model="funcDataKeys" md-autocomplete-snap
md-transform-chip="transformDataKeyChip($chip)" md-transform-chip="transformFuncDataKeyChip($chip)"
md-require-match="false"> md-require-match="false">
<md-autocomplete <md-autocomplete
md-no-cache="false" md-no-cache="false"
@ -75,8 +76,58 @@
</div> </div>
</md-chip-template> </md-chip-template>
</md-chips> </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 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> </div>
</section> </section>
</section> </section>

13
ui/src/app/components/datasource.directive.js

@ -30,7 +30,7 @@ export default angular.module('thingsboard.directives.datasource', [thingsboardT
.name; .name;
/*@ngInject*/ /*@ngInject*/
function Datasource($compile, $templateCache, types) { function Datasource($compile, $templateCache, utils, types) {
var linker = function (scope, element, attrs, ngModelCtrl) { var linker = function (scope, element, attrs, ngModelCtrl) {
@ -53,8 +53,12 @@ function Datasource($compile, $templateCache, types) {
} }
scope.$watch('model.type', function (newType, prevType) { scope.$watch('model.type', function (newType, prevType) {
if (newType != prevType) { if (newType && prevType && newType != prevType) {
scope.model.dataKeys = []; 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); }, true);
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
scope.model = {};
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
scope.model = ngModelCtrl.$viewValue; scope.model = ngModelCtrl.$viewValue;
} else {
scope.model = {};
} }
}; };

1
ui/src/app/components/datasource.tpl.html

@ -29,6 +29,7 @@
ng-model="model" ng-model="model"
datakey-settings-schema="datakeySettingsSchema" datakey-settings-schema="datakeySettingsSchema"
ng-required="model.type === types.datasourceType.function" ng-required="model.type === types.datasourceType.function"
widget-type="widgetType"
generate-data-key="generateDataKey({chip: chip, type: type})"> generate-data-key="generateDataKey({chip: chip, type: type})">
</tb-datasource-func> </tb-datasource-func>
<tb-datasource-entity flex <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.historyOnly = angular.isDefined(attrs.historyOnly);
scope.aggregation = angular.isDefined(attrs.aggregation); scope.aggregation = scope.$eval(attrs.aggregation);
scope.isToolbar = angular.isDefined(attrs.isToolbar); 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; .name;
/*@ngInject*/ /*@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) { var linker = function (scope, element, attrs, ngModelCtrl) {
@ -87,6 +87,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
value: null value: null
} }
scope.alarmSource = {
value: null
}
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var config = ngModelCtrl.$viewValue.config; var config = ngModelCtrl.$viewValue.config;
@ -113,7 +117,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.showLegend = angular.isDefined(config.showLegend) ? scope.showLegend = angular.isDefined(config.showLegend) ?
config.showLegend : scope.widgetType === types.widgetType.timeseries.value; config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
scope.legendConfig = config.legendConfig; 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) { && scope.isDataEnabled) {
if (scope.datasources) { if (scope.datasources) {
scope.datasources.splice(0, scope.datasources.length); scope.datasources.splice(0, scope.datasources.length);
@ -137,6 +143,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
} else { } else {
scope.targetDeviceAlias.value = null; 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; 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) { if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0;
ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); 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) { } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
valid = config && config.datasources && config.datasources.length > 0; valid = config && config.datasources && config.datasources.length > 0;
ngModelCtrl.$setValidity('datasources', valid); ngModelCtrl.$setValidity('datasources', valid);
@ -253,7 +268,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true); }, true);
scope.$watch('datasources', function () { 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) { && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
var config = value.config; 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 () { scope.addDatasource = function () {
var newDatasource; var newDatasource;
if (scope.functionsOnly) { if (scope.functionsOnly) {
@ -320,10 +351,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
return chip; 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 = { var result = {
name: chip, name: chip,
type: type, type: type,
label: scope.genNextLabel(chip), label: label,
color: scope.genNextColor(), color: scope.genNextColor(),
settings: {}, settings: {},
_hash: Math.random() _hash: Math.random()
@ -351,15 +391,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
var matches = false; var matches = false;
do { do {
matches = false; matches = false;
if (value.config.datasources) { var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
for (var d=0;d<value.config.datasources.length;d++) { if (datasources) {
var datasource = value.config.datasources[d]; for (var d=0;d<datasources.length;d++) {
for (var k=0;k<datasource.dataKeys.length;k++) { var datasource = datasources[d];
var dataKey = datasource.dataKeys[k]; if (datasource && datasource.dataKeys) {
if (dataKey.label === label) { for (var k = 0; k < datasource.dataKeys.length; k++) {
i++; var dataKey = datasource.dataKeys[k];
label = name + ' ' + i; if (dataKey.label === label) {
matches = true; i++;
label = name + ' ' + i;
matches = true;
}
} }
} }
} }
@ -371,10 +414,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.genNextColor = function () { scope.genNextColor = function () {
var i = 0; var i = 0;
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
if (value.config.datasources) { var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
for (var d=0;d<value.config.datasources.length;d++) { if (datasources) {
var datasource = value.config.datasources[d]; for (var d=0;d<datasources.length;d++) {
i += datasource.dataKeys.length; var datasource = datasources[d];
if (datasource && datasource.dataKeys) {
i += datasource.dataKeys.length;
}
} }
} }
return utils.getMaterialColor(i); return utils.getMaterialColor(i);

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

@ -20,18 +20,22 @@
<md-tab label="{{ 'widget-config.data' | translate }}" <md-tab label="{{ 'widget-config.data' | translate }}"
ng-show="widgetType !== types.widgetType.static.value"> ng-show="widgetType !== types.widgetType.static.value">
<md-content class="md-padding" layout="column"> <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"> layout-gt-sm='row' layout-align-gt-sm="start center">
<md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}" <md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}"
ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }} ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }}
</md-checkbox> </md-checkbox>
<section flex layout="row" layout-align="start center" style="margin-bottom: 16px;"> <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> <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> </section>
</div> </div>
<v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" <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 id="datasources-pane" expanded="true">
<v-pane-header> <v-pane-header>
{{ 'widget-config.datasources' | translate }} {{ 'widget-config.datasources' | translate }}
@ -112,6 +116,24 @@
</v-pane-content> </v-pane-content>
</v-pane> </v-pane>
</v-accordion> </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-content>
</md-tab> </md-tab>
<md-tab label="{{ 'widget-config.settings' | translate }}"> <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*/ /*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, 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) { dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
var vm = this; var vm = this;
@ -44,7 +44,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var widgetContext = { var widgetContext = {
inited: false, inited: false,
$scope: $scope,
$container: null, $container: null,
$containerParent: null, $containerParent: null,
width: 0, width: 0,
@ -113,6 +112,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
timeService: timeService, timeService: timeService,
deviceService: deviceService, deviceService: deviceService,
datasourceService: datasourceService, datasourceService: datasourceService,
alarmService: alarmService,
utils: utils, utils: utils,
widgetUtils: widgetContext.utils, widgetUtils: widgetContext.utils,
dashboardTimewindowApi: dashboardTimewindowApi, dashboardTimewindowApi: dashboardTimewindowApi,
@ -285,9 +285,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var deferred = $q.defer(); var deferred = $q.defer();
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
options = { options = {
type: widget.type, type: widget.type
datasources: angular.copy(widget.config.datasources) }
}; if (widget.type == types.widgetType.alarm.value) {
options.alarmSource = angular.copy(widget.config.alarmSource);
} else {
options.datasources = angular.copy(widget.config.datasources)
}
defaultComponentsOptions(options); defaultComponentsOptions(options);
createSubscription(options).then( createSubscription(options).then(
@ -320,7 +325,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.executingRpcRequest = subscription.executingRpcRequest; $scope.executingRpcRequest = subscription.executingRpcRequest;
}, },
onRpcSuccess: function(subscription) { onRpcSuccess: function(subscription) {
$scope.executingRpcRequest = subscription.executingRpcRequest; $scope.executingRpcRequest = subscription.executingRpcRequest;
$scope.rpcErrorText = subscription.rpcErrorText; $scope.rpcErrorText = subscription.rpcErrorText;
$scope.rpcRejection = subscription.rpcRejection; $scope.rpcRejection = subscription.rpcRejection;
}, },
@ -436,7 +441,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
widgetContext.$container = $('#container', containerElement); widgetContext.$container = $('#container', containerElement);
widgetContext.$containerParent = $(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 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() { function destroyWidgetElement() {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$element.html(''); $element.html('');
if (widgetContext.$scope) {
widgetContext.$scope.$destroy();
}
widgetContext.$container = null; widgetContext.$container = null;
widgetContext.$containerParent = 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'; link = 'widgetsConfigRpc';
break; break;
} }
case types.widgetType.alarm.value: {
link = 'widgetsConfigAlarm';
break;
}
case types.widgetType.static.value: { case types.widgetType.static.value: {
link = 'widgetsConfigStatic'; link = 'widgetsConfigStatic';
break; break;

24
ui/src/app/dashboard/dashboard.controller.js

@ -47,6 +47,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
vm.latestWidgetTypes = []; vm.latestWidgetTypes = [];
vm.timeseriesWidgetTypes = []; vm.timeseriesWidgetTypes = [];
vm.rpcWidgetTypes = []; vm.rpcWidgetTypes = [];
vm.alarmWidgetTypes = [];
vm.staticWidgetTypes = []; vm.staticWidgetTypes = [];
vm.widgetEditMode = $state.$current.data.widgetEditMode; vm.widgetEditMode = $state.$current.data.widgetEditMode;
vm.iframeMode = $rootScope.iframeMode; vm.iframeMode = $rootScope.iframeMode;
@ -263,6 +264,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
vm.latestWidgetTypes = []; vm.latestWidgetTypes = [];
vm.timeseriesWidgetTypes = []; vm.timeseriesWidgetTypes = [];
vm.rpcWidgetTypes = []; vm.rpcWidgetTypes = [];
vm.alarmWidgetTypes = [];
vm.staticWidgetTypes = []; vm.staticWidgetTypes = [];
if (vm.widgetsBundle) { if (vm.widgetsBundle) {
var bundleAlias = vm.widgetsBundle.alias; var bundleAlias = vm.widgetsBundle.alias;
@ -308,6 +310,8 @@ export default function DashboardController(types, utils, dashboardUtils, widget
vm.latestWidgetTypes.push(widget); vm.latestWidgetTypes.push(widget);
} else if (widgetTypeInfo.type === types.widgetType.rpc.value) { } else if (widgetTypeInfo.type === types.widgetType.rpc.value) {
vm.rpcWidgetTypes.push(widget); vm.rpcWidgetTypes.push(widget);
} else if (widgetTypeInfo.type === types.widgetType.alarm.value) {
vm.alarmWidgetTypes.push(widget);
} else if (widgetTypeInfo.type === types.widgetType.static.value) { } else if (widgetTypeInfo.type === types.widgetType.static.value) {
vm.staticWidgetTypes.push(widget); vm.staticWidgetTypes.push(widget);
} }
@ -358,21 +362,6 @@ export default function DashboardController(types, utils, dashboardUtils, widget
vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils, vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils,
types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases); 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() { }, function fail() {
vm.configurationError = true; vm.configurationError = true;
}); });
@ -744,6 +733,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget
link = 'widgetsConfigRpc'; link = 'widgetsConfigRpc';
break; break;
} }
case types.widgetType.alarm.value: {
link = 'widgetsConfigAlarm';
break;
}
case types.widgetType.static.value: { case types.widgetType.static.value: {
link = 'widgetsConfigStatic'; link = 'widgetsConfigStatic';
break; break;
@ -851,6 +844,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
vm.timeseriesWidgetTypes = []; vm.timeseriesWidgetTypes = [];
vm.latestWidgetTypes = []; vm.latestWidgetTypes = [];
vm.rpcWidgetTypes = []; vm.rpcWidgetTypes = [];
vm.alarmWidgetTypes = [];
vm.staticWidgetTypes = []; vm.staticWidgetTypes = [];
} }

19
ui/src/app/dashboard/dashboard.tpl.html

@ -52,7 +52,7 @@
<tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()" <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
is-toolbar is-toolbar
direction="left" direction="left"
tooltip-direction="bottom" aggregation tooltip-direction="bottom" aggregation="true"
ng-model="vm.dashboardCtx.dashboardTimewindow"> ng-model="vm.dashboardCtx.dashboardTimewindow">
</tb-timewindow> </tb-timewindow>
<tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()" <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
@ -179,6 +179,7 @@
<tb-edit-widget <tb-edit-widget
dashboard="vm.dashboard" dashboard="vm.dashboard"
alias-controller="vm.dashboardCtx.aliasController" alias-controller="vm.dashboardCtx.aliasController"
widget-edit-mode="vm.widgetEditMode"
widget="vm.editingWidget" widget="vm.editingWidget"
widget-layout="vm.editingWidgetLayout" widget-layout="vm.editingWidgetLayout"
the-form="vm.widgetForm"> the-form="vm.widgetForm">
@ -205,7 +206,8 @@
</header-pane> </header-pane>
<div ng-if="vm.isAddingWidget"> <div ng-if="vm.isAddingWidget">
<md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 || <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 flex
class="tb-absolute-fill" md-border-bottom> class="tb-absolute-fill" md-border-bottom>
<md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> <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)"> on-widget-clicked="vm.addWidgetFromType(event, widget)">
</tb-dashboard> </tb-dashboard>
</md-tab> </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 }}"> <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard <tb-dashboard
widgets="vm.staticWidgetTypes" widgets="vm.staticWidgetTypes"
@ -250,7 +262,8 @@
</md-tab> </md-tab>
</md-tabs> </md-tabs>
<span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 && <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" layout-align="center center"
style="text-transform: uppercase; display: flex;" style="text-transform: uppercase; display: flex;"
class="md-headline tb-absolute-fill">widgets-bundle.empty</span> 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: { scope: {
dashboard: '=', dashboard: '=',
aliasController: '=', aliasController: '=',
widgetEditMode: '=',
widget: '=', widget: '=',
widgetLayout: '=', widgetLayout: '=',
theForm: '=' theForm: '='

2
ui/src/app/dashboard/edit-widget.tpl.html

@ -22,7 +22,7 @@
widget-settings-schema="settingsSchema" widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema" datakey-settings-schema="dataKeySettingsSchema"
alias-controller="aliasController" alias-controller="aliasController"
functions-only="functionsOnly" functions-only="widgetEditMode"
fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
the-form="theForm"></tb-widget-config> 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", widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries",
widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest", widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest",
widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc",
widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm",
widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static", widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static",
}, },
getPluginLink: function(plugin) { getPluginLink: function(plugin) {

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

@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', [])
"type": "Type", "type": "Type",
"severity": "Severity", "severity": "Severity",
"originator": "Originator", "originator": "Originator",
"originator-type": "Originator type",
"details": "Details", "details": "Details",
"status": "Status", "status": "Status",
"alarm-details": "Alarm details", "alarm-details": "Alarm details",
@ -144,7 +145,10 @@ export default angular.module('thingsboard.locale', [])
"severity-warning": "Warning", "severity-warning": "Warning",
"severity-indeterminate": "Indeterminate", "severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge", "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": { "alias": {
"add": "Add alias", "add": "Add alias",
@ -486,8 +490,10 @@ export default angular.module('thingsboard.locale', [])
"configuration": "Data key configuration", "configuration": "Data key configuration",
"timeseries": "Timeseries", "timeseries": "Timeseries",
"attributes": "Attributes", "attributes": "Attributes",
"alarm": "Alarm fields",
"timeseries-required": "Entity timeseries are required.", "timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes are required.", "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
"alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types", "function-types": "Function types",
"function-types-required": "Function types are required." "function-types-required": "Function types are required."
}, },
@ -1046,6 +1052,7 @@ export default angular.module('thingsboard.locale', [])
"timeseries": "Time series", "timeseries": "Time series",
"latest-values": "Latest values", "latest-values": "Latest values",
"rpc": "Control widget", "rpc": "Control widget",
"alarm": "Alarm widget",
"static": "Static widget", "static": "Static widget",
"select-widget-type": "Select widget type", "select-widget-type": "Select widget type",
"missing-widget-title-error": "Widget title must be specified!", "missing-widget-title-error": "Widget title must be specified!",
@ -1133,7 +1140,8 @@ export default angular.module('thingsboard.locale', [])
"datasource-parameters": "Parameters", "datasource-parameters": "Parameters",
"remove-datasource": "Remove datasource", "remove-datasource": "Remove datasource",
"add-datasource": "Add datasource", "add-datasource": "Add datasource",
"target-device": "Target device" "target-device": "Target device",
"alarm-source": "Alarm source"
}, },
"widget-type": { "widget-type": {
"import": "Import 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> </md-icon>
<span translate>{{vm.types.widgetType.rpc.name}}</span> <span translate>{{vm.types.widgetType.rpc.name}}</span>
</md-button> </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" <md-button class="tb-card-button md-raised md-primary" layout="column"
ng-click="vm.typeSelected(vm.types.widgetType.static.value)"> ng-click="vm.typeSelected(vm.types.widgetType.static.value)">
<md-icon class="material-icons tb-md-96" <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) { $scope.$watch('vm.widget.type', function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) { if (!angular.equals(newVal, oldVal)) {
var config = angular.fromJson(vm.widget.defaultConfig); 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) { if (config.targetDeviceAliases) {
delete config.targetDeviceAliases; delete config.targetDeviceAliases;
} }
if (config.alarmSource) {
delete config.alarmSource;
}
if (!config.datasources) { if (!config.datasources) {
config.datasources = []; config.datasources = [];
} }
@ -346,22 +350,38 @@ export default function WidgetEditorController(widgetService, userService, types
for (var i = 0; i < config.datasources.length; i++) { for (var i = 0; i < config.datasources.length; i++) {
var datasource = config.datasources[i]; var datasource = config.datasources[i];
datasource.type = vm.widget.type; 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) { if (config.datasources) {
delete config.datasources; delete config.datasources;
} }
if (config.alarmSource) {
delete config.alarmSource;
}
if (config.timewindow) { if (config.timewindow) {
delete config.timewindow; delete config.timewindow;
} }
if (!config.targetDeviceAliases) { if (!config.targetDeviceAliases) {
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); vm.widget.defaultConfig = angular.toJson(config);
} }

Loading…
Cancel
Save