Commit f806c392b2f7128878b2847135ffb1fa9676e2cc

Authored by Volodymyr Babak
2 parents 1141a26a c277b6a9

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

Showing 56 changed files with 2081 additions and 284 deletions
... ... @@ -143,4 +143,30 @@ public class AlarmController extends BaseController {
143 143 }
144 144 }
145 145
  146 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
  147 + @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET)
  148 + @ResponseBody
  149 + public AlarmSeverity getHighestAlarmSeverity(
  150 + @PathVariable("entityType") String strEntityType,
  151 + @PathVariable("entityId") String strEntityId,
  152 + @RequestParam(required = false) String searchStatus,
  153 + @RequestParam(required = false) String status
  154 + ) throws ThingsboardException {
  155 + checkParameter("EntityId", strEntityId);
  156 + checkParameter("EntityType", strEntityType);
  157 + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
  158 + AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
  159 + AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
  160 + if (alarmSearchStatus != null && alarmStatus != null) {
  161 + throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " +
  162 + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  163 + }
  164 + checkEntityId(entityId);
  165 + try {
  166 + return alarmService.findHighestAlarmSeverity(entityId, alarmSearchStatus, alarmStatus);
  167 + } catch (Exception e) {
  168 + throw handleException(e);
  169 + }
  170 + }
  171 +
146 172 }
... ...
... ... @@ -16,10 +16,8 @@
16 16 package org.thingsboard.server.dao.alarm;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
19   -import org.thingsboard.server.common.data.alarm.Alarm;
20   -import org.thingsboard.server.common.data.alarm.AlarmId;
21   -import org.thingsboard.server.common.data.alarm.AlarmInfo;
22   -import org.thingsboard.server.common.data.alarm.AlarmQuery;
  19 +import org.thingsboard.server.common.data.alarm.*;
  20 +import org.thingsboard.server.common.data.id.EntityId;
23 21 import org.thingsboard.server.common.data.page.TimePageData;
24 22
25 23 /**
... ... @@ -39,4 +37,7 @@ public interface AlarmService {
39 37
40 38 ListenableFuture<TimePageData<AlarmInfo>> findAlarms(AlarmQuery query);
41 39
  40 + AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus,
  41 + AlarmStatus alarmStatus);
  42 +
42 43 }
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.Tenant;
28 28 import org.thingsboard.server.common.data.alarm.*;
29 29 import org.thingsboard.server.common.data.id.EntityId;
30 30 import org.thingsboard.server.common.data.page.TimePageData;
  31 +import org.thingsboard.server.common.data.page.TimePageLink;
31 32 import org.thingsboard.server.common.data.relation.EntityRelation;
32 33 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
33 34 import org.thingsboard.server.dao.entity.AbstractEntityService;
... ... @@ -45,6 +46,7 @@ import javax.annotation.PostConstruct;
45 46 import javax.annotation.PreDestroy;
46 47 import java.util.ArrayList;
47 48 import java.util.List;
  49 +import java.util.UUID;
48 50 import java.util.concurrent.ExecutionException;
49 51 import java.util.concurrent.ExecutorService;
50 52 import java.util.concurrent.Executors;
... ... @@ -240,6 +242,46 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
240 242 });
241 243 }
242 244
  245 + @Override
  246 + public AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus,
  247 + AlarmStatus alarmStatus) {
  248 + TimePageLink nextPageLink = new TimePageLink(100);
  249 + boolean hasNext = true;
  250 + AlarmSeverity highestSeverity = null;
  251 + AlarmQuery query;
  252 + while (hasNext) {
  253 + query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false);
  254 + List<AlarmInfo> alarms;
  255 + try {
  256 + alarms = alarmDao.findAlarms(query).get();
  257 + } catch (ExecutionException | InterruptedException e) {
  258 + log.warn("Failed to find highest alarm severity. EntityId: [{}], AlarmSearchStatus: [{}], AlarmStatus: [{}]",
  259 + entityId, alarmSearchStatus, alarmStatus);
  260 + throw new RuntimeException(e);
  261 + }
  262 + hasNext = alarms.size() == nextPageLink.getLimit();
  263 + if (hasNext) {
  264 + nextPageLink = new TimePageData<>(alarms, nextPageLink).getNextPageLink();
  265 + }
  266 + if (alarms.isEmpty()) {
  267 + continue;
  268 + } else {
  269 + List<AlarmInfo> sorted = new ArrayList(alarms);
  270 + sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity()));
  271 + AlarmSeverity severity = sorted.get(0).getSeverity();
  272 + if (severity == AlarmSeverity.CRITICAL) {
  273 + highestSeverity = severity;
  274 + break;
  275 + } else if (highestSeverity == null) {
  276 + highestSeverity = severity;
  277 + } else {
  278 + highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity;
  279 + }
  280 + }
  281 + }
  282 + return highestSeverity;
  283 + }
  284 +
243 285 private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
244 286 log.debug("Deleting Alarm relation: {}", alarmRelation);
245 287 relationService.deleteRelation(alarmRelation).get();
... ...
... ... @@ -23,11 +23,14 @@ import './alarm-details-dialog.scss';
23 23 const js_beautify = beautify.js;
24 24
25 25 /*@ngInject*/
26   -export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, alarmService, alarmId, showingCallback) {
  26 +export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types,
  27 + alarmService, alarmId, allowAcknowledgment, allowClear, showingCallback) {
27 28
28 29 var vm = this;
29 30
30 31 vm.alarmId = alarmId;
  32 + vm.allowAcknowledgment = allowAcknowledgment;
  33 + vm.allowClear = allowClear;
31 34 vm.types = types;
32 35 vm.alarm = null;
33 36
... ...
... ... @@ -84,16 +84,16 @@
84 84 </div>
85 85 </md-dialog-content>
86 86 <md-dialog-actions layout="row">
87   - <md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeUnack ||
88   - vm.alarm.status==vm.types.alarmStatus.clearedUnack"
  87 + <md-button ng-if="vm.allowAcknowledgment && (vm.alarm.status==vm.types.alarmStatus.activeUnack ||
  88 + vm.alarm.status==vm.types.alarmStatus.clearedUnack)"
89 89 class="md-raised md-primary"
90 90 ng-disabled="loading"
91 91 ng-click="vm.acknowledge()"
92 92 style="margin-right:20px;">{{ 'alarm.acknowledge' |
93 93 translate }}
94 94 </md-button>
95   - <md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeAck ||
96   - vm.alarm.status==vm.types.alarmStatus.activeUnack"
  95 + <md-button ng-if="vm.allowClear && (vm.alarm.status==vm.types.alarmStatus.activeAck ||
  96 + vm.alarm.status==vm.types.alarmStatus.activeUnack)"
97 97 class="md-raised md-primary"
98 98 ng-disabled="loading"
99 99 ng-click="vm.clear()">{{ 'alarm.clear' |
... ...
... ... @@ -40,7 +40,12 @@ export default function AlarmRowDirective($compile, $templateCache, types, $mdDi
40 40 controller: 'AlarmDetailsDialogController',
41 41 controllerAs: 'vm',
42 42 templateUrl: alarmDetailsDialogTemplate,
43   - locals: {alarmId: scope.alarm.id.id, showingCallback: onShowingCallback},
  43 + locals: {
  44 + alarmId: scope.alarm.id.id,
  45 + allowAcknowledgment: true,
  46 + allowClear: true,
  47 + showingCallback: onShowingCallback
  48 + },
44 49 parent: angular.element($document[0].body),
45 50 targetEvent: $event,
46 51 fullscreen: true,
... ...
... ... @@ -18,7 +18,29 @@ export default angular.module('thingsboard.api.alarm', [])
18 18 .name;
19 19
20 20 /*@ngInject*/
21   -function AlarmService($http, $q, $interval, $filter) {
  21 +function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) {
  22 +
  23 + var alarmSourceListeners = {};
  24 +
  25 + var simulatedAlarm = {
  26 + createdTime: (new Date).getTime(),
  27 + startTs: (new Date).getTime(),
  28 + endTs: 0,
  29 + ackTs: 0,
  30 + clearTs: 0,
  31 + originatorName: 'Simulated',
  32 + originator: {
  33 + entityType: "DEVICE",
  34 + id: "1"
  35 + },
  36 + type: 'TEMPERATURE',
  37 + severity: "MAJOR",
  38 + status: types.alarmStatus.activeUnack,
  39 + details: {
  40 + message: "Temperature is high!"
  41 + }
  42 + };
  43 +
22 44 var service = {
23 45 getAlarm: getAlarm,
24 46 getAlarmInfo: getAlarmInfo,
... ... @@ -26,8 +48,11 @@ function AlarmService($http, $q, $interval, $filter) {
26 48 ackAlarm: ackAlarm,
27 49 clearAlarm: clearAlarm,
28 50 getAlarms: getAlarms,
  51 + getHighestAlarmSeverity: getHighestAlarmSeverity,
29 52 pollAlarms: pollAlarms,
30   - cancelPollAlarms: cancelPollAlarms
  53 + cancelPollAlarms: cancelPollAlarms,
  54 + subscribeForAlarms: subscribeForAlarms,
  55 + unsubscribeFromAlarms: unsubscribeFromAlarms
31 56 }
32 57
33 58 return service;
... ... @@ -141,6 +166,23 @@ function AlarmService($http, $q, $interval, $filter) {
141 166 return deferred.promise;
142 167 }
143 168
  169 + function getHighestAlarmSeverity(entityType, entityId, alarmSearchStatus, alarmStatus, config) {
  170 + var deferred = $q.defer();
  171 + var url = '/api/alarm/highestSeverity/' + entityType + '/' + entityId;
  172 +
  173 + if (alarmSearchStatus) {
  174 + url += '?searchStatus=' + alarmSearchStatus;
  175 + } else if (alarmStatus) {
  176 + url += '?status=' + alarmStatus;
  177 + }
  178 + $http.get(url, config).then(function success(response) {
  179 + deferred.resolve(response.data);
  180 + }, function fail() {
  181 + deferred.reject();
  182 + });
  183 + return deferred.promise;
  184 + }
  185 +
144 186 function fetchAlarms(alarmsQuery, pageLink, deferred, alarmsList) {
145 187 getAlarms(alarmsQuery.entityType, alarmsQuery.entityId,
146 188 pageLink, alarmsQuery.alarmSearchStatus, alarmsQuery.alarmStatus,
... ... @@ -171,12 +213,21 @@ function AlarmService($http, $q, $interval, $filter) {
171 213 pageLink = {
172 214 limit: alarmsQuery.limit
173 215 };
174   - } else {
  216 + } else if (alarmsQuery.interval) {
175 217 pageLink = {
176 218 limit: 100,
177 219 startTime: time - alarmsQuery.interval
178 220 };
  221 + } else if (alarmsQuery.startTime) {
  222 + pageLink = {
  223 + limit: 100,
  224 + startTime: alarmsQuery.startTime
  225 + }
  226 + if (alarmsQuery.endTime) {
  227 + pageLink.endTime = alarmsQuery.endTime;
  228 + }
179 229 }
  230 +
180 231 fetchAlarms(alarmsQuery, pageLink, deferred);
181 232 return deferred.promise;
182 233 }
... ... @@ -211,4 +262,59 @@ function AlarmService($http, $q, $interval, $filter) {
211 262 }
212 263 }
213 264
  265 + function subscribeForAlarms(alarmSourceListener) {
  266 + alarmSourceListener.id = utils.guid();
  267 + alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener;
  268 + var alarmSource = alarmSourceListener.alarmSource;
  269 + if (alarmSource.type == types.datasourceType.function) {
  270 + $timeout(function() {
  271 + alarmSourceListener.alarmsUpdated([simulatedAlarm], false);
  272 + });
  273 + } else if (alarmSource.entityType && alarmSource.entityId) {
  274 + var pollingInterval = alarmSourceListener.alarmsPollingInterval;
  275 + alarmSourceListener.alarmsQuery = {
  276 + entityType: alarmSource.entityType,
  277 + entityId: alarmSource.entityId,
  278 + alarmSearchStatus: alarmSourceListener.alarmSearchStatus,
  279 + alarmStatus: null
  280 + }
  281 + var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'});
  282 + if (originatorKeys && originatorKeys.length) {
  283 + alarmSourceListener.alarmsQuery.fetchOriginator = true;
  284 + }
  285 + var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow;
  286 + if (subscriptionTimewindow.realtimeWindowMs) {
  287 + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs;
  288 + } else {
  289 + alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs;
  290 + alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs;
  291 + }
  292 + alarmSourceListener.alarmsQuery.onAlarms = function(alarms) {
  293 + if (subscriptionTimewindow.realtimeWindowMs) {
  294 + var now = Date.now();
  295 + if (alarmSourceListener.lastUpdateTs) {
  296 + var interval = now - alarmSourceListener.lastUpdateTs;
  297 + alarmSourceListener.alarmsQuery.startTime += interval;
  298 + } else {
  299 + alarmSourceListener.lastUpdateTs = now;
  300 + }
  301 + }
  302 + alarmSourceListener.alarmsUpdated(alarms, false);
  303 + }
  304 + onPollAlarms(alarmSourceListener.alarmsQuery);
  305 + alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval,
  306 + 0, false, alarmSourceListener.alarmsQuery);
  307 + }
  308 +
  309 + }
  310 +
  311 + function unsubscribeFromAlarms(alarmSourceListener) {
  312 + if (alarmSourceListener && alarmSourceListener.id) {
  313 + if (alarmSourceListener.pollPromise) {
  314 + $interval.cancel(alarmSourceListener.pollPromise);
  315 + alarmSourceListener.pollPromise = null;
  316 + }
  317 + delete alarmSourceListeners[alarmSourceListener.id];
  318 + }
  319 + }
214 320 }
... ...
... ... @@ -14,8 +14,6 @@
14 14 * limitations under the License.
15 15 */
16 16
17   -const varsRegex = /\$\{([^\}]*)\}/g;
18   -
19 17 export default class AliasController {
20 18
21 19 constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) {
... ... @@ -113,14 +111,14 @@ export default class AliasController {
113 111 }
114 112 }
115 113
116   - resolveDatasource(datasource) {
  114 + resolveDatasource(datasource, isSingle) {
117 115 var deferred = this.$q.defer();
118 116 if (datasource.type === this.types.datasourceType.entity) {
119 117 if (datasource.entityAliasId) {
120 118 this.getAliasInfo(datasource.entityAliasId).then(
121 119 function success(aliasInfo) {
122 120 datasource.aliasName = aliasInfo.alias;
123   - if (aliasInfo.resolveMultiple) {
  121 + if (aliasInfo.resolveMultiple && !isSingle) {
124 122 var newDatasource;
125 123 var resolvedEntities = aliasInfo.resolvedEntities;
126 124 if (resolvedEntities && resolvedEntities.length) {
... ... @@ -178,30 +176,44 @@ export default class AliasController {
178 176 return deferred.promise;
179 177 }
180 178
  179 + resolveAlarmSource(alarmSource) {
  180 + var deferred = this.$q.defer();
  181 + var aliasCtrl = this;
  182 + this.resolveDatasource(alarmSource, true).then(
  183 + function success(datasources) {
  184 + var datasource = datasources[0];
  185 + if (datasource.type === aliasCtrl.types.datasourceType.function) {
  186 + var name;
  187 + if (datasource.name && datasource.name.length) {
  188 + name = datasource.name;
  189 + } else {
  190 + name = aliasCtrl.types.datasourceType.function;
  191 + }
  192 + datasource.name = name;
  193 + datasource.aliasName = name;
  194 + datasource.entityName = name;
  195 + } else if (datasource.unresolvedStateEntity) {
  196 + datasource.name = "Unresolved";
  197 + datasource.entityName = "Unresolved";
  198 + }
  199 + deferred.resolve(datasource);
  200 + },
  201 + function fail() {
  202 + deferred.reject();
  203 + }
  204 + );
  205 + return deferred.promise;
  206 + }
  207 +
181 208 resolveDatasources(datasources) {
182 209
  210 + var aliasCtrl = this;
  211 +
183 212 function updateDataKeyLabel(dataKey, datasource) {
184 213 if (!dataKey.pattern) {
185 214 dataKey.pattern = angular.copy(dataKey.label);
186 215 }
187   - var pattern = dataKey.pattern;
188   - var label = dataKey.pattern;
189   - var match = varsRegex.exec(pattern);
190   - while (match !== null) {
191   - var variable = match[0];
192   - var variableName = match[1];
193   - if (variableName === 'dsName') {
194   - label = label.split(variable).join(datasource.name);
195   - } else if (variableName === 'entityName') {
196   - label = label.split(variable).join(datasource.entityName);
197   - } else if (variableName === 'deviceName') {
198   - label = label.split(variable).join(datasource.entityName);
199   - } else if (variableName === 'aliasName') {
200   - label = label.split(variable).join(datasource.aliasName);
201   - }
202   - match = varsRegex.exec(pattern);
203   - }
204   - dataKey.label = label;
  216 + dataKey.label = aliasCtrl.utils.createLabelFromDatasource(datasource, dataKey.pattern);
205 217 }
206 218
207 219 function updateDatasourceKeyLabels(datasource) {
... ... @@ -213,7 +225,7 @@ export default class AliasController {
213 225 var deferred = this.$q.defer();
214 226 var newDatasources = angular.copy(datasources);
215 227 var datasorceResolveTasks = [];
216   - var aliasCtrl = this;
  228 +
217 229 newDatasources.forEach(function (datasource) {
218 230 var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource);
219 231 datasorceResolveTasks.push(resolveDatasourceTask);
... ...
... ... @@ -64,6 +64,45 @@ export default class Subscription {
64 64 deferred.resolve(subscription);
65 65 }
66 66 );
  67 + } else if (this.type === this.ctx.types.widgetType.alarm.value) {
  68 + this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
  69 + this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
  70 + this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
  71 + this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
  72 + this.alarmSource = options.alarmSource;
  73 +
  74 + this.alarmSearchStatus = angular.isDefined(options.alarmSearchStatus) ?
  75 + options.alarmSearchStatus : this.ctx.types.alarmSearchStatus.any;
  76 + this.alarmsPollingInterval = angular.isDefined(options.alarmsPollingInterval) ?
  77 + options.alarmsPollingInterval : 5000;
  78 +
  79 + this.alarmSourceListener = null;
  80 + this.alarms = [];
  81 +
  82 + this.originalTimewindow = null;
  83 + this.timeWindow = {
  84 + stDiff: this.ctx.stDiff
  85 + }
  86 + this.useDashboardTimewindow = options.useDashboardTimewindow;
  87 +
  88 + if (this.useDashboardTimewindow) {
  89 + this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
  90 + } else {
  91 + this.timeWindowConfig = angular.copy(options.timeWindowConfig);
  92 + }
  93 +
  94 + this.subscriptionTimewindow = null;
  95 +
  96 + this.loadingData = false;
  97 + this.displayLegend = false;
  98 + this.initAlarmSubscription().then(
  99 + function success() {
  100 + deferred.resolve(subscription);
  101 + },
  102 + function fail() {
  103 + deferred.reject();
  104 + }
  105 + );
67 106 } else {
68 107 this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
69 108 this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
... ... @@ -132,6 +171,43 @@ export default class Subscription {
132 171 return deferred.promise;
133 172 }
134 173
  174 + initAlarmSubscription() {
  175 + var deferred = this.ctx.$q.defer();
  176 + if (!this.ctx.aliasController) {
  177 + this.configureAlarmsData();
  178 + deferred.resolve();
  179 + } else {
  180 + var subscription = this;
  181 + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).then(
  182 + function success(alarmSource) {
  183 + subscription.alarmSource = alarmSource;
  184 + subscription.configureAlarmsData();
  185 + deferred.resolve();
  186 + },
  187 + function fail() {
  188 + deferred.reject();
  189 + }
  190 + );
  191 + }
  192 + return deferred.promise;
  193 + }
  194 +
  195 + configureAlarmsData() {
  196 + var subscription = this;
  197 + var registration;
  198 + if (this.useDashboardTimewindow) {
  199 + registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
  200 + if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
  201 + subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
  202 + subscription.update();
  203 + }
  204 + });
  205 + this.registrations.push(registration);
  206 + } else {
  207 + this.startWatchingTimewindow();
  208 + }
  209 + }
  210 +
135 211 initDataSubscription() {
136 212 var deferred = this.ctx.$q.defer();
137 213 if (!this.ctx.aliasController) {
... ... @@ -210,8 +286,7 @@ export default class Subscription {
210 286 registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
211 287 if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
212 288 subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
213   - subscription.unsubscribe();
214   - subscription.subscribe();
  289 + subscription.update();
215 290 }
216 291 });
217 292 this.registrations.push(registration);
... ... @@ -227,8 +302,7 @@ export default class Subscription {
227 302 return subscription.timeWindowConfig;
228 303 }, function (newTimewindow, prevTimewindow) {
229 304 if (!angular.equals(newTimewindow, prevTimewindow)) {
230   - subscription.unsubscribe();
231   - subscription.subscribe();
  305 + subscription.update();
232 306 }
233 307 }, true);
234 308 this.registrations.push(this.timeWindowWatchRegistration);
... ... @@ -393,6 +467,8 @@ export default class Subscription {
393 467 onAliasesChanged(aliasIds) {
394 468 if (this.type === this.ctx.types.widgetType.rpc.value) {
395 469 return this.checkRpcTarget(aliasIds);
  470 + } else if (this.type === this.ctx.types.widgetType.alarm.value) {
  471 + return this.checkAlarmSource(aliasIds);
396 472 } else {
397 473 return this.checkSubscriptions(aliasIds);
398 474 }
... ... @@ -429,8 +505,7 @@ export default class Subscription {
429 505 this.timeWindowConfig = angular.copy(this.originalTimewindow);
430 506 this.originalTimewindow = null;
431 507 this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
432   - this.unsubscribe();
433   - this.subscribe();
  508 + this.update();
434 509 this.startWatchingTimewindow();
435 510 }
436 511 }
... ... @@ -446,8 +521,7 @@ export default class Subscription {
446 521 }
447 522 this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs);
448 523 this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
449   - this.unsubscribe();
450   - this.subscribe();
  524 + this.update();
451 525 this.startWatchingTimewindow();
452 526 }
453 527 }
... ... @@ -516,6 +590,15 @@ export default class Subscription {
516 590 }
517 591 }
518 592
  593 + alarmsUpdated(alarms, apply) {
  594 + this.notifyDataLoaded();
  595 + this.alarms = alarms;
  596 + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
  597 + this.updateTimewindow();
  598 + }
  599 + this.onDataUpdated(apply);
  600 + }
  601 +
519 602 updateLegend(dataIndex, data, apply) {
520 603 var dataKey = this.legendData.keys[dataIndex].dataKey;
521 604 var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;
... ... @@ -536,66 +619,115 @@ export default class Subscription {
536 619 this.callbacks.legendDataUpdated(this, apply !== false);
537 620 }
538 621
  622 + update() {
  623 + this.unsubscribe();
  624 + this.subscribe();
  625 + }
  626 +
539 627 subscribe() {
540 628 if (this.type === this.ctx.types.widgetType.rpc.value) {
541 629 return;
542 630 }
  631 + if (this.type === this.ctx.types.widgetType.alarm.value) {
  632 + this.alarmsSubscribe();
  633 + } else {
  634 + this.notifyDataLoading();
  635 + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
  636 + this.updateRealtimeSubscription();
  637 + if (this.subscriptionTimewindow.fixedWindow) {
  638 + this.onDataUpdated();
  639 + }
  640 + }
  641 + var index = 0;
  642 + for (var i = 0; i < this.datasources.length; i++) {
  643 + var datasource = this.datasources[i];
  644 + if (angular.isFunction(datasource))
  645 + continue;
  646 +
  647 + var subscription = this;
  648 +
  649 + var listener = {
  650 + subscriptionType: this.type,
  651 + subscriptionTimewindow: this.subscriptionTimewindow,
  652 + datasource: datasource,
  653 + entityType: datasource.entityType,
  654 + entityId: datasource.entityId,
  655 + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
  656 + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
  657 + },
  658 + updateRealtimeSubscription: function () {
  659 + this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
  660 + return this.subscriptionTimewindow;
  661 + },
  662 + setRealtimeSubscription: function (subscriptionTimewindow) {
  663 + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
  664 + },
  665 + datasourceIndex: index
  666 + };
  667 +
  668 + for (var a = 0; a < datasource.dataKeys.length; a++) {
  669 + this.data[index + a].data = [];
  670 + }
  671 +
  672 + index += datasource.dataKeys.length;
  673 +
  674 + this.datasourceListeners.push(listener);
  675 + this.ctx.datasourceService.subscribeToDatasource(listener);
  676 + if (datasource.unresolvedStateEntity) {
  677 + this.notifyDataLoaded();
  678 + this.onDataUpdated();
  679 + }
  680 +
  681 + }
  682 + }
  683 + }
  684 +
  685 + alarmsSubscribe() {
543 686 this.notifyDataLoading();
544   - if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
  687 + if (this.timeWindowConfig) {
545 688 this.updateRealtimeSubscription();
546 689 if (this.subscriptionTimewindow.fixedWindow) {
547 690 this.onDataUpdated();
548 691 }
549 692 }
550   - var index = 0;
551   - for (var i = 0; i < this.datasources.length; i++) {
552   - var datasource = this.datasources[i];
553   - if (angular.isFunction(datasource))
554   - continue;
555   -
556   - var subscription = this;
557   -
558   - var listener = {
559   - subscriptionType: this.type,
560   - subscriptionTimewindow: this.subscriptionTimewindow,
561   - datasource: datasource,
562   - entityType: datasource.entityType,
563   - entityId: datasource.entityId,
564   - dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
565   - subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
566   - },
567   - updateRealtimeSubscription: function () {
568   - this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
569   - return this.subscriptionTimewindow;
570   - },
571   - setRealtimeSubscription: function (subscriptionTimewindow) {
572   - subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
573   - },
574   - datasourceIndex: index
575   - };
576   -
577   - for (var a = 0; a < datasource.dataKeys.length; a++) {
578   - this.data[index + a].data = [];
  693 + var subscription = this;
  694 + this.alarmSourceListener = {
  695 + subscriptionTimewindow: this.subscriptionTimewindow,
  696 + alarmSource: this.alarmSource,
  697 + alarmSearchStatus: this.alarmSearchStatus,
  698 + alarmsPollingInterval: this.alarmsPollingInterval,
  699 + alarmsUpdated: function(alarms, apply) {
  700 + subscription.alarmsUpdated(alarms, apply);
579 701 }
  702 + }
  703 + this.alarms = [];
580 704
581   - index += datasource.dataKeys.length;
  705 + this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener);
582 706
583   - this.datasourceListeners.push(listener);
584   - this.ctx.datasourceService.subscribeToDatasource(listener);
585   - if (datasource.unresolvedStateEntity) {
586   - this.notifyDataLoaded();
587   - this.onDataUpdated();
588   - }
  707 + if (this.alarmSource.unresolvedStateEntity) {
  708 + this.notifyDataLoaded();
  709 + this.onDataUpdated();
589 710 }
590 711 }
591 712
592 713 unsubscribe() {
593 714 if (this.type !== this.ctx.types.widgetType.rpc.value) {
594   - for (var i = 0; i < this.datasourceListeners.length; i++) {
595   - var listener = this.datasourceListeners[i];
596   - this.ctx.datasourceService.unsubscribeFromDatasource(listener);
  715 + if (this.type == this.ctx.types.widgetType.alarm.value) {
  716 + this.alarmsUnsubscribe();
  717 + } else {
  718 + for (var i = 0; i < this.datasourceListeners.length; i++) {
  719 + var listener = this.datasourceListeners[i];
  720 + this.ctx.datasourceService.unsubscribeFromDatasource(listener);
  721 + }
  722 + this.datasourceListeners = [];
597 723 }
598   - this.datasourceListeners = [];
  724 + }
  725 + }
  726 +
  727 + alarmsUnsubscribe() {
  728 + if (this.alarmSourceListener) {
  729 + this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener);
  730 + this.alarmSourceListener = null;
599 731 }
600 732 }
601 733
... ... @@ -607,6 +739,14 @@ export default class Subscription {
607 739 }
608 740 }
609 741
  742 + checkAlarmSource(aliasIds) {
  743 + if (this.alarmSource && this.alarmSource.entityAliasId) {
  744 + return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1;
  745 + } else {
  746 + return false;
  747 + }
  748 + }
  749 +
610 750 checkSubscriptions(aliasIds) {
611 751 var subscriptionsChanged = false;
612 752 for (var i = 0; i < this.datasourceListeners.length; i++) {
... ...
... ... @@ -19,6 +19,7 @@ import tinycolor from 'tinycolor2';
19 19
20 20 import thingsboardLedLight from '../components/led-light.directive';
21 21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
  22 +import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
22 23
23 24 import TbFlot from '../widget/lib/flot-widget';
24 25 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
... ... @@ -33,7 +34,7 @@ import thingsboardTypes from '../common/types.constant';
33 34 import thingsboardUtils from '../common/utils.service';
34 35
35 36 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
36   - thingsboardTypes, thingsboardUtils])
  37 + thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils])
37 38 .factory('widgetService', WidgetService)
38 39 .name;
39 40
... ...
... ... @@ -31,7 +31,7 @@
31 31 on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
32 32 on-delete-asset="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-asset>
33 33 </md-tab>
34   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
  34 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
35 35 <tb-attribute-table flex
36 36 entity-id="vm.grid.operatingItem().id.id"
37 37 entity-type="{{vm.types.entityType.asset}}"
... ... @@ -39,7 +39,7 @@
39 39 default-attribute-scope="{{vm.types.attributesScope.server.value}}">
40 40 </tb-attribute-table>
41 41 </md-tab>
42   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
  42 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
43 43 <tb-attribute-table flex
44 44 entity-id="vm.grid.operatingItem().id.id"
45 45 entity-type="{{vm.types.entityType.asset}}"
... ... @@ -48,19 +48,19 @@
48 48 disable-attribute-scope-selection="true">
49 49 </tb-attribute-table>
50 50 </md-tab>
51   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
  51 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
52 52 <tb-alarm-table flex entity-type="vm.types.entityType.asset"
53 53 entity-id="vm.grid.operatingItem().id.id">
54 54 </tb-alarm-table>
55 55 </md-tab>
56   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'asset.events' | translate }}">
  56 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'asset.events' | translate }}">
57 57 <tb-event-table flex entity-type="vm.types.entityType.asset"
58 58 entity-id="vm.grid.operatingItem().id.id"
59 59 tenant-id="vm.grid.operatingItem().tenantId.id"
60 60 default-event-type="{{vm.types.eventType.error.value}}">
61 61 </tb-event-table>
62 62 </md-tab>
63   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  63 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
64 64 <tb-relation-table flex
65 65 entity-id="vm.grid.operatingItem().id.id"
66 66 entity-type="{{vm.types.entityType.asset}}">
... ...
... ... @@ -59,6 +59,53 @@ export default angular.module('thingsboard.types', [])
59 59 name: "aggregation.none"
60 60 }
61 61 },
  62 + alarmFields: {
  63 + createdTime: {
  64 + value: "createdTime",
  65 + name: "alarm.created-time",
  66 + time: true
  67 + },
  68 + startTime: {
  69 + value: "startTs",
  70 + name: "alarm.start-time",
  71 + time: true
  72 + },
  73 + endTime: {
  74 + value: "endTs",
  75 + name: "alarm.end-time",
  76 + time: true
  77 + },
  78 + ackTime: {
  79 + value: "ackTs",
  80 + name: "alarm.ack-time",
  81 + time: true
  82 + },
  83 + clearTime: {
  84 + value: "clearTs",
  85 + name: "alarm.clear-time",
  86 + time: true
  87 + },
  88 + originator: {
  89 + value: "originatorName",
  90 + name: "alarm.originator"
  91 + },
  92 + originatorType: {
  93 + value: "originator.entityType",
  94 + name: "alarm.originator-type"
  95 + },
  96 + type: {
  97 + value: "type",
  98 + name: "alarm.type"
  99 + },
  100 + severity: {
  101 + value: "severity",
  102 + name: "alarm.severity"
  103 + },
  104 + status: {
  105 + value: "status",
  106 + name: "alarm.status"
  107 + }
  108 + },
62 109 alarmStatus: {
63 110 activeUnack: "ACTIVE_UNACK",
64 111 activeAck: "ACTIVE_ACK",
... ... @@ -75,23 +122,28 @@ export default angular.module('thingsboard.types', [])
75 122 alarmSeverity: {
76 123 "CRITICAL": {
77 124 name: "alarm.severity-critical",
78   - class: "tb-critical"
  125 + class: "tb-critical",
  126 + color: "red"
79 127 },
80 128 "MAJOR": {
81 129 name: "alarm.severity-major",
82   - class: "tb-major"
  130 + class: "tb-major",
  131 + color: "orange"
83 132 },
84 133 "MINOR": {
85 134 name: "alarm.severity-minor",
86   - class: "tb-minor"
  135 + class: "tb-minor",
  136 + color: "#ffca3d"
87 137 },
88 138 "WARNING": {
89 139 name: "alarm.severity-warning",
90   - class: "tb-warning"
  140 + class: "tb-warning",
  141 + color: "#abab00"
91 142 },
92 143 "INDETERMINATE": {
93 144 name: "alarm.severity-indeterminate",
94   - class: "tb-indeterminate"
  145 + class: "tb-indeterminate",
  146 + color: "green"
95 147 }
96 148 },
97 149 aliasFilterType: {
... ... @@ -153,7 +205,8 @@ export default angular.module('thingsboard.types', [])
153 205 dataKeyType: {
154 206 timeseries: "timeseries",
155 207 attribute: "attribute",
156   - function: "function"
  208 + function: "function",
  209 + alarm: "alarm"
157 210 },
158 211 componentType: {
159 212 filter: "FILTER",
... ... @@ -319,6 +372,14 @@ export default angular.module('thingsboard.types', [])
319 372 alias: "basic_gpio_control"
320 373 }
321 374 },
  375 + alarm: {
  376 + value: "alarm",
  377 + name: "widget.alarm",
  378 + template: {
  379 + bundleAlias: "alarm_widgets",
  380 + alias: "alarms_table"
  381 + }
  382 + },
322 383 static: {
323 384 value: "static",
324 385 name: "widget.static",
... ... @@ -333,7 +394,7 @@ export default angular.module('thingsboard.types', [])
333 394 cards: "cards"
334 395 },
335 396 translate: {
336   - dashboardStatePrefix: "dashboardState.state."
  397 + customTranslationsPrefix: "custom."
337 398 }
338 399 }
339 400 ).name;
... ...
... ... @@ -21,8 +21,10 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
21 21 .factory('utils', Utils)
22 22 .name;
23 23
  24 +const varsRegex = /\$\{([^\}]*)\}/g;
  25 +
24 26 /*@ngInject*/
25   -function Utils($mdColorPalette, $rootScope, $window, types) {
  27 +function Utils($mdColorPalette, $rootScope, $window, $translate, types) {
26 28
27 29 var predefinedFunctions = {},
28 30 predefinedFunctionsList = [],
... ... @@ -93,9 +95,32 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
93 95 dataKeys: [angular.copy(defaultDataKey)]
94 96 };
95 97
  98 + var defaultAlarmFields = [
  99 + 'createdTime',
  100 + 'originator',
  101 + 'type',
  102 + 'severity',
  103 + 'status'
  104 + ];
  105 +
  106 + var defaultAlarmDataKeys = [];
  107 + for (var i=0;i<defaultAlarmFields.length;i++) {
  108 + var name = defaultAlarmFields[i];
  109 + var dataKey = {
  110 + name: name,
  111 + type: types.dataKeyType.alarm,
  112 + label: $translate.instant(types.alarmFields[name].name)+'',
  113 + color: getMaterialColor(i),
  114 + settings: {},
  115 + _hash: Math.random()
  116 + };
  117 + defaultAlarmDataKeys.push(dataKey);
  118 + }
  119 +
96 120 var service = {
97 121 getDefaultDatasource: getDefaultDatasource,
98 122 getDefaultDatasourceJson: getDefaultDatasourceJson,
  123 + getDefaultAlarmDataKeys: getDefaultAlarmDataKeys,
99 124 getMaterialColor: getMaterialColor,
100 125 getPredefinedFunctionBody: getPredefinedFunctionBody,
101 126 getPredefinedFunctionsList: getPredefinedFunctionsList,
... ... @@ -109,7 +134,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
109 134 cleanCopy: cleanCopy,
110 135 isLocalUrl: isLocalUrl,
111 136 validateDatasources: validateDatasources,
112   - createKey: createKey
  137 + createKey: createKey,
  138 + createLabelFromDatasource: createLabelFromDatasource,
  139 + insertVariable: insertVariable
113 140 }
114 141
115 142 return service;
... ... @@ -212,6 +239,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
212 239 return angular.toJson(getDefaultDatasource(dataKeySchema));
213 240 }
214 241
  242 + function getDefaultAlarmDataKeys() {
  243 + return angular.copy(defaultAlarmDataKeys);
  244 + }
  245 +
215 246 function isDescriptorSchemaNotEmpty(descriptor) {
216 247 if (descriptor && descriptor.schema && descriptor.schema.properties) {
217 248 for(var prop in descriptor.schema.properties) {
... ... @@ -357,4 +388,38 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
357 388 return dataKey;
358 389 }
359 390
  391 + function createLabelFromDatasource(datasource, pattern) {
  392 + var label = angular.copy(pattern);
  393 + var match = varsRegex.exec(pattern);
  394 + while (match !== null) {
  395 + var variable = match[0];
  396 + var variableName = match[1];
  397 + if (variableName === 'dsName') {
  398 + label = label.split(variable).join(datasource.name);
  399 + } else if (variableName === 'entityName') {
  400 + label = label.split(variable).join(datasource.entityName);
  401 + } else if (variableName === 'deviceName') {
  402 + label = label.split(variable).join(datasource.entityName);
  403 + } else if (variableName === 'aliasName') {
  404 + label = label.split(variable).join(datasource.aliasName);
  405 + }
  406 + match = varsRegex.exec(pattern);
  407 + }
  408 + return label;
  409 + }
  410 +
  411 + function insertVariable(pattern, name, value) {
  412 + var result = angular.copy(pattern);
  413 + var match = varsRegex.exec(pattern);
  414 + while (match !== null) {
  415 + var variable = match[0];
  416 + var variableName = match[1];
  417 + if (variableName === name) {
  418 + result = result.split(variable).join(value);
  419 + }
  420 + match = varsRegex.exec(pattern);
  421 + }
  422 + return result;
  423 + }
  424 +
360 425 }
... ...
... ... @@ -58,6 +58,7 @@ function Dashboard() {
58 58 columns: '=',
59 59 margins: '=',
60 60 isEdit: '=',
  61 + autofillHeight: '=',
61 62 isMobile: '=',
62 63 isMobileDisabled: '=?',
63 64 isEditActionEnabled: '=',
... ... @@ -102,6 +103,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
102 103
103 104 vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
104 105
  106 + vm.isMobileSize = false;
  107 +
105 108 if (!('dashboardTimewindow' in vm)) {
106 109 vm.dashboardTimewindow = timeService.defaultTimewindow();
107 110 }
... ... @@ -177,10 +180,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
177 180 vm.widgetBackgroundColor = widgetBackgroundColor;
178 181 vm.widgetPadding = widgetPadding;
179 182 vm.showWidgetTitle = showWidgetTitle;
  183 + vm.showWidgetTitlePanel = showWidgetTitlePanel;
  184 + vm.showWidgetActions = showWidgetActions;
180 185 vm.widgetTitleStyle = widgetTitleStyle;
  186 + vm.widgetTitle = widgetTitle;
  187 + vm.widgetActions = widgetActions;
181 188 vm.dropWidgetShadow = dropWidgetShadow;
182 189 vm.enableWidgetFullscreen = enableWidgetFullscreen;
183 190 vm.hasTimewindow = hasTimewindow;
  191 + vm.hasAggregation = hasAggregation;
184 192 vm.editWidget = editWidget;
185 193 vm.exportWidget = exportWidget;
186 194 vm.removeWidget = removeWidget;
... ... @@ -267,12 +275,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
267 275
268 276 function updateMobileOpts() {
269 277 var isMobileDisabled = vm.isMobileDisabled === true;
270   - var isMobile = vm.isMobile === true && !isMobileDisabled;
  278 + var isMobile = vm.isMobile === true && !isMobileDisabled || vm.autofillHeight;
271 279 var mobileBreakPoint = isMobileDisabled ? 0 : (isMobile ? 20000 : 960);
  280 +
272 281 if (!isMobile && !isMobileDisabled) {
273 282 isMobile = !$mdMedia('gt-sm');
274 283 }
275   - var rowHeight = isMobile ? 70 : 'match';
  284 +
  285 + var rowHeight = detectRowSize(isMobile);
  286 +
276 287 if (vm.gridsterOpts.isMobile != isMobile) {
277 288 vm.gridsterOpts.isMobile = isMobile;
278 289 vm.gridsterOpts.mobileModeEnabled = isMobile;
... ... @@ -283,6 +294,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
283 294 if (vm.gridsterOpts.rowHeight != rowHeight) {
284 295 vm.gridsterOpts.rowHeight = rowHeight;
285 296 }
  297 +
  298 + vm.isMobileSize = checkIsMobileSize();
  299 + }
  300 +
  301 + function checkIsMobileSize() {
  302 + var isMobileDisabled = vm.isMobileDisabled === true;
  303 + var isMobileSize = vm.isMobile === true && !isMobileDisabled;
  304 + if (!isMobileSize && !isMobileDisabled) {
  305 + isMobileSize = !$mdMedia('gt-sm');
  306 + }
  307 + return isMobileSize;
286 308 }
287 309
288 310 $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
... ... @@ -293,6 +315,34 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
293 315 updateMobileOpts();
294 316 });
295 317
  318 + $scope.$watch('vm.autofillHeight', function () {
  319 + if (vm.autofillHeight) {
  320 + //if (gridsterParent.height()) {
  321 + // updateMobileOpts();
  322 + //} else {
  323 + if ($scope.parentHeighWatcher) {
  324 + $scope.parentHeighWatcher();
  325 + }
  326 + if (gridsterParent.height()) {
  327 + updateMobileOpts();
  328 + }
  329 + $scope.parentHeighWatcher = $scope.$watch(function() { return gridsterParent.height(); },
  330 + function(newHeight) {
  331 + if (newHeight) {
  332 + updateMobileOpts();
  333 + }
  334 + }
  335 + );
  336 + } else {
  337 + if ($scope.parentHeighWatcher) {
  338 + $scope.parentHeighWatcher();
  339 + $scope.parentHeighWatcher = null;
  340 + }
  341 +
  342 + updateMobileOpts();
  343 + }
  344 + });
  345 +
296 346 $scope.$watch('vm.isMobileDisabled', function () {
297 347 updateMobileOpts();
298 348 });
... ... @@ -329,6 +379,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
329 379 $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
330 380 });
331 381
  382 + $scope.$watch('vm.isMobileSize', function (newVal, prevVal) {
  383 + if (!angular.equals(newVal, prevVal)) {
  384 + $scope.$broadcast('mobileModeChanged', vm.isMobileSize);
  385 + }
  386 + });
  387 +
332 388 $scope.$on('gridster-resized', function (event, sizes, theGridster) {
333 389 if (checkIsLocalGridsterElement(theGridster)) {
334 390 vm.gridster = theGridster;
... ... @@ -341,13 +397,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
341 397 $scope.$on('gridster-mobile-changed', function (event, theGridster) {
342 398 if (checkIsLocalGridsterElement(theGridster)) {
343 399 vm.gridster = theGridster;
344   - var rowHeight = vm.gridster.isMobile ? 70 : 'match';
  400 + var rowHeight = detectRowSize(vm.gridster.isMobile);
345 401 if (vm.gridsterOpts.rowHeight != rowHeight) {
346 402 vm.gridsterOpts.rowHeight = rowHeight;
347 403 updateGridsterParams();
348 404 }
349   -
350   - $scope.$broadcast('mobileModeChanged', vm.gridster.isMobile);
  405 + vm.isMobileSize = checkIsMobileSize();
351 406
352 407 //TODO: widgets visibility
353 408 /*$timeout(function () {
... ... @@ -356,6 +411,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
356 411 }
357 412 });
358 413
  414 + function detectRowSize(isMobile) {
  415 + var rowHeight = isMobile ? 70 : 'match';
  416 + if (vm.autofillHeight) {
  417 + var viewportHeight = gridsterParent.height();
  418 + var totalRows = 0;
  419 + for (var i = 0; i < vm.widgets.length; i++) {
  420 + var w = vm.widgets[i];
  421 + var sizeY = widgetSizeY(w);
  422 + totalRows += sizeY;
  423 + }
  424 + rowHeight = (viewportHeight - (vm.gridsterOpts.margins[1])) / totalRows;
  425 + }
  426 + return rowHeight;
  427 + }
  428 +
359 429 function widgetOrder(widget) {
360 430 var order;
361 431 if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
... ... @@ -646,7 +716,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
646 716 }
647 717
648 718 function widgetSizeY(widget) {
649   - if (vm.gridsterOpts.isMobile) {
  719 + if (vm.gridsterOpts.isMobile && !vm.autofillHeight) {
650 720 var mobileHeight;
651 721 if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
652 722 mobileHeight = vm.widgetLayouts[widget.id].mobileHeight;
... ... @@ -669,7 +739,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
669 739 }
670 740
671 741 function setWidgetSizeY(widget, sizeY) {
672   - if (!vm.gridsterOpts.isMobile) {
  742 + if (!vm.gridsterOpts.isMobile && !vm.autofillHeight) {
673 743 if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
674 744 vm.widgetLayouts[widget.id].sizeY = sizeY;
675 745 } else {
... ... @@ -746,6 +816,24 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
746 816 }
747 817 }
748 818
  819 + function showWidgetTitlePanel(widget) {
  820 + var ctx = widgetContext(widget);
  821 + if (ctx && ctx.hideTitlePanel) {
  822 + return false;
  823 + } else {
  824 + return showWidgetTitle(widget) || hasTimewindow(widget);
  825 + }
  826 + }
  827 +
  828 + function showWidgetActions(widget) {
  829 + var ctx = widgetContext(widget);
  830 + if (ctx && ctx.hideTitlePanel) {
  831 + return false;
  832 + } else {
  833 + return true;
  834 + }
  835 + }
  836 +
749 837 function widgetTitleStyle(widget) {
750 838 if (angular.isDefined(widget.config.titleStyle)) {
751 839 return widget.config.titleStyle;
... ... @@ -754,6 +842,33 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
754 842 }
755 843 }
756 844
  845 + function widgetTitle(widget) {
  846 + var ctx = widgetContext(widget);
  847 + if (ctx && ctx.widgetTitle
  848 + && ctx.widgetTitle.length) {
  849 + return ctx.widgetTitle;
  850 + } else {
  851 + return widget.config.title;
  852 + }
  853 + }
  854 +
  855 + function widgetActions(widget) {
  856 + var ctx = widgetContext(widget);
  857 + if (ctx && ctx.widgetActions && ctx.widgetActions.length) {
  858 + return ctx.widgetActions;
  859 + } else {
  860 + return [];
  861 + }
  862 + }
  863 +
  864 + function widgetContext(widget) {
  865 + var context;
  866 + if (widget.$ctx) {
  867 + context = widget.$ctx();
  868 + }
  869 + return context;
  870 + }
  871 +
757 872 function dropWidgetShadow(widget) {
758 873 if (angular.isDefined(widget.config.dropShadow)) {
759 874 return widget.config.dropShadow;
... ... @@ -771,7 +886,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
771 886 }
772 887
773 888 function hasTimewindow(widget) {
774   - if (widget.type === types.widgetType.timeseries.value) {
  889 + if (widget.type === types.widgetType.timeseries.value || widget.type === types.widgetType.alarm.value) {
775 890 return angular.isDefined(widget.config.useDashboardTimewindow) ?
776 891 !widget.config.useDashboardTimewindow : false;
777 892 } else {
... ... @@ -779,6 +894,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
779 894 }
780 895 }
781 896
  897 + function hasAggregation(widget) {
  898 + return widget.type === types.widgetType.timeseries.value;
  899 + }
  900 +
782 901 function adoptMaxRows() {
783 902 if (vm.widgets) {
784 903 var maxRows = vm.gridsterOpts.maxRows;
... ...
... ... @@ -23,7 +23,7 @@
23 23 </md-content>
24 24 <md-menu md-position-mode="target target" tb-mousepoint-menu>
25 25 <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
26   - <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%;">
  26 + <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
27 27 <div id="gridster-child" gridster="vm.gridsterOpts">
28 28 <ul>
29 29 <li gridster-item="vm.widgetItemMap" class="tb-noselect" ng-repeat="widget in vm.widgets">
... ... @@ -37,7 +37,8 @@
37 37 class="tb-widget"
38 38 ng-class="{'tb-highlighted': vm.isHighlighted(widget),
39 39 'tb-not-highlighted': vm.isNotHighlighted(widget),
40   - 'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}"
  40 + 'md-whiteframe-4dp': vm.dropWidgetShadow(widget),
  41 + 'tb-has-timewindow': vm.hasTimewindow(widget)}"
41 42 tb-mousedown="vm.widgetMouseDown($event, widget)"
42 43 ng-click="vm.widgetClicked($event, widget)"
43 44 tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
... ... @@ -45,11 +46,21 @@
45 46 color: vm.widgetColor(widget),
46 47 backgroundColor: vm.widgetBackgroundColor(widget),
47 48 padding: vm.widgetPadding(widget)}">
48   - <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
49   - <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
50   - <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
  49 + <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitlePanel(widget)">
  50 + <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{vm.widgetTitle(widget)}}</span>
  51 + <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
51 52 </div>
52   - <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()">
  53 + <div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
  54 + <md-button ng-repeat="action in vm.widgetActions(widget)"
  55 + aria-label="{{ action.name | translate }}"
  56 + ng-show="!vm.isEdit && action.show"
  57 + ng-click="action.onAction($event)"
  58 + class="md-icon-button">
  59 + <md-tooltip md-direction="top">
  60 + {{ action.name | translate }}
  61 + </md-tooltip>
  62 + <ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
  63 + </md-button>
53 64 <md-button id="expand-button"
54 65 ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
55 66 aria-label="{{ 'fullscreen.fullscreen' | translate }}"
... ... @@ -92,6 +103,7 @@
92 103 aliasController: vm.aliasController,
93 104 stateController: vm.stateController,
94 105 isEdit: vm.isEdit,
  106 + isMobile: vm.isMobileSize,
95 107 stDiff: vm.stDiff,
96 108 dashboardTimewindow: vm.dashboardTimewindow,
97 109 dashboardTimewindowApi: vm.dashboardTimewindowApi }">
... ...
... ... @@ -63,6 +63,12 @@ function DatakeyConfig($compile, $templateCache, $q, types) {
63 63 element.html(template);
64 64
65 65 scope.types = types;
  66 +
  67 + scope.alarmFields = [];
  68 + for (var alarmField in types.alarmFields) {
  69 + scope.alarmFields.push(alarmField);
  70 + }
  71 +
66 72 scope.selectedKey = null;
67 73 scope.keySearchText = null;
68 74 scope.usePostProcessing = false;
... ... @@ -112,21 +118,39 @@ function DatakeyConfig($compile, $templateCache, $q, types) {
112 118 }, true);
113 119
114 120 scope.keysSearch = function (searchText) {
115   - if (scope.entityAlias) {
116   - var deferred = $q.defer();
117   - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: scope.model.type})
118   - .then(function (keys) {
119   - keys.push(searchText);
120   - deferred.resolve(keys);
121   - }, function (e) {
122   - deferred.reject(e);
123   - });
124   - return deferred.promise;
  121 + if (scope.model.type === types.dataKeyType.alarm) {
  122 + var dataKeys = searchText ? scope.alarmFields.filter(
  123 + scope.createFilterForDataKey(searchText)) : scope.alarmFields;
  124 + dataKeys.push(searchText);
  125 + return dataKeys;
125 126 } else {
126   - return $q.when([]);
  127 + if (scope.entityAlias) {
  128 + var deferred = $q.defer();
  129 + scope.fetchEntityKeys({
  130 + entityAliasId: scope.entityAlias.id,
  131 + query: searchText,
  132 + type: scope.model.type
  133 + })
  134 + .then(function (keys) {
  135 + keys.push(searchText);
  136 + deferred.resolve(keys);
  137 + }, function (e) {
  138 + deferred.reject(e);
  139 + });
  140 + return deferred.promise;
  141 + } else {
  142 + return $q.when([]);
  143 + }
127 144 }
128 145 };
129 146
  147 + scope.createFilterForDataKey = function (query) {
  148 + var lowercaseQuery = angular.lowercase(query);
  149 + return function filterFn(dataKey) {
  150 + return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0);
  151 + };
  152 + };
  153 +
130 154 $compile(element.contents())(scope);
131 155 }
132 156
... ...
... ... @@ -16,7 +16,9 @@
16 16
17 17 -->
18 18 <md-content class="md-padding" layout="column">
19   - <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries || model.type === types.dataKeyType.attribute"
  19 + <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries ||
  20 + model.type === types.dataKeyType.attribute ||
  21 + model.type === types.dataKeyType.alarm"
20 22 style="padding-bottom: 8px;"
21 23 ng-required="true"
22 24 md-no-cache="true"
... ... @@ -27,8 +29,8 @@
27 29 md-items="item in keysSearch(keySearchText)"
28 30 md-item-text="item"
29 31 md-min-length="0"
30   - placeholder="Key name"
31   - md-floating-label="Key">
  32 + placeholder="{{ 'entity.key-name' | translate }}"
  33 + md-floating-label="{{ 'entity.key' | translate }}">
32 34 <span md-highlight-text="keySearchText" md-highlight-flags="^i">{{item}}</span>
33 35 </md-autocomplete>
34 36 <div layout="row" layout-align="start center">
... ... @@ -48,7 +50,7 @@
48 50 md-color-history="false">
49 51 </div>
50 52 </div>
51   - <div layout="row" layout-align="start center">
  53 + <div layout="row" layout-align="start center" ng-if="model.type !== types.dataKeyType.alarm">
52 54 <md-input-container flex>
53 55 <label translate>datakey.units</label>
54 56 <input name="units" ng-model="model.units">
... ...
... ... @@ -45,12 +45,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
45 45 scope.ngModelCtrl = ngModelCtrl;
46 46 scope.types = types;
47 47
  48 + scope.alarmFields = [];
  49 + for (var alarmField in types.alarmFields) {
  50 + scope.alarmFields.push(alarmField);
  51 + }
  52 +
48 53 scope.selectedTimeseriesDataKey = null;
49 54 scope.timeseriesDataKeySearchText = null;
50 55
51 56 scope.selectedAttributeDataKey = null;
52 57 scope.attributeDataKeySearchText = null;
53 58
  59 + scope.selectedAlarmDataKey = null;
  60 + scope.alarmDataKeySearchText = null;
  61 +
54 62 scope.updateValidity = function () {
55 63 if (ngModelCtrl.$viewValue) {
56 64 var value = ngModelCtrl.$viewValue;
... ... @@ -81,24 +89,27 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
81 89 });
82 90
83 91 scope.$watch('timeseriesDataKeys', function () {
84   - if (ngModelCtrl.$viewValue) {
85   - var dataKeys = [];
86   - dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
87   - dataKeys = dataKeys.concat(scope.attributeDataKeys);
88   - ngModelCtrl.$viewValue.dataKeys = dataKeys;
89   - scope.updateValidity();
90   - }
  92 + updateDataKeys();
91 93 }, true);
92 94
93 95 scope.$watch('attributeDataKeys', function () {
  96 + updateDataKeys();
  97 + }, true);
  98 +
  99 + scope.$watch('alarmDataKeys', function () {
  100 + updateDataKeys();
  101 + }, true);
  102 +
  103 + function updateDataKeys() {
94 104 if (ngModelCtrl.$viewValue) {
95 105 var dataKeys = [];
96 106 dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
97 107 dataKeys = dataKeys.concat(scope.attributeDataKeys);
  108 + dataKeys = dataKeys.concat(scope.alarmDataKeys);
98 109 ngModelCtrl.$viewValue.dataKeys = dataKeys;
99 110 scope.updateValidity();
100 111 }
101   - }, true);
  112 + }
102 113
103 114 ngModelCtrl.$render = function () {
104 115 if (ngModelCtrl.$viewValue) {
... ... @@ -111,16 +122,20 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
111 122 }
112 123 var timeseriesDataKeys = [];
113 124 var attributeDataKeys = [];
  125 + var alarmDataKeys = [];
114 126 for (var d in ngModelCtrl.$viewValue.dataKeys) {
115 127 var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
116 128 if (dataKey.type === types.dataKeyType.timeseries) {
117 129 timeseriesDataKeys.push(dataKey);
118 130 } else if (dataKey.type === types.dataKeyType.attribute) {
119 131 attributeDataKeys.push(dataKey);
  132 + } else if (dataKey.type === types.dataKeyType.alarm) {
  133 + alarmDataKeys.push(dataKey);
120 134 }
121 135 }
122 136 scope.timeseriesDataKeys = timeseriesDataKeys;
123 137 scope.attributeDataKeys = attributeDataKeys;
  138 + scope.alarmDataKeys = alarmDataKeys;
124 139 }
125 140 };
126 141
... ... @@ -135,6 +150,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
135 150 if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
136 151 scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
137 152 }
  153 + if (!scope.alarmDataKeySearchText || scope.alarmDataKeySearchText === '') {
  154 + scope.alarmDataKeySearchText = scope.alarmDataKeySearchText === '' ? null : '';
  155 + }
138 156 };
139 157
140 158 scope.transformTimeseriesDataKeyChip = function (chip) {
... ... @@ -145,6 +163,10 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
145 163 return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
146 164 };
147 165
  166 + scope.transformAlarmDataKeyChip = function (chip) {
  167 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm});
  168 + };
  169 +
148 170 scope.showColorPicker = function (event, dataKey) {
149 171 $mdColorPicker.show({
150 172 value: dataKey.color,
... ... @@ -196,6 +218,8 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
196 218 scope.timeseriesDataKeys[index] = dataKey;
197 219 } else if (dataKey.type === types.dataKeyType.attribute) {
198 220 scope.attributeDataKeys[index] = dataKey;
  221 + } else if (dataKey.type === types.dataKeyType.alarm) {
  222 + scope.alarmDataKeys[index] = dataKey;
199 223 }
200 224 ngModelCtrl.$setDirty();
201 225 }, function () {
... ... @@ -203,20 +227,33 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
203 227 };
204 228
205 229 scope.dataKeysSearch = function (searchText, type) {
206   - if (scope.entityAlias) {
207   - var deferred = $q.defer();
208   - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})
209   - .then(function (dataKeys) {
210   - deferred.resolve(dataKeys);
211   - }, function (e) {
212   - deferred.reject(e);
213   - });
214   - return deferred.promise;
  230 + if (scope.widgetType == types.widgetType.alarm.value) {
  231 + var dataKeys = searchText ? scope.alarmFields.filter(
  232 + scope.createFilterForDataKey(searchText)) : scope.alarmFields;
  233 + return dataKeys;
215 234 } else {
216   - return $q.when([]);
  235 + if (scope.entityAlias) {
  236 + var deferred = $q.defer();
  237 + scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})
  238 + .then(function (dataKeys) {
  239 + deferred.resolve(dataKeys);
  240 + }, function (e) {
  241 + deferred.reject(e);
  242 + });
  243 + return deferred.promise;
  244 + } else {
  245 + return $q.when([]);
  246 + }
217 247 }
218 248 };
219 249
  250 + scope.createFilterForDataKey = function (query) {
  251 + var lowercaseQuery = angular.lowercase(query);
  252 + return function filterFn(dataKey) {
  253 + return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0);
  254 + };
  255 + };
  256 +
220 257 scope.createKey = function (event, chipsId) {
221 258 var chipsChild = $(chipsId, element)[0].firstElementChild;
222 259 var el = angular.element(chipsChild);
... ...
... ... @@ -15,7 +15,7 @@
15 15 */
16 16 @import '../../scss/constants';
17 17
18   -.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
  18 +.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete, .tb-alarm-datakey-autocomplete {
19 19 .tb-not-found {
20 20 display: block;
21 21 line-height: 1.5;
... ...
... ... @@ -24,7 +24,7 @@
24 24 </tb-entity-alias-select>
25 25 <section flex layout='column'>
26 26 <section flex layout='column' layout-align="center" style="padding-left: 4px;">
27   - <md-chips flex
  27 + <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
28 28 id="timeseries_datakey_chips"
29 29 ng-required="true"
30 30 ng-model="timeseriesDataKeys" md-autocomplete-snap
... ... @@ -128,10 +128,63 @@
128 128 </div>
129 129 </md-chip-template>
130 130 </md-chips>
  131 + <md-chips flex ng-if="widgetType == types.widgetType.alarm.value"
  132 + id="alarm_datakey_chips"
  133 + ng-required="true"
  134 + ng-model="alarmDataKeys" md-autocomplete-snap
  135 + md-transform-chip="transformAlarmDataKeyChip($chip)"
  136 + md-require-match="false">
  137 + <md-autocomplete
  138 + md-no-cache="true"
  139 + id="alarm_datakey"
  140 + md-selected-item="selectedAlarmDataKey"
  141 + md-search-text="alarmDataKeySearchText"
  142 + md-items="item in dataKeysSearch(alarmDataKeySearchText, types.dataKeyType.alarm)"
  143 + md-item-text="item.name"
  144 + md-min-length="0"
  145 + placeholder="{{'datakey.alarm' | translate }}"
  146 + md-menu-class="tb-alarm-datakey-autocomplete">
  147 + <span md-highlight-text="alarmDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  148 + <md-not-found>
  149 + <div class="tb-not-found">
  150 + <div class="tb-no-entries" ng-if="!textIsNotEmpty(alarmDataKeySearchText)">
  151 + <span translate>entity.no-keys-found</span>
  152 + </div>
  153 + <div ng-if="textIsNotEmpty(alarmDataKeySearchText)">
  154 + <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
  155 + <span>
  156 + <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a>
  157 + </span>
  158 + </div>
  159 + </div>
  160 + </md-not-found>
  161 + </md-autocomplete>
  162 + <md-chip-template>
  163 + <div layout="row" layout-align="start center" class="tb-attribute-chip">
  164 + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
  165 + <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
  166 + </div>
  167 + <div layout="row" flex>
  168 + <div class="tb-chip-label">
  169 + {{$chip.label}}
  170 + </div>
  171 + <div class="tb-chip-separator">: </div>
  172 + <div class="tb-chip-label">
  173 + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
  174 + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
  175 + </div>
  176 + </div>
  177 + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
  178 + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
  179 + </md-button>
  180 + </div>
  181 + </md-chip-template>
  182 + </md-chips>
131 183 </section>
132 184 <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
133 185 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
134 186 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
  187 + <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
135 188 </div>
136 189 </section>
137 190 </section>
... ...
... ... @@ -43,18 +43,27 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
43 43 element.html(template);
44 44
45 45 scope.ngModelCtrl = ngModelCtrl;
  46 + scope.types = types;
  47 +
46 48 scope.functionTypes = utils.getPredefinedFunctionsList();
  49 + scope.alarmFields = [];
  50 + for (var alarmField in types.alarmFields) {
  51 + scope.alarmFields.push(alarmField);
  52 + }
47 53
48 54 scope.selectedDataKey = null;
49 55 scope.dataKeySearchText = null;
50 56
  57 + scope.selectedAlarmDataKey = null;
  58 + scope.alarmDataKeySearchText = null;
  59 +
51 60 scope.updateValidity = function () {
52 61 if (ngModelCtrl.$viewValue) {
53 62 var value = ngModelCtrl.$viewValue;
54 63 var dataValid = angular.isDefined(value) && value != null;
55 64 ngModelCtrl.$setValidity('deviceData', dataValid);
56 65 if (dataValid) {
57   - ngModelCtrl.$setValidity('funcTypes',
  66 + ngModelCtrl.$setValidity('datasourceKeys',
58 67 angular.isDefined(value.dataKeys) &&
59 68 value.dataKeys != null &&
60 69 value.dataKeys.length > 0);
... ... @@ -63,13 +72,22 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
63 72 };
64 73
65 74 scope.$watch('funcDataKeys', function () {
  75 + updateDataKeys();
  76 + }, true);
  77 +
  78 + scope.$watch('alarmDataKeys', function () {
  79 + updateDataKeys();
  80 + }, true);
  81 +
  82 + function updateDataKeys() {
66 83 if (ngModelCtrl.$viewValue) {
67 84 var dataKeys = [];
68 85 dataKeys = dataKeys.concat(scope.funcDataKeys);
  86 + dataKeys = dataKeys.concat(scope.alarmDataKeys);
69 87 ngModelCtrl.$viewValue.dataKeys = dataKeys;
70 88 scope.updateValidity();
71 89 }
72   - }, true);
  90 + }
73 91
74 92 scope.$watch('datasourceName', function () {
75 93 if (ngModelCtrl.$viewValue) {
... ... @@ -81,18 +99,31 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
81 99 ngModelCtrl.$render = function () {
82 100 if (ngModelCtrl.$viewValue) {
83 101 var funcDataKeys = [];
  102 + var alarmDataKeys = [];
84 103 if (ngModelCtrl.$viewValue.dataKeys) {
85   - funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys);
  104 + for (var d=0;d<ngModelCtrl.$viewValue.dataKeys.length;d++) {
  105 + var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
  106 + if (dataKey.type === types.dataKeyType.function) {
  107 + funcDataKeys.push(dataKey);
  108 + } else if (dataKey.type === types.dataKeyType.alarm) {
  109 + alarmDataKeys.push(dataKey);
  110 + }
  111 + }
86 112 }
87 113 scope.funcDataKeys = funcDataKeys;
  114 + scope.alarmDataKeys = alarmDataKeys;
88 115 scope.datasourceName = ngModelCtrl.$viewValue.name;
89 116 }
90 117 };
91 118
92   - scope.transformDataKeyChip = function (chip) {
  119 + scope.transformFuncDataKeyChip = function (chip) {
93 120 return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
94 121 };
95 122
  123 + scope.transformAlarmDataKeyChip = function (chip) {
  124 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm});
  125 + };
  126 +
96 127 scope.showColorPicker = function (event, dataKey) {
97 128 $mdColorPicker.show({
98 129 value: dataKey.color,
... ... @@ -129,7 +160,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
129 160 dataKey: angular.copy(dataKey),
130 161 dataKeySettingsSchema: scope.datakeySettingsSchema,
131 162 entityAlias: null,
132   - entityAliases: null
  163 + aliasController: null
133 164 },
134 165 parent: angular.element($document[0].body),
135 166 fullscreen: true,
... ... @@ -140,7 +171,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
140 171 w.triggerHandler('resize');
141 172 }
142 173 }).then(function (dataKey) {
143   - scope.funcDataKeys[index] = dataKey;
  174 + if (dataKey.type === types.dataKeyType.function) {
  175 + scope.funcDataKeys[index] = dataKey;
  176 + } else if (dataKey.type === types.dataKeyType.alarm) {
  177 + scope.alarmDataKeys[index] = dataKey;
  178 + }
144 179 ngModelCtrl.$setDirty();
145 180 }, function () {
146 181 });
... ... @@ -151,8 +186,9 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
151 186 }
152 187
153 188 scope.dataKeysSearch = function (dataKeySearchText) {
154   - var dataKeys = dataKeySearchText ? scope.functionTypes.filter(
155   - scope.createFilterForDataKey(dataKeySearchText)) : scope.functionTypes;
  189 + var targetKeys = scope.widgetType == types.widgetType.alarm.value ? scope.alarmFields : scope.functionTypes;
  190 + var dataKeys = dataKeySearchText ? targetKeys.filter(
  191 + scope.createFilterForDataKey(dataKeySearchText)) : targetKeys;
156 192 return dataKeys;
157 193 };
158 194
... ... @@ -180,6 +216,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
180 216 restrict: "E",
181 217 require: "^ngModel",
182 218 scope: {
  219 + widgetType: '=',
183 220 generateDataKey: '&',
184 221 datakeySettingsSchema: '='
185 222 },
... ...
... ... @@ -26,7 +26,7 @@
26 26 }
27 27 }
28 28
29   - .tb-func-datakey-autocomplete {
  29 + .tb-func-datakey-autocomplete, .tb-alarm-datakey-autocomplete {
30 30 .tb-not-found {
31 31 display: block;
32 32 line-height: 1.5;
... ...
... ... @@ -17,18 +17,19 @@
17 17 -->
18 18 <section class="tb-datasource-func" flex layout='column'
19 19 layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
20   - <md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;">
  20 + <md-input-container ng-if="widgetType != types.widgetType.alarm.value"
  21 + class="tb-datasource-name" md-no-float style="min-width: 200px;">
21 22 <input name="datasourceName"
22 23 placeholder="{{ 'datasource.name' | translate }}"
23 24 ng-model="datasourceName"
24 25 aria-label="{{ 'datasource.name' | translate }}">
25 26 </md-input-container>
26 27 <section flex layout='column' style="padding-left: 4px;">
27   - <md-chips flex
  28 + <md-chips flex ng-if="widgetType != types.widgetType.alarm.value"
28 29 id="function_datakey_chips"
29 30 ng-required="true"
30 31 ng-model="funcDataKeys" md-autocomplete-snap
31   - md-transform-chip="transformDataKeyChip($chip)"
  32 + md-transform-chip="transformFuncDataKeyChip($chip)"
32 33 md-require-match="false">
33 34 <md-autocomplete
34 35 md-no-cache="false"
... ... @@ -47,9 +48,9 @@
47 48 <span translate>device.no-keys-found</span>
48 49 </div>
49 50 <div ng-if="textIsNotEmpty(dataKeySearchText)">
50   - <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>
  51 + <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
51 52 <span>
52   - <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
  53 + <a translate ng-click="createKey($event, '#function_datakey_chips')">entity.create-new-key</a>
53 54 </span>
54 55 </div>
55 56 </div>
... ... @@ -75,8 +76,61 @@
75 76 </div>
76 77 </md-chip-template>
77 78 </md-chips>
  79 + <md-chips flex ng-if="widgetType == types.widgetType.alarm.value"
  80 + id="alarm_datakey_chips"
  81 + ng-required="true"
  82 + ng-model="alarmDataKeys" md-autocomplete-snap
  83 + md-transform-chip="transformAlarmDataKeyChip($chip)"
  84 + md-require-match="false">
  85 + <md-autocomplete
  86 + md-no-cache="true"
  87 + id="alarm_datakey"
  88 + md-selected-item="selectedAlarmDataKey"
  89 + md-search-text="alarmDataKeySearchText"
  90 + md-items="item in dataKeysSearch(alarmDataKeySearchText, types.dataKeyType.alarm)"
  91 + md-item-text="item.name"
  92 + md-min-length="0"
  93 + placeholder="{{'datakey.alarm' | translate }}"
  94 + md-menu-class="tb-alarm-datakey-autocomplete">
  95 + <span md-highlight-text="alarmDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  96 + <md-not-found>
  97 + <div class="tb-not-found">
  98 + <div class="tb-no-entries" ng-if="!textIsNotEmpty(alarmDataKeySearchText)">
  99 + <span translate>entity.no-keys-found</span>
  100 + </div>
  101 + <div ng-if="textIsNotEmpty(alarmDataKeySearchText)">
  102 + <span translate translate-values='{ key: "{{alarmDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
  103 + <span>
  104 + <a translate ng-click="createKey($event, '#alarm_datakey_chips')">entity.create-new-key</a>
  105 + </span>
  106 + </div>
  107 + </div>
  108 + </md-not-found>
  109 + </md-autocomplete>
  110 + <md-chip-template>
  111 + <div layout="row" layout-align="start center" class="tb-attribute-chip">
  112 + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
  113 + <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
  114 + </div>
  115 + <div layout="row" flex>
  116 + <div class="tb-chip-label">
  117 + {{$chip.label}}
  118 + </div>
  119 + <div class="tb-chip-separator">: </div>
  120 + <div class="tb-chip-label">
  121 + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
  122 + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
  123 + </div>
  124 + </div>
  125 + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
  126 + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
  127 + </md-button>
  128 + </div>
  129 + </md-chip-template>
  130 + </md-chips>
78 131 <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
79   - <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
  132 + <div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div>
  133 + <div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
80 134 </div>
81 135 </section>
82 136 </section>
... ...
... ... @@ -30,7 +30,7 @@ export default angular.module('thingsboard.directives.datasource', [thingsboardT
30 30 .name;
31 31
32 32 /*@ngInject*/
33   -function Datasource($compile, $templateCache, types) {
  33 +function Datasource($compile, $templateCache, utils, types) {
34 34
35 35 var linker = function (scope, element, attrs, ngModelCtrl) {
36 36
... ... @@ -53,8 +53,12 @@ function Datasource($compile, $templateCache, types) {
53 53 }
54 54
55 55 scope.$watch('model.type', function (newType, prevType) {
56   - if (newType != prevType) {
57   - scope.model.dataKeys = [];
  56 + if (newType && prevType && newType != prevType) {
  57 + if (scope.widgetType == types.widgetType.alarm.value) {
  58 + scope.model.dataKeys = utils.getDefaultAlarmDataKeys();
  59 + } else {
  60 + scope.model.dataKeys = [];
  61 + }
58 62 }
59 63 });
60 64
... ... @@ -63,9 +67,10 @@ function Datasource($compile, $templateCache, types) {
63 67 }, true);
64 68
65 69 ngModelCtrl.$render = function () {
66   - scope.model = {};
67 70 if (ngModelCtrl.$viewValue) {
68 71 scope.model = ngModelCtrl.$viewValue;
  72 + } else {
  73 + scope.model = {};
69 74 }
70 75 };
71 76
... ...
... ... @@ -29,6 +29,7 @@
29 29 ng-model="model"
30 30 datakey-settings-schema="datakeySettingsSchema"
31 31 ng-required="model.type === types.datasourceType.function"
  32 + widget-type="widgetType"
32 33 generate-data-key="generateDataKey({chip: chip, type: type})">
33 34 </tb-datasource-func>
34 35 <tb-datasource-entity flex
... ...
... ... @@ -124,7 +124,7 @@ function Grid() {
124 124 }
125 125
126 126 /*@ngInject*/
127   -function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache) {
  127 +function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache, $window) {
128 128
129 129 var vm = this;
130 130
... ... @@ -155,6 +155,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
155 155 vm.refreshList = refreshList;
156 156 vm.saveItem = saveItem;
157 157 vm.toggleItemSelection = toggleItemSelection;
  158 + vm.triggerResize = triggerResize;
158 159
159 160 $scope.$watch(function () {
160 161 return $mdMedia('xs') || $mdMedia('sm');
... ... @@ -600,6 +601,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
600 601 }
601 602 }
602 603
  604 + function triggerResize() {
  605 + var w = angular.element($window);
  606 + w.triggerHandler('resize');
  607 + }
  608 +
603 609 function moveToTop() {
604 610 moveToIndex(0, true);
605 611 }
... ...
... ... @@ -64,7 +64,7 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
64 64
65 65 scope.historyOnly = angular.isDefined(attrs.historyOnly);
66 66
67   - scope.aggregation = angular.isDefined(attrs.aggregation);
  67 + scope.aggregation = scope.$eval(attrs.aggregation);
68 68
69 69 scope.isToolbar = angular.isDefined(attrs.isToolbar);
70 70
... ...
... ... @@ -43,7 +43,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar
43 43 .name;
44 44
45 45 /*@ngInject*/
46   -function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, utils) {
  46 +function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout, types, utils) {
47 47
48 48 var linker = function (scope, element, attrs, ngModelCtrl) {
49 49
... ... @@ -87,6 +87,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
87 87 value: null
88 88 }
89 89
  90 + scope.alarmSource = {
  91 + value: null
  92 + }
  93 +
90 94 ngModelCtrl.$render = function () {
91 95 if (ngModelCtrl.$viewValue) {
92 96 var config = ngModelCtrl.$viewValue.config;
... ... @@ -113,7 +117,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
113 117 scope.showLegend = angular.isDefined(config.showLegend) ?
114 118 config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
115 119 scope.legendConfig = config.legendConfig;
116   - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
  120 + if (scope.widgetType !== types.widgetType.rpc.value &&
  121 + scope.widgetType !== types.widgetType.alarm.value &&
  122 + scope.widgetType !== types.widgetType.static.value
117 123 && scope.isDataEnabled) {
118 124 if (scope.datasources) {
119 125 scope.datasources.splice(0, scope.datasources.length);
... ... @@ -137,6 +143,16 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
137 143 } else {
138 144 scope.targetDeviceAlias.value = null;
139 145 }
  146 + } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
  147 + scope.alarmSearchStatus = angular.isDefined(config.alarmSearchStatus) ?
  148 + config.alarmSearchStatus : types.alarmSearchStatus.any;
  149 + scope.alarmsPollingInterval = angular.isDefined(config.alarmsPollingInterval) ?
  150 + config.alarmsPollingInterval : 5;
  151 + if (config.alarmSource) {
  152 + scope.alarmSource.value = config.alarmSource;
  153 + } else {
  154 + scope.alarmSource.value = null;
  155 + }
140 156 }
141 157
142 158 scope.settings = config.settings;
... ... @@ -175,6 +191,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
175 191 if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
176 192 valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0;
177 193 ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
  194 + } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
  195 + valid = config && config.alarmSource;
  196 + ngModelCtrl.$setValidity('alarmSource', valid);
178 197 } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
179 198 valid = config && config.datasources && config.datasources.length > 0;
180 199 ngModelCtrl.$setValidity('datasources', valid);
... ... @@ -190,7 +209,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
190 209 };
191 210
192 211 scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' +
193   - 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () {
  212 + 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' +
  213 + 'alarmSearchStatus + alarmsPollingInterval + showLegend', function () {
194 214 if (ngModelCtrl.$viewValue) {
195 215 var value = ngModelCtrl.$viewValue;
196 216 if (value.config) {
... ... @@ -210,6 +230,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
210 230 config.units = scope.units;
211 231 config.decimals = scope.decimals;
212 232 config.useDashboardTimewindow = scope.useDashboardTimewindow;
  233 + config.alarmSearchStatus = scope.alarmSearchStatus;
  234 + config.alarmsPollingInterval = scope.alarmsPollingInterval;
213 235 config.showLegend = scope.showLegend;
214 236 }
215 237 if (value.layout) {
... ... @@ -253,7 +275,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
253 275 }, true);
254 276
255 277 scope.$watch('datasources', function () {
256   - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value
  278 + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config
  279 + && scope.widgetType !== types.widgetType.rpc.value
  280 + && scope.widgetType !== types.widgetType.alarm.value
257 281 && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
258 282 var value = ngModelCtrl.$viewValue;
259 283 var config = value.config;
... ... @@ -286,6 +310,20 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
286 310 }
287 311 });
288 312
  313 + scope.$watch('alarmSource.value', function () {
  314 + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) {
  315 + var value = ngModelCtrl.$viewValue;
  316 + var config = value.config;
  317 + if (scope.alarmSource.value) {
  318 + config.alarmSource = scope.alarmSource.value;
  319 + } else {
  320 + config.alarmSource = null;
  321 + }
  322 + ngModelCtrl.$setViewValue(value);
  323 + scope.updateValidity();
  324 + }
  325 + });
  326 +
289 327 scope.addDatasource = function () {
290 328 var newDatasource;
291 329 if (scope.functionsOnly) {
... ... @@ -320,10 +358,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
320 358 return chip;
321 359 }
322 360
  361 + var label = chip;
  362 + if (type === types.dataKeyType.alarm) {
  363 + var alarmField = types.alarmFields[chip];
  364 + if (alarmField) {
  365 + label = $translate.instant(alarmField.name)+'';
  366 + }
  367 + }
  368 + label = scope.genNextLabel(label);
  369 +
323 370 var result = {
324 371 name: chip,
325 372 type: type,
326   - label: scope.genNextLabel(chip),
  373 + label: label,
327 374 color: scope.genNextColor(),
328 375 settings: {},
329 376 _hash: Math.random()
... ... @@ -351,15 +398,18 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
351 398 var matches = false;
352 399 do {
353 400 matches = false;
354   - if (value.config.datasources) {
355   - for (var d=0;d<value.config.datasources.length;d++) {
356   - var datasource = value.config.datasources[d];
357   - for (var k=0;k<datasource.dataKeys.length;k++) {
358   - var dataKey = datasource.dataKeys[k];
359   - if (dataKey.label === label) {
360   - i++;
361   - label = name + ' ' + i;
362   - matches = true;
  401 + var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
  402 + if (datasources) {
  403 + for (var d=0;d<datasources.length;d++) {
  404 + var datasource = datasources[d];
  405 + if (datasource && datasource.dataKeys) {
  406 + for (var k = 0; k < datasource.dataKeys.length; k++) {
  407 + var dataKey = datasource.dataKeys[k];
  408 + if (dataKey.label === label) {
  409 + i++;
  410 + label = name + ' ' + i;
  411 + matches = true;
  412 + }
363 413 }
364 414 }
365 415 }
... ... @@ -371,10 +421,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
371 421 scope.genNextColor = function () {
372 422 var i = 0;
373 423 var value = ngModelCtrl.$viewValue;
374   - if (value.config.datasources) {
375   - for (var d=0;d<value.config.datasources.length;d++) {
376   - var datasource = value.config.datasources[d];
377   - i += datasource.dataKeys.length;
  424 + var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources;
  425 + if (datasources) {
  426 + for (var d=0;d<datasources.length;d++) {
  427 + var datasource = datasources[d];
  428 + if (datasource && datasource.dataKeys) {
  429 + i += datasource.dataKeys.length;
  430 + }
378 431 }
379 432 }
380 433 return utils.getMaterialColor(i);
... ...
... ... @@ -20,18 +20,46 @@
20 20 <md-tab label="{{ 'widget-config.data' | translate }}"
21 21 ng-show="widgetType !== types.widgetType.static.value">
22 22 <md-content class="md-padding" layout="column">
23   - <div ng-show="widgetType === types.widgetType.timeseries.value" layout='column' layout-align="center"
  23 + <div ng-show="widgetType === types.widgetType.timeseries.value || widgetType === types.widgetType.alarm.value" layout='column' layout-align="center"
24 24 layout-gt-sm='row' layout-align-gt-sm="start center">
25 25 <md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}"
26 26 ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }}
27 27 </md-checkbox>
28 28 <section flex layout="row" layout-align="start center" style="margin-bottom: 16px;">
29 29 <span ng-class="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
30   - <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation flex ng-model="timewindow"></tb-timewindow>
  30 + <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation="{{ widgetType === types.widgetType.timeseries.value }}"
  31 + flex ng-model="timewindow"></tb-timewindow>
31 32 </section>
32 33 </div>
  34 + <div ng-show="widgetType === types.widgetType.alarm.value" layout='column' layout-align="center"
  35 + layout-gt-sm='row' layout-align-gt-sm="start center">
  36 + <md-input-container class="md-block" flex>
  37 + <label translate>alarm.alarm-status</label>
  38 + <md-select ng-model="alarmSearchStatus" style="padding-bottom: 24px;">
  39 + <md-option ng-repeat="searchStatus in types.alarmSearchStatus" ng-value="searchStatus">
  40 + {{ ('alarm.search-status.' + searchStatus) | translate }}
  41 + </md-option>
  42 + </md-select>
  43 + </md-input-container>
  44 + <md-input-container flex class="md-block">
  45 + <label translate>alarm.polling-interval</label>
  46 + <input ng-required="widgetType === types.widgetType.alarm.value"
  47 + type="number"
  48 + step="1"
  49 + min="1"
  50 + name="alarmsPollingInterval"
  51 + ng-model="alarmsPollingInterval"/>
  52 + <div ng-messages="theForm.alarmsPollingInterval.$error" multiple md-auto-hide="false">
  53 + <div ng-message="required" translate>alarm.polling-interval-required</div>
  54 + <div ng-message="min" translate>alarm.min-polling-interval-message</div>
  55 + </div>
  56 + </md-input-container>
  57 + </div>
33 58 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
34   - ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled">
  59 + ng-show="widgetType !== types.widgetType.rpc.value
  60 + && widgetType !== types.widgetType.alarm.value
  61 + && widgetType !== types.widgetType.static.value
  62 + && isDataEnabled">
35 63 <v-pane id="datasources-pane" expanded="true">
36 64 <v-pane-header>
37 65 {{ 'widget-config.datasources' | translate }}
... ... @@ -112,6 +140,24 @@
112 140 </v-pane-content>
113 141 </v-pane>
114 142 </v-accordion>
  143 + <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
  144 + ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
  145 + <v-pane id="alarm-source-pane" expanded="true">
  146 + <v-pane-header>
  147 + {{ 'widget-config.alarm-source' | translate }}
  148 + </v-pane-header>
  149 + <v-pane-content style="padding: 0 5px;">
  150 + <tb-datasource flex
  151 + ng-model="alarmSource.value"
  152 + widget-type="widgetType"
  153 + functions-only="functionsOnly"
  154 + alias-controller="aliasController"
  155 + datakey-settings-schema="datakeySettingsSchema"
  156 + generate-data-key="generateDataKey(chip,type)"
  157 + on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"></tb-datasource>
  158 + </v-pane-content>
  159 + </v-pane>
  160 + </v-accordion>
115 161 </md-content>
116 162 </md-tab>
117 163 <md-tab label="{{ 'widget-config.settings' | translate }}">
... ...
... ... @@ -21,7 +21,7 @@ import Subscription from '../api/subscription';
21 21
22 22 /*@ngInject*/
23 23 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
24   - datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
  24 + datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow,
25 25 dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
26 26
27 27 var vm = this;
... ... @@ -44,13 +44,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
44 44
45 45 var widgetContext = {
46 46 inited: false,
47   - $scope: $scope,
48 47 $container: null,
49 48 $containerParent: null,
50 49 width: 0,
51 50 height: 0,
  51 + hideTitlePanel: false,
52 52 isEdit: isEdit,
53   - isMobile: false,
  53 + isMobile: isMobile,
54 54 widgetConfig: widget.config,
55 55 settings: widget.config.settings,
56 56 units: widget.config.units || '',
... ... @@ -113,6 +113,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
113 113 timeService: timeService,
114 114 deviceService: deviceService,
115 115 datasourceService: datasourceService,
  116 + alarmService: alarmService,
116 117 utils: utils,
117 118 widgetUtils: widgetContext.utils,
118 119 dashboardTimewindowApi: dashboardTimewindowApi,
... ... @@ -121,6 +122,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
121 122 aliasController: aliasController
122 123 };
123 124
  125 + widget.$ctx = function() {
  126 + return widgetContext;
  127 + }
  128 +
124 129 var widgetTypeInstance;
125 130
126 131 vm.useCustomDatasources = false;
... ... @@ -285,9 +290,18 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
285 290 var deferred = $q.defer();
286 291 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
287 292 options = {
288   - type: widget.type,
289   - datasources: angular.copy(widget.config.datasources)
290   - };
  293 + type: widget.type
  294 + }
  295 + if (widget.type == types.widgetType.alarm.value) {
  296 + options.alarmSource = angular.copy(widget.config.alarmSource);
  297 + options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ?
  298 + widget.config.alarmSearchStatus : types.alarmSearchStatus.any;
  299 + options.alarmsPollingInterval = angular.isDefined(widget.config.alarmsPollingInterval) ?
  300 + widget.config.alarmsPollingInterval * 1000 : 5000;
  301 + } else {
  302 + options.datasources = angular.copy(widget.config.datasources)
  303 + }
  304 +
291 305 defaultComponentsOptions(options);
292 306
293 307 createSubscription(options).then(
... ... @@ -320,7 +334,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
320 334 $scope.executingRpcRequest = subscription.executingRpcRequest;
321 335 },
322 336 onRpcSuccess: function(subscription) {
323   - $scope.executingRpcRequest = subscription.executingRpcRequest;
  337 + $scope.executingRpcRequest = subscription.executingRpcRequest;
324 338 $scope.rpcErrorText = subscription.rpcErrorText;
325 339 $scope.rpcRejection = subscription.rpcRejection;
326 340 },
... ... @@ -436,7 +450,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
436 450 widgetContext.$container = $('#container', containerElement);
437 451 widgetContext.$containerParent = $(containerElement);
438 452
439   - $compile($element.contents())($scope);
  453 + if (widgetSizeDetected) {
  454 + widgetContext.$container.css('height', widgetContext.height + 'px');
  455 + widgetContext.$container.css('width', widgetContext.width + 'px');
  456 + }
  457 +
  458 + widgetContext.$scope = $scope.$new();
  459 +
  460 + $compile($element.contents())(widgetContext.$scope);
440 461
441 462 addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
442 463 }
... ... @@ -444,6 +465,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
444 465 function destroyWidgetElement() {
445 466 removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
446 467 $element.html('');
  468 + if (widgetContext.$scope) {
  469 + widgetContext.$scope.$destroy();
  470 + }
447 471 widgetContext.$container = null;
448 472 widgetContext.$containerParent = null;
449 473 }
... ... @@ -594,7 +618,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
594 618
595 619 function gridsterItemInitialized(item) {
596 620 if (item && item.gridster) {
597   - widgetContext.isMobile = item.gridster.isMobile;
598 621 gridsterItemInited = true;
599 622 onInit();
600 623 // gridsterItemElement = $(item.$element);
... ...
... ... @@ -31,7 +31,7 @@
31 31 on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)"
32 32 on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer>
33 33 </md-tab>
34   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
  34 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
35 35 <tb-attribute-table flex
36 36 entity-id="vm.grid.operatingItem().id.id"
37 37 entity-type="{{vm.types.entityType.customer}}"
... ... @@ -39,7 +39,7 @@
39 39 default-attribute-scope="{{vm.types.attributesScope.server.value}}">
40 40 </tb-attribute-table>
41 41 </md-tab>
42   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
  42 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
43 43 <tb-attribute-table flex
44 44 entity-id="vm.grid.operatingItem().id.id"
45 45 entity-type="{{vm.types.entityType.customer}}"
... ... @@ -48,19 +48,19 @@
48 48 disable-attribute-scope-selection="true">
49 49 </tb-attribute-table>
50 50 </md-tab>
51   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
  51 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
52 52 <tb-alarm-table flex entity-type="vm.types.entityType.customer"
53 53 entity-id="vm.grid.operatingItem().id.id">
54 54 </tb-alarm-table>
55 55 </md-tab>
56   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'customer.events' | translate }}">
  56 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'customer.events' | translate }}">
57 57 <tb-event-table flex entity-type="vm.types.entityType.customer"
58 58 entity-id="vm.grid.operatingItem().id.id"
59 59 tenant-id="vm.grid.operatingItem().tenantId.id"
60 60 default-event-type="{{vm.types.eventType.error.value}}">
61 61 </tb-event-table>
62 62 </md-tab>
63   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  63 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
64 64 <tb-relation-table flex
65 65 entity-id="vm.grid.operatingItem().id.id"
66 66 entity-type="{{vm.types.entityType.customer}}">
... ...
... ... @@ -76,6 +76,10 @@ export default function AddWidgetController($scope, widgetService, entityService
76 76 link = 'widgetsConfigRpc';
77 77 break;
78 78 }
  79 + case types.widgetType.alarm.value: {
  80 + link = 'widgetsConfigAlarm';
  81 + break;
  82 + }
79 83 case types.widgetType.static.value: {
80 84 link = 'widgetsConfigStatic';
81 85 break;
... ...
... ... @@ -68,6 +68,7 @@ export default function DashboardSettingsController($scope, $mdDialog, statesCon
68 68 vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)';
69 69 vm.gridSettings.columns = vm.gridSettings.columns || 24;
70 70 vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
  71 + vm.gridSettings.autoFillHeight = angular.isDefined(vm.gridSettings.autoFillHeight) ? vm.gridSettings.autoFillHeight : false;
71 72 vm.hMargin = vm.gridSettings.margins[0];
72 73 vm.vMargin = vm.gridSettings.margins[1];
73 74 vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%';
... ...
... ... @@ -121,6 +121,9 @@
121 121 </div>
122 122 </md-input-container>
123 123 </div>
  124 + <md-checkbox flex aria-label="{{ 'dashboard.autofill-height' | translate }}"
  125 + ng-model="vm.gridSettings.autoFillHeight">{{ 'dashboard.autofill-height' | translate }}
  126 + </md-checkbox>
124 127 <div flex
125 128 ng-required="false"
126 129 md-color-picker
... ...
... ... @@ -47,6 +47,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
47 47 vm.latestWidgetTypes = [];
48 48 vm.timeseriesWidgetTypes = [];
49 49 vm.rpcWidgetTypes = [];
  50 + vm.alarmWidgetTypes = [];
50 51 vm.staticWidgetTypes = [];
51 52 vm.widgetEditMode = $state.$current.data.widgetEditMode;
52 53 vm.iframeMode = $rootScope.iframeMode;
... ... @@ -263,6 +264,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
263 264 vm.latestWidgetTypes = [];
264 265 vm.timeseriesWidgetTypes = [];
265 266 vm.rpcWidgetTypes = [];
  267 + vm.alarmWidgetTypes = [];
266 268 vm.staticWidgetTypes = [];
267 269 if (vm.widgetsBundle) {
268 270 var bundleAlias = vm.widgetsBundle.alias;
... ... @@ -308,6 +310,8 @@ export default function DashboardController(types, utils, dashboardUtils, widget
308 310 vm.latestWidgetTypes.push(widget);
309 311 } else if (widgetTypeInfo.type === types.widgetType.rpc.value) {
310 312 vm.rpcWidgetTypes.push(widget);
  313 + } else if (widgetTypeInfo.type === types.widgetType.alarm.value) {
  314 + vm.alarmWidgetTypes.push(widget);
311 315 } else if (widgetTypeInfo.type === types.widgetType.static.value) {
312 316 vm.staticWidgetTypes.push(widget);
313 317 }
... ... @@ -358,21 +362,6 @@ export default function DashboardController(types, utils, dashboardUtils, widget
358 362 vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
359 363 vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils,
360 364 types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases);
361   -
362   - /* entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
363   - .then(
364   - function(resolution) {
365   - if (resolution.error && !isTenantAdmin()) {
366   - vm.configurationError = true;
367   - showAliasesResolutionError(resolution.error);
368   - } else {
369   - vm.dashboardConfiguration = vm.dashboard.configuration;
370   - vm.dashboardCtx.dashboard = vm.dashboard;
371   - vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;
372   - vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
373   - }
374   - }
375   - );*/
376 365 }, function fail() {
377 366 vm.configurationError = true;
378 367 });
... ... @@ -744,6 +733,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget
744 733 link = 'widgetsConfigRpc';
745 734 break;
746 735 }
  736 + case types.widgetType.alarm.value: {
  737 + link = 'widgetsConfigAlarm';
  738 + break;
  739 + }
747 740 case types.widgetType.static.value: {
748 741 link = 'widgetsConfigStatic';
749 742 break;
... ... @@ -851,6 +844,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
851 844 vm.timeseriesWidgetTypes = [];
852 845 vm.latestWidgetTypes = [];
853 846 vm.rpcWidgetTypes = [];
  847 + vm.alarmWidgetTypes = [];
854 848 vm.staticWidgetTypes = [];
855 849 }
856 850
... ...
... ... @@ -52,7 +52,7 @@
52 52 <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
53 53 is-toolbar
54 54 direction="left"
55   - tooltip-direction="bottom" aggregation
  55 + tooltip-direction="bottom" aggregation="true"
56 56 ng-model="vm.dashboardCtx.dashboardTimewindow">
57 57 </tb-timewindow>
58 58 <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
... ... @@ -179,6 +179,7 @@
179 179 <tb-edit-widget
180 180 dashboard="vm.dashboard"
181 181 alias-controller="vm.dashboardCtx.aliasController"
  182 + widget-edit-mode="vm.widgetEditMode"
182 183 widget="vm.editingWidget"
183 184 widget-layout="vm.editingWidgetLayout"
184 185 the-form="vm.widgetForm">
... ... @@ -205,7 +206,8 @@
205 206 </header-pane>
206 207 <div ng-if="vm.isAddingWidget">
207 208 <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||
208   - vm.rpcWidgetTypes.length > 0 || vm.staticWidgetTypes.length > 0"
  209 + vm.rpcWidgetTypes.length > 0 || vm.alarmWidgetTypes.length > 0 ||
  210 + vm.staticWidgetTypes.length > 0"
209 211 flex
210 212 class="tb-absolute-fill" md-border-bottom>
211 213 <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
... ... @@ -238,6 +240,16 @@
238 240 on-widget-clicked="vm.addWidgetFromType(event, widget)">
239 241 </tb-dashboard>
240 242 </md-tab>
  243 + <md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
  244 + <tb-dashboard
  245 + widgets="vm.alarmWidgetTypes"
  246 + is-edit="false"
  247 + is-mobile="true"
  248 + is-edit-action-enabled="false"
  249 + is-remove-action-enabled="false"
  250 + on-widget-clicked="vm.addWidgetFromType(event, widget)">
  251 + </tb-dashboard>
  252 + </md-tab>
241 253 <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
242 254 <tb-dashboard
243 255 widgets="vm.staticWidgetTypes"
... ... @@ -250,7 +262,8 @@
250 262 </md-tab>
251 263 </md-tabs>
252 264 <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&
253   - vm.rpcWidgetTypes.length === 0 && vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
  265 + vm.rpcWidgetTypes.length === 0 && vm.alarmWidgetTypes.length === 0 &&
  266 + vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
254 267 layout-align="center center"
255 268 style="text-transform: uppercase; display: flex;"
256 269 class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
... ...
... ... @@ -131,6 +131,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
131 131 scope: {
132 132 dashboard: '=',
133 133 aliasController: '=',
  134 + widgetEditMode: '=',
134 135 widget: '=',
135 136 widgetLayout: '=',
136 137 theForm: '='
... ...
... ... @@ -22,7 +22,7 @@
22 22 widget-settings-schema="settingsSchema"
23 23 datakey-settings-schema="dataKeySettingsSchema"
24 24 alias-controller="aliasController"
25   - functions-only="functionsOnly"
  25 + functions-only="widgetEditMode"
26 26 fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
27 27 on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
28 28 the-form="theForm"></tb-widget-config>
... ...
... ... @@ -49,6 +49,7 @@
49 49 state-controller="vm.dashboardCtx.stateController"
50 50 dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow"
51 51 is-edit="vm.isEdit"
  52 + autofill-height="vm.layoutCtx.gridSettings.autoFillHeight && !vm.isEdit"
52 53 is-mobile="vm.isMobile"
53 54 is-mobile-disabled="vm.widgetEditMode"
54 55 is-edit-action-enabled="vm.isEdit"
... ...
... ... @@ -53,12 +53,6 @@ export default function DashboardStateDialogController($scope, $mdDialog, $filte
53 53 if (!vm.stateIdTouched && vm.isAdd) {
54 54 vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_");
55 55 }
56   - var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true);
57   - if (result && result.length && result[0].id !== vm.prevStateId) {
58   - $scope.theForm.name.$setValidity('stateExists', false);
59   - } else {
60   - $scope.theForm.name.$setValidity('stateExists', true);
61   - }
62 56 }
63 57
64 58 function checkStateId() {
... ...
... ... @@ -37,18 +37,15 @@
37 37 <input name="name" required ng-model="vm.state.name">
38 38 <div ng-messages="theForm.name.$error">
39 39 <div ng-message="required" translate>dashboard.state-name-required</div>
40   - <div ng-message="stateExists" translate>dashboard.state-name-exists</div>
41 40 </div>
42 41 </md-input-container>
43 42 <md-input-container class="md-block">
44 43 <label translate>dashboard.state-id</label>
45   - <input name="stateId" ng-model="vm.state.id"
46   - ng-change="vm.stateIdTouched = true"
47   - ng-pattern="/^[a-zA-Z0-9_]*$/">
  44 + <input name="stateId" required ng-model="vm.state.id"
  45 + ng-change="vm.stateIdTouched = true">
48 46 <div ng-messages="theForm.stateId.$error">
49 47 <div ng-message="required" translate>dashboard.state-id-required</div>
50 48 <div ng-message="stateExists" translate>dashboard.state-id-exists</div>
51   - <div ng-message="pattern" translate>dashboard.invalid-state-id-format</div>
52 49 </div>
53 50 </md-input-container>
54 51 <md-checkbox flex aria-label="{{ 'dashboard.is-root-state' | translate }}"
... ...
... ... @@ -97,10 +97,10 @@ export default function DefaultStateController($scope, $location, $state, $state
97 97
98 98 function getStateName(id, state) {
99 99 var result = '';
100   - var translationId = types.translate.dashboardStatePrefix + id;
  100 + var translationId = types.translate.customTranslationsPrefix + state.name;
101 101 var translation = $translate.instant(translationId);
102 102 if (translation != translationId) {
103   - result = translation;
  103 + result = translation + '';
104 104 } else {
105 105 result = state.name;
106 106 }
... ...
... ... @@ -17,7 +17,7 @@
17 17 import './entity-state-controller.scss';
18 18
19 19 /*@ngInject*/
20   -export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, types, dashboardUtils, entityService) {
  20 +export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, utils, types, dashboardUtils, entityService) {
21 21
22 22 var vm = this;
23 23
... ... @@ -106,18 +106,17 @@ export default function EntityStateController($scope, $location, $state, $stateP
106 106 function getStateName(index) {
107 107 var result = '';
108 108 if (vm.stateObject[index]) {
  109 + var stateName = vm.states[vm.stateObject[index].id].name;
  110 + var translationId = types.translate.customTranslationsPrefix + stateName;
  111 + var translation = $translate.instant(translationId);
  112 + if (translation != translationId) {
  113 + stateName = translation + '';
  114 + }
109 115 var params = vm.stateObject[index].params;
110 116 if (params && params.entityName) {
111   - result = params.entityName;
  117 + result = utils.insertVariable(stateName, 'entityName', params.entityName);
112 118 } else {
113   - var id = vm.stateObject[index].id;
114   - var translationId = types.translate.dashboardStatePrefix + id;
115   - var translation = $translate.instant(translationId);
116   - if (translation != translationId) {
117   - result = translation;
118   - } else {
119   - result = vm.states[vm.stateObject[index].id].name;
120   - }
  119 + result = stateName;
121 120 }
122 121 }
123 122 return result;
... ... @@ -243,11 +242,9 @@ export default function EntityStateController($scope, $location, $state, $stateP
243 242 }
244 243
245 244 function gotoState(stateId, update, openRightLayout) {
246   - if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
247   - vm.dashboardCtrl.openDashboardState(stateId, openRightLayout);
248   - if (update) {
249   - updateLocation();
250   - }
  245 + vm.dashboardCtrl.openDashboardState(stateId, openRightLayout);
  246 + if (update) {
  247 + updateLocation();
251 248 }
252 249 }
253 250
... ...
... ... @@ -32,7 +32,7 @@
32 32 on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
33 33 on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
34 34 </md-tab>
35   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
  35 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
36 36 <tb-attribute-table flex
37 37 entity-id="vm.grid.operatingItem().id.id"
38 38 entity-type="{{vm.types.entityType.device}}"
... ... @@ -40,7 +40,7 @@
40 40 default-attribute-scope="{{vm.types.attributesScope.client.value}}">
41 41 </tb-attribute-table>
42 42 </md-tab>
43   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
  43 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
44 44 <tb-attribute-table flex
45 45 entity-id="vm.grid.operatingItem().id.id"
46 46 entity-type="{{vm.types.entityType.device}}"
... ... @@ -49,19 +49,19 @@
49 49 disable-attribute-scope-selection="true">
50 50 </tb-attribute-table>
51 51 </md-tab>
52   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
  52 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
53 53 <tb-alarm-table flex entity-type="vm.types.entityType.device"
54 54 entity-id="vm.grid.operatingItem().id.id">
55 55 </tb-alarm-table>
56 56 </md-tab>
57   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'device.events' | translate }}">
  57 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'device.events' | translate }}">
58 58 <tb-event-table flex entity-type="vm.types.entityType.device"
59 59 entity-id="vm.grid.operatingItem().id.id"
60 60 tenant-id="vm.grid.operatingItem().tenantId.id"
61 61 default-event-type="{{vm.types.eventType.error.value}}">
62 62 </tb-event-table>
63 63 </md-tab>
64   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  64 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
65 65 <tb-relation-table flex
66 66 entity-id="vm.grid.operatingItem().id.id"
67 67 entity-type="{{vm.types.entityType.device}}">
... ...
... ... @@ -87,6 +87,7 @@ export default angular.module('thingsboard.help', [])
87 87 widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries",
88 88 widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest",
89 89 widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc",
  90 + widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm",
90 91 widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static",
91 92 },
92 93 getPluginLink: function(plugin) {
... ...
... ... @@ -131,6 +131,7 @@ export default angular.module('thingsboard.locale', [])
131 131 "type": "Type",
132 132 "severity": "Severity",
133 133 "originator": "Originator",
  134 + "originator-type": "Originator type",
134 135 "details": "Details",
135 136 "status": "Status",
136 137 "alarm-details": "Alarm details",
... ... @@ -144,7 +145,17 @@ export default angular.module('thingsboard.locale', [])
144 145 "severity-warning": "Warning",
145 146 "severity-indeterminate": "Indeterminate",
146 147 "acknowledge": "Acknowledge",
147   - "clear": "Clear"
  148 + "clear": "Clear",
  149 + "search": "Search alarms",
  150 + "selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected",
  151 + "no-data": "No data to display",
  152 + "polling-interval": "Alarms polling interval (sec)",
  153 + "polling-interval-required": "Alarms polling interval is required.",
  154 + "min-polling-interval-message": "At least 1 sec polling interval is allowed.",
  155 + "aknowledge-alarms-title": "Acknowledge { count, select, 1 {1 alarm} other {# alarms} }",
  156 + "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, select, 1 {1 alarm} other {# alarms} }?",
  157 + "clear-alarms-title": "Clear { count, select, 1 {1 alarm} other {# alarms} }",
  158 + "clear-alarms-text": "Are you sure you want to clear { count, select, 1 {1 alarm} other {# alarms} }?"
148 159 },
149 160 "alias": {
150 161 "add": "Add alias",
... ... @@ -420,6 +431,7 @@ export default angular.module('thingsboard.locale', [])
420 431 "vertical-margin-required": "Vertical margin value is required.",
421 432 "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
422 433 "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
  434 + "autofill-height": "Auto fill layout height",
423 435 "display-title": "Display dashboard title",
424 436 "toolbar-always-open": "Keep toolbar opened",
425 437 "title-color": "Title color",
... ... @@ -461,11 +473,9 @@ export default angular.module('thingsboard.locale', [])
461 473 "state": "Dashboard state",
462 474 "state-name": "Name",
463 475 "state-name-required": "Dashboard state name is required.",
464   - "state-name-exists": "Dashboard state with the same name is already exists.",
465 476 "state-id": "State Id",
466 477 "state-id-required": "Dashboard state id is required.",
467 478 "state-id-exists": "Dashboard state with the same id is already exists.",
468   - "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.",
469 479 "is-root-state": "Root state",
470 480 "delete-state-title": "Delete dashboard state",
471 481 "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?",
... ... @@ -486,8 +496,10 @@ export default angular.module('thingsboard.locale', [])
486 496 "configuration": "Data key configuration",
487 497 "timeseries": "Timeseries",
488 498 "attributes": "Attributes",
  499 + "alarm": "Alarm fields",
489 500 "timeseries-required": "Entity timeseries are required.",
490 501 "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
  502 + "alarm-fields-required": "Alarm fields are required.",
491 503 "function-types": "Function types",
492 504 "function-types-required": "Function types are required."
493 505 },
... ... @@ -637,6 +649,8 @@ export default angular.module('thingsboard.locale', [])
637 649 "no-aliases-found": "No aliases found.",
638 650 "no-alias-matching": "'{{alias}}' not found.",
639 651 "create-new-alias": "Create a new one!",
  652 + "key": "Key",
  653 + "key-name": "Key name",
640 654 "no-keys-found": "No keys found.",
641 655 "no-key-matching": "'{{key}}' not found.",
642 656 "create-new-key": "Create a new one!",
... ... @@ -1046,6 +1060,7 @@ export default angular.module('thingsboard.locale', [])
1046 1060 "timeseries": "Time series",
1047 1061 "latest-values": "Latest values",
1048 1062 "rpc": "Control widget",
  1063 + "alarm": "Alarm widget",
1049 1064 "static": "Static widget",
1050 1065 "select-widget-type": "Select widget type",
1051 1066 "missing-widget-title-error": "Widget title must be specified!",
... ... @@ -1133,7 +1148,8 @@ export default angular.module('thingsboard.locale', [])
1133 1148 "datasource-parameters": "Parameters",
1134 1149 "remove-datasource": "Remove datasource",
1135 1150 "add-datasource": "Add datasource",
1136   - "target-device": "Target device"
  1151 + "target-device": "Target device",
  1152 + "alarm-source": "Alarm source"
1137 1153 },
1138 1154 "widget-type": {
1139 1155 "import": "Import widget type",
... ... @@ -1150,6 +1166,8 @@ export default angular.module('thingsboard.locale', [])
1150 1166 "zh_CN": "Chinese",
1151 1167 "ru_RU": "Russian",
1152 1168 "es_ES": "Spanish"
  1169 + },
  1170 + "custom": {
1153 1171 }
1154 1172 }
1155 1173 }
... ...
... ... @@ -18,7 +18,7 @@
18 18 export default function ThingsboardMissingTranslateHandler($log, types) {
19 19
20 20 return function (translationId) {
21   - if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) {
  21 + if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) {
22 22 $log.warn('Translation for ' + translationId + ' doesn\'t exist');
23 23 }
24 24 };
... ...
... ... @@ -31,7 +31,7 @@
31 31 on-export-plugin="vm.exportPlugin(event, vm.grid.detailsConfig.currentItem)"
32 32 on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-plugin>
33 33 </md-tab>
34   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'attribute.attributes' | translate }}">
  34 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
35 35 <tb-attribute-table flex
36 36 entity-id="vm.grid.operatingItem().id.id"
37 37 entity-type="{{vm.types.entityType.plugin}}"
... ... @@ -39,7 +39,7 @@
39 39 default-attribute-scope="{{vm.types.attributesScope.server.value}}">
40 40 </tb-attribute-table>
41 41 </md-tab>
42   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'attribute.latest-telemetry' | translate }}">
  42 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
43 43 <tb-attribute-table flex
44 44 entity-id="vm.grid.operatingItem().id.id"
45 45 entity-type="{{vm.types.entityType.plugin}}"
... ... @@ -48,19 +48,19 @@
48 48 disable-attribute-scope-selection="true">
49 49 </tb-attribute-table>
50 50 </md-tab>
51   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'alarm.alarms' | translate }}">
  51 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
52 52 <tb-alarm-table flex entity-type="vm.types.entityType.plugin"
53 53 entity-id="vm.grid.operatingItem().id.id">
54 54 </tb-alarm-table>
55 55 </md-tab>
56   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'plugin.events' | translate }}">
  56 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'plugin.events' | translate }}">
57 57 <tb-event-table flex entity-type="vm.types.entityType.plugin"
58 58 entity-id="vm.grid.operatingItem().id.id"
59 59 tenant-id="vm.grid.operatingItem().tenantId.id"
60 60 default-event-type="{{vm.types.eventType.lcEvent.value}}">
61 61 </tb-event-table>
62 62 </md-tab>
63   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" label="{{ 'relation.relations' | translate }}">
  63 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
64 64 <tb-relation-table flex
65 65 entity-id="vm.grid.operatingItem().id.id"
66 66 entity-type="{{vm.types.entityType.plugin}}">
... ...
... ... @@ -31,7 +31,7 @@
31 31 on-export-rule="vm.exportRule(event, vm.grid.detailsConfig.currentItem)"
32 32 on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule>
33 33 </md-tab>
34   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'attribute.attributes' | translate }}">
  34 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
35 35 <tb-attribute-table flex
36 36 entity-id="vm.grid.operatingItem().id.id"
37 37 entity-type="{{vm.types.entityType.rule}}"
... ... @@ -39,7 +39,7 @@
39 39 default-attribute-scope="{{vm.types.attributesScope.server.value}}">
40 40 </tb-attribute-table>
41 41 </md-tab>
42   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'attribute.latest-telemetry' | translate }}">
  42 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
43 43 <tb-attribute-table flex
44 44 entity-id="vm.grid.operatingItem().id.id"
45 45 entity-type="{{vm.types.entityType.rule}}"
... ... @@ -48,19 +48,19 @@
48 48 disable-attribute-scope-selection="true">
49 49 </tb-attribute-table>
50 50 </md-tab>
51   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'alarm.alarms' | translate }}">
  51 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
52 52 <tb-alarm-table flex entity-type="vm.types.entityType.rule"
53 53 entity-id="vm.grid.operatingItem().id.id">
54 54 </tb-alarm-table>
55 55 </md-tab>
56   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'rule.events' | translate }}">
  56 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rule.events' | translate }}">
57 57 <tb-event-table flex entity-type="vm.types.entityType.rule"
58 58 entity-id="vm.grid.operatingItem().id.id"
59 59 tenant-id="vm.grid.operatingItem().tenantId.id"
60 60 default-event-type="{{vm.types.eventType.lcEvent.value}}">
61 61 </tb-event-table>
62 62 </md-tab>
63   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" label="{{ 'relation.relations' | translate }}">
  63 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
64 64 <tb-relation-table flex
65 65 entity-id="vm.grid.operatingItem().id.id"
66 66 entity-type="{{vm.types.entityType.rule}}">
... ...
... ... @@ -29,7 +29,7 @@
29 29 on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)"
30 30 on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant>
31 31 </md-tab>
32   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
  32 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
33 33 <tb-attribute-table flex
34 34 entity-id="vm.grid.operatingItem().id.id"
35 35 entity-type="{{vm.types.entityType.tenant}}"
... ... @@ -37,7 +37,7 @@
37 37 default-attribute-scope="{{vm.types.attributesScope.server.value}}">
38 38 </tb-attribute-table>
39 39 </md-tab>
40   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
  40 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
41 41 <tb-attribute-table flex
42 42 entity-id="vm.grid.operatingItem().id.id"
43 43 entity-type="{{vm.types.entityType.tenant}}"
... ... @@ -46,19 +46,19 @@
46 46 disable-attribute-scope-selection="true">
47 47 </tb-attribute-table>
48 48 </md-tab>
49   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
  49 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
50 50 <tb-alarm-table flex entity-type="vm.types.entityType.tenant"
51 51 entity-id="vm.grid.operatingItem().id.id">
52 52 </tb-alarm-table>
53 53 </md-tab>
54   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'tenant.events' | translate }}">
  54 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'tenant.events' | translate }}">
55 55 <tb-event-table flex entity-type="vm.types.entityType.tenant"
56 56 entity-id="vm.grid.operatingItem().id.id"
57 57 tenant-id="vm.types.id.nullUid"
58 58 default-event-type="{{vm.types.eventType.error.value}}">
59 59 </tb-event-table>
60 60 </md-tab>
61   - <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  61 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
62 62 <tb-relation-table flex
63 63 entity-id="vm.grid.operatingItem().id.id"
64 64 entity-type="{{vm.types.entityType.tenant}}">
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './alarms-table-widget.scss';
  18 +
  19 +/* eslint-disable import/no-unresolved, import/default */
  20 +
  21 +import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html';
  22 +import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html';
  23 +
  24 +/* eslint-enable import/no-unresolved, import/default */
  25 +
  26 +import tinycolor from 'tinycolor2';
  27 +import cssjs from '../../../vendor/css.js/css';
  28 +
  29 +export default angular.module('thingsboard.widgets.alarmsTableWidget', [])
  30 + .directive('tbAlarmsTableWidget', AlarmsTableWidget)
  31 + .name;
  32 +
  33 +/*@ngInject*/
  34 +function AlarmsTableWidget() {
  35 + return {
  36 + restrict: "E",
  37 + scope: true,
  38 + bindToController: {
  39 + tableId: '=',
  40 + ctx: '='
  41 + },
  42 + controller: AlarmsTableWidgetController,
  43 + controllerAs: 'vm',
  44 + templateUrl: alarmsTableWidgetTemplate
  45 + };
  46 +}
  47 +
  48 +/*@ngInject*/
  49 +function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, alarmService, utils, types) {
  50 + var vm = this;
  51 +
  52 + vm.stylesInfo = {};
  53 + vm.contentsInfo = {};
  54 + vm.columnWidth = {};
  55 +
  56 + vm.showData = true;
  57 + vm.hasData = false;
  58 +
  59 + vm.alarms = [];
  60 + vm.alarmsCount = 0;
  61 + vm.selectedAlarms = []
  62 +
  63 + vm.alarmSource = null;
  64 + vm.allAlarms = null;
  65 +
  66 + vm.currentAlarm = null;
  67 +
  68 + vm.enableSelection = true;
  69 + vm.displayDetails = true;
  70 + vm.allowAcknowledgment = true;
  71 + vm.allowClear = true;
  72 + vm.displayPagination = true;
  73 + vm.defaultPageSize = 10;
  74 + vm.defaultSortOrder = '-'+types.alarmFields.createdTime.value;
  75 +
  76 + vm.query = {
  77 + order: vm.defaultSortOrder,
  78 + limit: vm.defaultPageSize,
  79 + page: 1,
  80 + search: null
  81 + };
  82 +
  83 + vm.searchAction = {
  84 + name: 'action.search',
  85 + show: true,
  86 + onAction: function() {
  87 + vm.enterFilterMode();
  88 + },
  89 + icon: 'search'
  90 + };
  91 +
  92 + vm.enterFilterMode = enterFilterMode;
  93 + vm.exitFilterMode = exitFilterMode;
  94 + vm.onReorder = onReorder;
  95 + vm.onPaginate = onPaginate;
  96 + vm.onRowClick = onRowClick;
  97 + vm.isCurrent = isCurrent;
  98 + vm.openAlarmDetails = openAlarmDetails;
  99 + vm.ackAlarms = ackAlarms;
  100 + vm.clearAlarms = clearAlarms;
  101 +
  102 + vm.cellStyle = cellStyle;
  103 + vm.cellContent = cellContent;
  104 +
  105 + $scope.$watch('vm.ctx', function() {
  106 + if (vm.ctx) {
  107 + vm.settings = vm.ctx.settings;
  108 + vm.widgetConfig = vm.ctx.widgetConfig;
  109 + vm.subscription = vm.ctx.defaultSubscription;
  110 + vm.alarmSource = vm.subscription.alarmSource;
  111 + initializeConfig();
  112 + updateAlarmSource();
  113 + }
  114 + });
  115 +
  116 + $scope.$watch("vm.query.search", function(newVal, prevVal) {
  117 + if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
  118 + updateAlarms();
  119 + }
  120 + });
  121 +
  122 + $scope.$on('alarms-table-data-updated', function(event, tableId) {
  123 + if (vm.tableId == tableId) {
  124 + if (vm.subscription) {
  125 + vm.allAlarms = vm.subscription.alarms;
  126 + updateAlarms(true);
  127 + $scope.$digest();
  128 + }
  129 + }
  130 + });
  131 +
  132 + $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
  133 + vm.isGtXs = isGtXs;
  134 + });
  135 +
  136 + $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
  137 + vm.isGtMd = isGtMd;
  138 + if (vm.isGtMd) {
  139 + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
  140 + } else {
  141 + vm.limitOptions = null;
  142 + }
  143 + });
  144 +
  145 + $scope.$watch('vm.selectedAlarms.length', function (newLength) {
  146 + var selectionMode = newLength ? true : false;
  147 + if (vm.ctx) {
  148 + if (selectionMode) {
  149 + vm.ctx.hideTitlePanel = true;
  150 + } else if (vm.query.search == null) {
  151 + vm.ctx.hideTitlePanel = false;
  152 + }
  153 + }
  154 + });
  155 +
  156 + function initializeConfig() {
  157 +
  158 + vm.ctx.widgetActions = [ vm.searchAction ];
  159 +
  160 + if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
  161 + var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle;
  162 + var translation = $translate.instant(translationId);
  163 + if (translation != translationId) {
  164 + vm.alarmsTitle = translation + '';
  165 + } else {
  166 + vm.alarmsTitle = vm.settings.alarmsTitle;
  167 + }
  168 + } else {
  169 + vm.alarmsTitle = $translate.instant('alarm.alarms');
  170 + }
  171 +
  172 + vm.ctx.widgetTitle = vm.alarmsTitle;
  173 +
  174 + vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true;
  175 + vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
  176 + vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true;
  177 + vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true;
  178 + vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true;
  179 + if (!vm.allowAcknowledgment && !vm.allowClear) {
  180 + vm.enableSelection = false;
  181 + }
  182 +
  183 + vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
  184 +
  185 + var pageSize = vm.settings.defaultPageSize;
  186 + if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) {
  187 + vm.defaultPageSize = pageSize;
  188 + }
  189 +
  190 + if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) {
  191 + vm.defaultSortOrder = vm.settings.defaultSortOrder;
  192 + }
  193 +
  194 + vm.query.order = vm.defaultSortOrder;
  195 + vm.query.limit = vm.defaultPageSize;
  196 + if (vm.isGtMd) {
  197 + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
  198 + } else {
  199 + vm.limitOptions = null;
  200 + }
  201 +
  202 + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
  203 + var defaultColor = tinycolor(origColor);
  204 + var mdDark = defaultColor.setAlpha(0.87).toRgbString();
  205 + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
  206 + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
  207 + //var mdDarkIcon = mdDarkSecondary;
  208 + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
  209 +
  210 + var cssString = 'table.md-table th.md-column {\n'+
  211 + 'color: ' + mdDarkSecondary + ';\n'+
  212 + '}\n'+
  213 + 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+
  214 + 'border-color: ' + mdDarkSecondary + ';\n'+
  215 + '}\n'+
  216 + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+
  217 + 'color: ' + mdDarkDisabled + ';\n'+
  218 + '}\n'+
  219 + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
  220 + 'color: ' + mdDark + ';\n'+
  221 + '}\n'+
  222 + 'table.md-table td.md-cell {\n'+
  223 + 'color: ' + mdDark + ';\n'+
  224 + 'border-top: 1px '+mdDarkDivider+' solid;\n'+
  225 + '}\n'+
  226 + 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
  227 + 'border-color: ' + mdDarkSecondary + ';\n'+
  228 + '}\n'+
  229 + 'table.md-table td.md-cell.md-placeholder {\n'+
  230 + 'color: ' + mdDarkDisabled + ';\n'+
  231 + '}\n'+
  232 + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
  233 + 'color: ' + mdDarkSecondary + ';\n'+
  234 + '}\n'+
  235 + '.md-table-pagination {\n'+
  236 + 'color: ' + mdDarkSecondary + ';\n'+
  237 + 'border-top: 1px '+mdDarkDivider+' solid;\n'+
  238 + '}\n'+
  239 + '.md-table-pagination .buttons md-icon {\n'+
  240 + 'color: ' + mdDarkSecondary + ';\n'+
  241 + '}\n'+
  242 + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
  243 + 'color: ' + mdDarkSecondary + ';\n'+
  244 + '}';
  245 +
  246 + var cssParser = new cssjs();
  247 + cssParser.testMode = false;
  248 + var namespace = 'ts-table-' + hashCode(cssString);
  249 + cssParser.cssPreviewNamespace = namespace;
  250 + cssParser.createStyleElement(namespace, cssString);
  251 + $element.addClass(namespace);
  252 +
  253 + function hashCode(str) {
  254 + var hash = 0;
  255 + var i, char;
  256 + if (str.length === 0) return hash;
  257 + for (i = 0; i < str.length; i++) {
  258 + char = str.charCodeAt(i);
  259 + hash = ((hash << 5) - hash) + char;
  260 + hash = hash & hash;
  261 + }
  262 + return hash;
  263 + }
  264 + }
  265 +
  266 + function enterFilterMode () {
  267 + vm.query.search = '';
  268 + vm.ctx.hideTitlePanel = true;
  269 + }
  270 +
  271 + function exitFilterMode () {
  272 + vm.query.search = null;
  273 + updateAlarms();
  274 + vm.ctx.hideTitlePanel = false;
  275 + }
  276 +
  277 + function onReorder () {
  278 + updateAlarms();
  279 + }
  280 +
  281 + function onPaginate () {
  282 + updateAlarms();
  283 + }
  284 +
  285 + function onRowClick($event, alarm) {
  286 + if (vm.currentAlarm != alarm) {
  287 + vm.currentAlarm = alarm;
  288 + }
  289 + }
  290 +
  291 + function isCurrent(alarm) {
  292 + return (vm.currentAlarm && alarm && vm.currentAlarm.id && alarm.id) &&
  293 + (vm.currentAlarm.id.id === alarm.id.id);
  294 + }
  295 +
  296 + function openAlarmDetails($event, alarm) {
  297 + if (alarm && alarm.id) {
  298 + var onShowingCallback = {
  299 + onShowing: function(){}
  300 + }
  301 + $mdDialog.show({
  302 + controller: 'AlarmDetailsDialogController',
  303 + controllerAs: 'vm',
  304 + templateUrl: alarmDetailsDialogTemplate,
  305 + locals: {
  306 + alarmId: alarm.id.id,
  307 + allowAcknowledgment: vm.allowAcknowledgment,
  308 + allowClear: vm.allowClear,
  309 + showingCallback: onShowingCallback
  310 + },
  311 + parent: angular.element($document[0].body),
  312 + targetEvent: $event,
  313 + fullscreen: true,
  314 + skipHide: true,
  315 + onShowing: function(scope, element) {
  316 + onShowingCallback.onShowing(scope, element);
  317 + }
  318 + }).then(function (alarm) {
  319 + if (alarm) {
  320 + vm.subscription.update();
  321 + }
  322 + });
  323 +
  324 + }
  325 + }
  326 +
  327 + function ackAlarms($event) {
  328 + if ($event) {
  329 + $event.stopPropagation();
  330 + }
  331 + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) {
  332 + var title = $translate.instant('alarm.aknowledge-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat');
  333 + var content = $translate.instant('alarm.aknowledge-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat');
  334 + var confirm = $mdDialog.confirm()
  335 + .targetEvent($event)
  336 + .title(title)
  337 + .htmlContent(content)
  338 + .ariaLabel(title)
  339 + .cancel($translate.instant('action.no'))
  340 + .ok($translate.instant('action.yes'));
  341 + $mdDialog.show(confirm).then(function () {
  342 + var tasks = [];
  343 + for (var i=0;i<vm.selectedAlarms.length;i++) {
  344 + var alarm = vm.selectedAlarms[i];
  345 + if (alarm.id) {
  346 + tasks.push(alarmService.ackAlarm(alarm.id.id));
  347 + }
  348 + }
  349 + if (tasks.length) {
  350 + $q.all(tasks).then(function () {
  351 + vm.selectedAlarms = [];
  352 + vm.subscription.update();
  353 + });
  354 + }
  355 +
  356 + });
  357 + }
  358 + }
  359 +
  360 + function clearAlarms($event) {
  361 + if ($event) {
  362 + $event.stopPropagation();
  363 + }
  364 + if (vm.selectedAlarms && vm.selectedAlarms.length > 0) {
  365 + var title = $translate.instant('alarm.clear-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat');
  366 + var content = $translate.instant('alarm.clear-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat');
  367 + var confirm = $mdDialog.confirm()
  368 + .targetEvent($event)
  369 + .title(title)
  370 + .htmlContent(content)
  371 + .ariaLabel(title)
  372 + .cancel($translate.instant('action.no'))
  373 + .ok($translate.instant('action.yes'));
  374 + $mdDialog.show(confirm).then(function () {
  375 + var tasks = [];
  376 + for (var i=0;i<vm.selectedAlarms.length;i++) {
  377 + var alarm = vm.selectedAlarms[i];
  378 + if (alarm.id) {
  379 + tasks.push(alarmService.clearAlarm(alarm.id.id));
  380 + }
  381 + }
  382 + if (tasks.length) {
  383 + $q.all(tasks).then(function () {
  384 + vm.selectedAlarms = [];
  385 + vm.subscription.update();
  386 + });
  387 + }
  388 +
  389 + });
  390 + }
  391 + }
  392 +
  393 +
  394 + function updateAlarms(preserveSelections) {
  395 + if (!preserveSelections) {
  396 + vm.selectedAlarms = [];
  397 + }
  398 + var result = $filter('orderBy')(vm.allAlarms, vm.query.order);
  399 + if (vm.query.search != null) {
  400 + result = $filter('filter')(result, {$: vm.query.search});
  401 + }
  402 + vm.alarmsCount = result.length;
  403 +
  404 + if (vm.displayPagination) {
  405 + var startIndex = vm.query.limit * (vm.query.page - 1);
  406 + vm.alarms = result.slice(startIndex, startIndex + vm.query.limit);
  407 + } else {
  408 + vm.alarms = result;
  409 + }
  410 +
  411 + if (preserveSelections) {
  412 + var newSelectedAlarms = [];
  413 + if (vm.selectedAlarms && vm.selectedAlarms.length) {
  414 + var i = vm.selectedAlarms.length;
  415 + while (i--) {
  416 + var selectedAlarm = vm.selectedAlarms[i];
  417 + if (selectedAlarm.id) {
  418 + result = $filter('filter')(vm.alarms, {id: {id: selectedAlarm.id.id} });
  419 + if (result && result.length) {
  420 + newSelectedAlarms.push(result[0]);
  421 + }
  422 + }
  423 + }
  424 + }
  425 + vm.selectedAlarms = newSelectedAlarms;
  426 + }
  427 + }
  428 +
  429 + function cellStyle(alarm, key) {
  430 + var style = {};
  431 + if (alarm && key) {
  432 + var styleInfo = vm.stylesInfo[key.label];
  433 + var value = getAlarmValue(alarm, key);
  434 + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
  435 + try {
  436 + style = styleInfo.cellStyleFunction(value);
  437 + } catch (e) {
  438 + style = {};
  439 + }
  440 + } else {
  441 + style = defaultStyle(key, value);
  442 + }
  443 + }
  444 + if (!style.width) {
  445 + var columnWidth = vm.columnWidth[key.label];
  446 + style.width = columnWidth;
  447 + }
  448 + return style;
  449 + }
  450 +
  451 + function cellContent(alarm, key) {
  452 + var strContent = '';
  453 + if (alarm && key) {
  454 + var contentInfo = vm.contentsInfo[key.label];
  455 + var value = getAlarmValue(alarm, key);
  456 + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
  457 + if (angular.isDefined(value)) {
  458 + strContent = '' + value;
  459 + }
  460 + var content = strContent;
  461 + try {
  462 + content = contentInfo.cellContentFunction(value, alarm, $filter);
  463 + } catch (e) {
  464 + content = strContent;
  465 + }
  466 + } else {
  467 + content = defaultContent(key, value);
  468 + }
  469 + return content;
  470 + } else {
  471 + return strContent;
  472 + }
  473 + }
  474 +
  475 + function defaultContent(key, value) {
  476 + if (angular.isDefined(value)) {
  477 + var alarmField = types.alarmFields[key.name];
  478 + if (alarmField) {
  479 + if (alarmField.time) {
  480 + return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss');
  481 + } else if (alarmField.value == types.alarmFields.severity.value) {
  482 + return $translate.instant(types.alarmSeverity[value].name);
  483 + } else if (alarmField.value == types.alarmFields.status.value) {
  484 + return $translate.instant('alarm.display-status.'+value);
  485 + } else if (alarmField.value == types.alarmFields.originatorType.value) {
  486 + return $translate.instant(types.entityTypeTranslations[value].type);
  487 + }
  488 + else {
  489 + return value;
  490 + }
  491 + } else {
  492 + return value;
  493 + }
  494 + } else {
  495 + return '';
  496 + }
  497 + }
  498 + function defaultStyle(key, value) {
  499 + if (angular.isDefined(value)) {
  500 + var alarmField = types.alarmFields[key.name];
  501 + if (alarmField) {
  502 + if (alarmField.value == types.alarmFields.severity.value) {
  503 + return {
  504 + fontWeight: 'bold',
  505 + color: types.alarmSeverity[value].color
  506 + };
  507 + } else {
  508 + return {};
  509 + }
  510 + } else {
  511 + return {};
  512 + }
  513 + } else {
  514 + return {};
  515 + }
  516 + }
  517 +
  518 + const getDescendantProp = (obj, path) => (
  519 + path.split('.').reduce((acc, part) => acc && acc[part], obj)
  520 + );
  521 +
  522 + function getAlarmValue(alarm, key) {
  523 + var alarmField = types.alarmFields[key.name];
  524 + if (alarmField) {
  525 + return getDescendantProp(alarm, alarmField.value);
  526 + } else {
  527 + return getDescendantProp(alarm, key.name);
  528 + }
  529 + }
  530 +
  531 + function updateAlarmSource() {
  532 +
  533 + vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle);
  534 +
  535 + vm.stylesInfo = {};
  536 + vm.contentsInfo = {};
  537 + vm.columnWidth = {};
  538 +
  539 + for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
  540 + var dataKey = vm.alarmSource.dataKeys[d];
  541 +
  542 + var translationId = types.translate.customTranslationsPrefix + dataKey.label;
  543 + var translation = $translate.instant(translationId);
  544 + if (translation != translationId) {
  545 + dataKey.title = translation + '';
  546 + } else {
  547 + dataKey.title = dataKey.label;
  548 + }
  549 +
  550 + var keySettings = dataKey.settings;
  551 +
  552 + var cellStyleFunction = null;
  553 + var useCellStyleFunction = false;
  554 +
  555 + if (keySettings.useCellStyleFunction === true) {
  556 + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
  557 + try {
  558 + cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
  559 + useCellStyleFunction = true;
  560 + } catch (e) {
  561 + cellStyleFunction = null;
  562 + useCellStyleFunction = false;
  563 + }
  564 + }
  565 + }
  566 +
  567 + vm.stylesInfo[dataKey.label] = {
  568 + useCellStyleFunction: useCellStyleFunction,
  569 + cellStyleFunction: cellStyleFunction
  570 + };
  571 +
  572 + var cellContentFunction = null;
  573 + var useCellContentFunction = false;
  574 +
  575 + if (keySettings.useCellContentFunction === true) {
  576 + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
  577 + try {
  578 + cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction);
  579 + useCellContentFunction = true;
  580 + } catch (e) {
  581 + cellContentFunction = null;
  582 + useCellContentFunction = false;
  583 + }
  584 + }
  585 + }
  586 +
  587 + vm.contentsInfo[dataKey.label] = {
  588 + useCellContentFunction: useCellContentFunction,
  589 + cellContentFunction: cellContentFunction
  590 + };
  591 +
  592 + var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
  593 + vm.columnWidth[dataKey.label] = columnWidth;
  594 + }
  595 + }
  596 +
  597 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-has-timewindow {
  18 + .tb-alarms-table {
  19 + md-toolbar {
  20 + min-height: 60px;
  21 + max-height: 60px;
  22 + &.md-table-toolbar {
  23 + .md-toolbar-tools {
  24 + max-height: 60px;
  25 + }
  26 + }
  27 + }
  28 + }
  29 +}
  30 +
  31 +.tb-alarms-table {
  32 +
  33 + md-toolbar {
  34 + min-height: 39px;
  35 + max-height: 39px;
  36 + &.md-table-toolbar {
  37 + .md-toolbar-tools {
  38 + max-height: 39px;
  39 + }
  40 + }
  41 + }
  42 +
  43 + &.tb-data-table {
  44 + table.md-table, table.md-table.md-row-select {
  45 + tbody {
  46 + tr {
  47 + td {
  48 + &.tb-action-cell {
  49 + min-width: 36px;
  50 + max-width: 36px;
  51 + width: 36px;
  52 + }
  53 + }
  54 + }
  55 + }
  56 + }
  57 + }
  58 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-absolute-fill tb-alarms-table tb-data-table" layout="column">
  19 + <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
  20 + <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedAlarms.length &&
  21 + vm.query.search != null">
  22 + <div class="md-toolbar-tools">
  23 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  24 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  25 + <md-tooltip md-direction="top">
  26 + {{'alarm.search' | translate}}
  27 + </md-tooltip>
  28 + </md-button>
  29 + <md-input-container flex>
  30 + <label>&nbsp;</label>
  31 + <input ng-model="vm.query.search" placeholder="{{'alarm.search' | translate}}"/>
  32 + </md-input-container>
  33 + <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
  34 + <md-icon aria-label="Close" class="material-icons">close</md-icon>
  35 + <md-tooltip md-direction="top">
  36 + {{ 'action.close' | translate }}
  37 + </md-tooltip>
  38 + </md-button>
  39 + </div>
  40 + </md-toolbar>
  41 + <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedAlarms.length">
  42 + <div class="md-toolbar-tools">
  43 + <span translate="alarm.selected-alarms"
  44 + translate-values="{count: vm.selectedAlarms.length}"
  45 + translate-interpolation="messageformat"></span>
  46 + <span flex></span>
  47 + <md-button ng-if="vm.allowAcknowledgment" class="md-icon-button" ng-click="vm.ackAlarms($event)">
  48 + <md-icon>done</md-icon>
  49 + <md-tooltip md-direction="top">
  50 + {{ 'alarm.acknowledge' | translate }}
  51 + </md-tooltip>
  52 + </md-button>
  53 + <md-button ng-if="vm.allowClear" class="md-icon-button" ng-click="vm.clearAlarms($event)">
  54 + <md-icon>clear</md-icon>
  55 + <md-tooltip md-direction="top">
  56 + {{ 'alarm.clear' | translate }}
  57 + </md-tooltip>
  58 + </md-button>
  59 + </div>
  60 + </md-toolbar>
  61 + <md-table-container flex>
  62 + <table md-table md-row-select="vm.enableSelection" multiple="" ng-model="vm.selectedAlarms">
  63 + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
  64 + <tr md-row>
  65 + <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.alarmSource.dataKeys"><span>{{ key.title }}</span></th>
  66 + <th md-column ng-if="vm.displayDetails"><span>&nbsp</span></th>
  67 + </tr>
  68 + </thead>
  69 + <tbody md-body>
  70 + <tr ng-show="vm.alarms.length" md-row md-select="alarm"
  71 + md-select-id="id.id" md-auto-select="false" ng-repeat="alarm in vm.alarms"
  72 + ng-click="vm.onRowClick($event, alarm)" ng-class="{'tb-current': vm.isCurrent(alarm)}">
  73 + <td md-cell flex ng-repeat="key in vm.alarmSource.dataKeys"
  74 + ng-style="vm.cellStyle(alarm, key)"
  75 + ng-bind-html="vm.cellContent(alarm, key)">
  76 + </td>
  77 + <td md-cell ng-if="vm.displayDetails" class="tb-action-cell">
  78 + <md-button class="md-icon-button" aria-label="{{ 'alarm.details' | translate }}"
  79 + ng-click="vm.openAlarmDetails($event, alarm)">
  80 + <md-icon aria-label="{{ 'alarm.details' | translate }}" class="material-icons">more_horiz</md-icon>
  81 + <md-tooltip md-direction="top">
  82 + {{ 'alarm.details' | translate }}
  83 + </md-tooltip>
  84 + </md-button>
  85 + </td>
  86 + </tr>
  87 + </tbody>
  88 + </table>
  89 + <md-divider></md-divider>
  90 + <span ng-show="!vm.alarms.length"
  91 + layout-align="center center"
  92 + class="no-data-found" translate>alarm.no-alarms-prompt</span>
  93 + </md-table-container>
  94 + <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions"
  95 + md-page="vm.query.page" md-total="{{vm.alarmsCount}}"
  96 + md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd">
  97 + </md-table-pagination>
  98 + </div>
  99 + <span ng-show="!vm.showData"
  100 + layout-align="center center"
  101 + style="text-transform: uppercase; display: flex;"
  102 + class="tb-absolute-fill" translate>alarm.no-data</span>
  103 +</div>
... ...
... ... @@ -54,6 +54,13 @@
54 54 <span translate>{{vm.types.widgetType.rpc.name}}</span>
55 55 </md-button>
56 56 <md-button class="tb-card-button md-raised md-primary" layout="column"
  57 + ng-click="vm.typeSelected(vm.types.widgetType.alarm.value)">
  58 + <md-icon class="material-icons tb-md-96"
  59 + aria-label="{{ vm.types.widgetType.alarm.name | translate }}">error
  60 + </md-icon>
  61 + <span translate>{{vm.types.widgetType.alarm.name}}</span>
  62 + </md-button>
  63 + <md-button class="tb-card-button md-raised md-primary" layout="column"
57 64 ng-click="vm.typeSelected(vm.types.widgetType.static.value)">
58 65 <md-icon class="material-icons tb-md-96"
59 66 aria-label="{{ vm.types.widgetType.static.name | translate }}">font_download
... ...
... ... @@ -329,10 +329,14 @@ export default function WidgetEditorController(widgetService, userService, types
329 329 $scope.$watch('vm.widget.type', function (newVal, oldVal) {
330 330 if (!angular.equals(newVal, oldVal)) {
331 331 var config = angular.fromJson(vm.widget.defaultConfig);
332   - if (vm.widget.type !== types.widgetType.rpc.value) {
  332 + if (vm.widget.type !== types.widgetType.rpc.value
  333 + && vm.widget.type !== types.widgetType.alarm.value) {
333 334 if (config.targetDeviceAliases) {
334 335 delete config.targetDeviceAliases;
335 336 }
  337 + if (config.alarmSource) {
  338 + delete config.alarmSource;
  339 + }
336 340 if (!config.datasources) {
337 341 config.datasources = [];
338 342 }
... ... @@ -346,22 +350,38 @@ export default function WidgetEditorController(widgetService, userService, types
346 350 for (var i = 0; i < config.datasources.length; i++) {
347 351 var datasource = config.datasources[i];
348 352 datasource.type = vm.widget.type;
349   - if (vm.widget.type !== types.widgetType.timeseries.value && datasource.intervalSec) {
350   - delete datasource.intervalSec;
351   - } else if (vm.widget.type === types.widgetType.timeseries.value && !datasource.intervalSec) {
352   - datasource.intervalSec = 60;
353   - }
354 353 }
355   - } else {
  354 + } else if (vm.widget.type == types.widgetType.rpc.value) {
356 355 if (config.datasources) {
357 356 delete config.datasources;
358 357 }
  358 + if (config.alarmSource) {
  359 + delete config.alarmSource;
  360 + }
359 361 if (config.timewindow) {
360 362 delete config.timewindow;
361 363 }
362 364 if (!config.targetDeviceAliases) {
363 365 config.targetDeviceAliases = [];
364 366 }
  367 + } else { // alarm
  368 + if (config.datasources) {
  369 + delete config.datasources;
  370 + }
  371 + if (config.targetDeviceAliases) {
  372 + delete config.targetDeviceAliases;
  373 + }
  374 + if (!config.alarmSource) {
  375 + config.alarmSource = {};
  376 + config.alarmSource.type = vm.widget.type
  377 + }
  378 + if (!config.timewindow) {
  379 + config.timewindow = {
  380 + realtime: {
  381 + timewindowMs: 24 * 60 * 60 * 1000
  382 + }
  383 + };
  384 + }
365 385 }
366 386 vm.widget.defaultConfig = angular.toJson(config);
367 387 }
... ...
... ... @@ -281,7 +281,61 @@ pre.tb-highlight {
281 281 display: flex;
282 282 }
283 283 table.md-table {
  284 + &.md-row-select td.md-cell,
  285 + &.md-row-select th.md-column {
  286 + &:first-child {
  287 + width: 20px;
  288 + padding: 0 0 0 12px;
  289 + }
  290 +
  291 + &:nth-child(2) {
  292 + padding: 0 12px;
  293 + }
  294 +
  295 + &:nth-child(n+3):nth-last-child(n+2) {
  296 + padding: 0 28px 0 0;
  297 + }
  298 + }
  299 +
  300 + &:not(.md-row-select) td.md-cell,
  301 + &:not(.md-row-select) th.md-column {
  302 + &:first-child {
  303 + padding: 0 12px;
  304 + }
  305 +
  306 + &:nth-child(n+2):nth-last-child(n+2) {
  307 + padding: 0 28px 0 0;
  308 + }
  309 + }
  310 +
  311 + td.md-cell,
  312 + th.md-column {
  313 +
  314 + &:last-child {
  315 + padding: 0 12px 0 0;
  316 + }
  317 +
  318 + }
  319 + }
  320 +
  321 + table.md-table, table.md-table.md-row-select {
284 322 tbody {
  323 + &.md-body {
  324 + tr {
  325 + &.md-row:not([disabled]) {
  326 + outline: none;
  327 + &:hover {
  328 + background-color: rgba(221, 221, 221, 0.3) !important;
  329 + }
  330 + &.md-selected {
  331 + background-color: rgba(221, 221, 221, 0.5) !important;
  332 + }
  333 + &.tb-current, &.tb-current:hover{
  334 + background-color: rgba(221, 221, 221, 0.65) !important;
  335 + }
  336 + }
  337 + }
  338 + }
285 339 tr {
286 340 td {
287 341 &.tb-action-cell {
... ...