Commit 193ef7ddbd3b506d6492c00b5d6cfcf137d30eb7

Authored by Igor Kulikov
1 parent 087cd95b

UI: Implement timewindow control on dashboard level

@@ -74,7 +74,7 @@ export default class DataAggregator { @@ -74,7 +74,7 @@ export default class DataAggregator {
74 }, this.aggregationTimeout, false); 74 }, this.aggregationTimeout, false);
75 } 75 }
76 76
77 - onData(data, update, history) { 77 + onData(data, update, history, apply) {
78 if (!this.dataReceived || this.resetPending) { 78 if (!this.dataReceived || this.resetPending) {
79 var updateIntervalScheduledTime = true; 79 var updateIntervalScheduledTime = true;
80 if (!this.dataReceived) { 80 if (!this.dataReceived) {
@@ -96,18 +96,18 @@ export default class DataAggregator { @@ -96,18 +96,18 @@ export default class DataAggregator {
96 if (updateIntervalScheduledTime) { 96 if (updateIntervalScheduledTime) {
97 this.intervalScheduledTime = currentTime(); 97 this.intervalScheduledTime = currentTime();
98 } 98 }
99 - this.onInterval(history); 99 + this.onInterval(history, apply);
100 } else { 100 } else {
101 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, 101 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
102 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); 102 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
103 if (history) { 103 if (history) {
104 this.intervalScheduledTime = currentTime(); 104 this.intervalScheduledTime = currentTime();
105 - this.onInterval(history); 105 + this.onInterval(history, apply);
106 } 106 }
107 } 107 }
108 } 108 }
109 109
110 - onInterval(history) { 110 + onInterval(history, apply) {
111 var now = currentTime(); 111 var now = currentTime();
112 this.elapsed += now - this.intervalScheduledTime; 112 this.elapsed += now - this.intervalScheduledTime;
113 this.intervalScheduledTime = now; 113 this.intervalScheduledTime = now;
@@ -127,7 +127,7 @@ export default class DataAggregator { @@ -127,7 +127,7 @@ export default class DataAggregator {
127 this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit); 127 this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
128 } 128 }
129 if (this.onDataCb) { 129 if (this.onDataCb) {
130 - this.onDataCb(this.data, this.startTs, this.endTs); 130 + this.onDataCb(this.data, this.startTs, this.endTs, apply);
131 } 131 }
132 132
133 var self = this; 133 var self = this;
@@ -197,7 +197,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -197,7 +197,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
197 var datasourceKey = key + '_' + i; 197 var datasourceKey = key + '_' + i;
198 listener.dataUpdated(datasourceData[datasourceKey], 198 listener.dataUpdated(datasourceData[datasourceKey],
199 listener.datasourceIndex, 199 listener.datasourceIndex,
200 - dataKey.index); 200 + dataKey.index, false);
201 } 201 }
202 } 202 }
203 } else { 203 } else {
@@ -205,7 +205,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -205,7 +205,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
205 dataKey = dataKeys[key]; 205 dataKey = dataKeys[key];
206 listener.dataUpdated(datasourceData[key], 206 listener.dataUpdated(datasourceData[key],
207 listener.datasourceIndex, 207 listener.datasourceIndex,
208 - dataKey.index); 208 + dataKey.index, false);
209 } 209 }
210 } 210 }
211 } 211 }
@@ -264,7 +264,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -264,7 +264,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
264 type: types.dataKeyType.timeseries, 264 type: types.dataKeyType.timeseries,
265 onData: function (data) { 265 onData: function (data) {
266 if (data.data) { 266 if (data.data) {
267 - onData(data.data, types.dataKeyType.timeseries); 267 + onData(data.data, types.dataKeyType.timeseries, null, null, true);
268 } 268 }
269 }, 269 },
270 onReconnected: function() {} 270 onReconnected: function() {}
@@ -287,9 +287,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -287,9 +287,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
287 287
288 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 288 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
289 updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw); 289 updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
290 - dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames); 290 + dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.timeseries);
291 subscriber.onData = function(data) { 291 subscriber.onData = function(data) {
292 - dataAggregator.onData(data); 292 + dataAggregator.onData(data, false, false, true);
293 } 293 }
294 subscriber.onReconnected = function() { 294 subscriber.onReconnected = function() {
295 var newSubsTw = null; 295 var newSubsTw = null;
@@ -308,7 +308,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -308,7 +308,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
308 subscriber.onReconnected = function() {} 308 subscriber.onReconnected = function() {}
309 subscriber.onData = function(data) { 309 subscriber.onData = function(data) {
310 if (data.data) { 310 if (data.data) {
311 - onData(data.data, types.dataKeyType.timeseries); 311 + onData(data.data, types.dataKeyType.timeseries, null, null, true);
312 } 312 }
313 } 313 }
314 } 314 }
@@ -331,7 +331,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -331,7 +331,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
331 type: types.dataKeyType.attribute, 331 type: types.dataKeyType.attribute,
332 onData: function (data) { 332 onData: function (data) {
333 if (data.data) { 333 if (data.data) {
334 - onData(data.data, types.dataKeyType.attribute); 334 + onData(data.data, types.dataKeyType.attribute, null, null, true);
335 } 335 }
336 }, 336 },
337 onReconnected: function() {} 337 onReconnected: function() {}
@@ -351,33 +351,24 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -351,33 +351,24 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
351 tsKeyNames.push(dataKey.name+'_'+dataKey.index); 351 tsKeyNames.push(dataKey.name+'_'+dataKey.index);
352 } 352 }
353 } 353 }
354 - dataAggregator = new DataAggregator(  
355 - function (data, startTs, endTs) {  
356 - onData(data, types.dataKeyType.function, startTs, endTs);  
357 - },  
358 - tsKeyNames,  
359 - subsTw.startTs,  
360 - subsTw.aggregation.limit,  
361 - subsTw.aggregation.type,  
362 - subsTw.aggregation.timeWindow,  
363 - subsTw.aggregation.interval,  
364 - types,  
365 - $timeout,  
366 - $filter  
367 - ); 354 + dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.function);
368 } 355 }
369 if (history) { 356 if (history) {
370 - onTick(); 357 + onTick(false);
371 } else { 358 } else {
372 - timer = $timeout(onTick, 0, false); 359 + timer = $timeout(
  360 + function() {onTick(true)},
  361 + 0,
  362 + false
  363 + );
373 } 364 }
374 } 365 }
375 } 366 }
376 367
377 - function createRealtimeDataAggregator(subsTw, tsKeyNames) { 368 + function createRealtimeDataAggregator(subsTw, tsKeyNames, dataKeyType) {
378 return new DataAggregator( 369 return new DataAggregator(
379 - function(data, startTs, endTs) {  
380 - onData(data, types.dataKeyType.timeseries, startTs, endTs); 370 + function(data, startTs, endTs, apply) {
  371 + onData(data, dataKeyType, startTs, endTs, apply);
381 }, 372 },
382 tsKeyNames, 373 tsKeyNames,
383 subsTw.startTs, 374 subsTw.startTs,
@@ -443,7 +434,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -443,7 +434,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
443 return data; 434 return data;
444 } 435 }
445 436
446 - function generateLatest(dataKey) { 437 + function generateLatest(dataKey, apply) {
447 var prevSeries; 438 var prevSeries;
448 var datasourceKeyData = datasourceData[dataKey.key].data; 439 var datasourceKeyData = datasourceData[dataKey.key].data;
449 if (datasourceKeyData.length > 0) { 440 if (datasourceKeyData.length > 0) {
@@ -461,11 +452,11 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -461,11 +452,11 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
461 var listener = listeners[i]; 452 var listener = listeners[i];
462 listener.dataUpdated(datasourceData[dataKey.key], 453 listener.dataUpdated(datasourceData[dataKey.key],
463 listener.datasourceIndex, 454 listener.datasourceIndex,
464 - dataKey.index); 455 + dataKey.index, apply);
465 } 456 }
466 } 457 }
467 458
468 - function onTick() { 459 + function onTick(apply) {
469 var key; 460 var key;
470 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 461 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
471 var startTime; 462 var startTime;
@@ -495,15 +486,15 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -495,15 +486,15 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
495 generatedData.data[dataKey.name+'_'+dataKey.index] = data; 486 generatedData.data[dataKey.name+'_'+dataKey.index] = data;
496 } 487 }
497 } 488 }
498 - dataAggregator.onData(generatedData, true, history); 489 + dataAggregator.onData(generatedData, true, history, apply);
499 } else if (datasourceSubscription.type === types.widgetType.latest.value) { 490 } else if (datasourceSubscription.type === types.widgetType.latest.value) {
500 for (key in dataKeys) { 491 for (key in dataKeys) {
501 - generateLatest(dataKeys[key]); 492 + generateLatest(dataKeys[key], apply);
502 } 493 }
503 } 494 }
504 495
505 if (!history) { 496 if (!history) {
506 - timer = $timeout(onTick, frequency / 2, false); 497 + timer = $timeout(function() {onTick(true)}, frequency / 2, false);
507 } 498 }
508 } 499 }
509 500
@@ -519,7 +510,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -519,7 +510,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
519 } 510 }
520 } 511 }
521 512
522 - function onData(sourceData, type, startTs, endTs) { 513 + function onData(sourceData, type, startTs, endTs, apply) {
523 for (var keyName in sourceData) { 514 for (var keyName in sourceData) {
524 var keyData = sourceData[keyName]; 515 var keyData = sourceData[keyName];
525 var key = keyName + '_' + type; 516 var key = keyName + '_' + type;
@@ -572,7 +563,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -572,7 +563,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
572 var listener = listeners[i2]; 563 var listener = listeners[i2];
573 listener.dataUpdated(datasourceData[datasourceKey], 564 listener.dataUpdated(datasourceData[datasourceKey],
574 listener.datasourceIndex, 565 listener.datasourceIndex,
575 - dataKey.index); 566 + dataKey.index, apply);
576 } 567 }
577 } 568 }
578 } 569 }
@@ -206,9 +206,9 @@ function TimeService($translate, types) { @@ -206,9 +206,9 @@ function TimeService($translate, types) {
206 function defaultTimewindow() { 206 function defaultTimewindow() {
207 var currentTime = (new Date).getTime(); 207 var currentTime = (new Date).getTime();
208 var timewindow = { 208 var timewindow = {
209 - displayValue: "",  
210 - selectedTab: 0,  
211 - realtime: { 209 + displayValue: "",
  210 + selectedTab: 0,
  211 + realtime: {
212 interval: SECOND, 212 interval: SECOND,
213 timewindowMs: MINUTE // 1 min by default 213 timewindowMs: MINUTE // 1 min by default
214 }, 214 },
@@ -52,6 +52,7 @@ function Dashboard() { @@ -52,6 +52,7 @@ function Dashboard() {
52 bindToController: { 52 bindToController: {
53 widgets: '=', 53 widgets: '=',
54 deviceAliasList: '=', 54 deviceAliasList: '=',
  55 + dashboardTimewindow: '=?',
55 columns: '=', 56 columns: '=',
56 margins: '=', 57 margins: '=',
57 isEdit: '=', 58 isEdit: '=',
@@ -71,7 +72,8 @@ function Dashboard() { @@ -71,7 +72,8 @@ function Dashboard() {
71 getStDiff: '&?', 72 getStDiff: '&?',
72 onInit: '&?', 73 onInit: '&?',
73 onInitFailed: '&?', 74 onInitFailed: '&?',
74 - dashboardStyle: '=?' 75 + dashboardStyle: '=?',
  76 + dashboardClass: '=?'
75 }, 77 },
76 controller: DashboardController, 78 controller: DashboardController,
77 controllerAs: 'vm', 79 controllerAs: 'vm',
@@ -80,7 +82,7 @@ function Dashboard() { @@ -80,7 +82,7 @@ function Dashboard() {
80 } 82 }
81 83
82 /*@ngInject*/ 84 /*@ngInject*/
83 -function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $log, toast, types) { 85 +function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) {
84 86
85 var highlightedMode = false; 87 var highlightedMode = false;
86 var highlightedWidget = null; 88 var highlightedWidget = null;
@@ -99,6 +101,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -99,6 +101,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
99 101
100 vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; 102 vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
101 103
  104 + if (!('dashboardTimewindow' in vm)) {
  105 + vm.dashboardTimewindow = timeService.defaultTimewindow();
  106 + }
  107 +
102 vm.dashboardLoading = true; 108 vm.dashboardLoading = true;
103 vm.visibleRect = { 109 vm.visibleRect = {
104 top: 0, 110 top: 0,
@@ -176,6 +182,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -176,6 +182,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
176 vm.widgetContextMenuItems = []; 182 vm.widgetContextMenuItems = [];
177 vm.widgetContextMenuEvent = null; 183 vm.widgetContextMenuEvent = null;
178 184
  185 + vm.dashboardTimewindowApi = {
  186 + onResetTimewindow: function() {
  187 + if (vm.originalDashboardTimewindow) {
  188 + vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
  189 + vm.originalDashboardTimewindow = null;
  190 + }
  191 + },
  192 + onUpdateTimewindow: function(startTimeMs, endTimeMs) {
  193 + if (!vm.originalDashboardTimewindow) {
  194 + vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow);
  195 + }
  196 + vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs);
  197 + }
  198 + };
  199 +
179 //$element[0].onmousemove=function(){ 200 //$element[0].onmousemove=function(){
180 // widgetMouseMove(); 201 // widgetMouseMove();
181 // } 202 // }
@@ -656,7 +677,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -656,7 +677,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
656 } 677 }
657 678
658 function hasTimewindow(widget) { 679 function hasTimewindow(widget) {
659 - return widget.type === types.widgetType.timeseries.value; 680 + if (widget.type === types.widgetType.timeseries.value) {
  681 + return angular.isDefined(widget.config.useDashboardTimewindow) ?
  682 + !widget.config.useDashboardTimewindow : false;
  683 + } else {
  684 + return false;
  685 + }
660 } 686 }
661 687
662 function adoptMaxRows() { 688 function adoptMaxRows() {
@@ -673,6 +699,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -673,6 +699,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
673 699
674 function dashboardLoaded() { 700 function dashboardLoaded() {
675 $timeout(function () { 701 $timeout(function () {
  702 + $scope.$watch('vm.dashboardTimewindow', function () {
  703 + $scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow);
  704 + }, true);
676 adoptMaxRows(); 705 adoptMaxRows();
677 vm.dashboardLoading = false; 706 vm.dashboardLoading = false;
678 $timeout(function () { 707 $timeout(function () {
@@ -51,7 +51,7 @@ div.tb-widget { @@ -51,7 +51,7 @@ div.tb-widget {
51 height: 32px; 51 height: 32px;
52 min-width: 32px; 52 min-width: 32px;
53 min-height: 32px; 53 min-height: 32px;
54 - md-icon { 54 + md-icon, ng-md-icon {
55 width: 20px; 55 width: 20px;
56 height: 20px; 56 height: 20px;
57 min-width: 20px; 57 min-width: 20px;
@@ -93,6 +93,7 @@ md-content.tb-dashboard-content { @@ -93,6 +93,7 @@ md-content.tb-dashboard-content {
93 right: 0; 93 right: 0;
94 bottom: 0; 94 bottom: 0;
95 outline: none; 95 outline: none;
  96 + background: none;
96 .gridster-item { 97 .gridster-item {
97 @include transition(none); 98 @include transition(none);
98 } 99 }
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 </md-content> 21 </md-content>
22 <md-menu md-position-mode="target target" tb-mousepoint-menu> 22 <md-menu md-position-mode="target target" tb-mousepoint-menu>
23 <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)"> 23 <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
24 - <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;"> 24 + <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%;">
25 <div id="gridster-child" gridster="vm.gridsterOpts"> 25 <div id="gridster-child" gridster="vm.gridsterOpts">
26 <ul> 26 <ul>
27 <!-- ng-click="widgetClicked($event, widget)" --> 27 <!-- ng-click="widgetClicked($event, widget)" -->
@@ -30,6 +30,7 @@ @@ -30,6 +30,7 @@
30 <div tb-expand-fullscreen 30 <div tb-expand-fullscreen
31 fullscreen-background-style="vm.dashboardStyle" 31 fullscreen-background-style="vm.dashboardStyle"
32 expand-button-id="expand-button" 32 expand-button-id="expand-button"
  33 + expand-button-size="20"
33 on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" 34 on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)"
34 layout="column" 35 layout="column"
35 class="tb-widget" 36 class="tb-widget"
@@ -45,55 +46,55 @@ @@ -45,55 +46,55 @@
45 color: vm.widgetColor(widget), 46 color: vm.widgetColor(widget),
46 backgroundColor: vm.widgetBackgroundColor(widget), 47 backgroundColor: vm.widgetBackgroundColor(widget),
47 padding: vm.widgetPadding(widget)}"> 48 padding: vm.widgetPadding(widget)}">
48 - <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> 49 + <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 <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
50 - <tb-timewindow button-color="vm.widgetColor(widget)" aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> 51 + <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
51 </div> 52 </div>
52 <div class="tb-widget-actions" layout="row" layout-align="start center"> 53 <div class="tb-widget-actions" layout="row" layout-align="start center">
53 <md-button id="expand-button" 54 <md-button id="expand-button"
54 ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)" 55 ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
55 aria-label="{{ 'fullscreen.fullscreen' | translate }}" 56 aria-label="{{ 'fullscreen.fullscreen' | translate }}"
56 - class="md-icon-button md-primary"></md-button> 57 + class="md-icon-button"></md-button>
57 <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded" 58 <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
58 ng-disabled="vm.loading()" 59 ng-disabled="vm.loading()"
59 - class="md-icon-button md-primary" 60 + class="md-icon-button"
60 ng-click="vm.editWidget($event, widget)" 61 ng-click="vm.editWidget($event, widget)"
61 aria-label="{{ 'widget.edit' | translate }}"> 62 aria-label="{{ 'widget.edit' | translate }}">
62 <md-tooltip md-direction="top"> 63 <md-tooltip md-direction="top">
63 {{ 'widget.edit' | translate }} 64 {{ 'widget.edit' | translate }}
64 </md-tooltip> 65 </md-tooltip>
65 - <md-icon class="material-icons">  
66 - edit  
67 - </md-icon> 66 + <ng-md-icon size="20" icon="edit"></ng-md-icon>
68 </md-button> 67 </md-button>
69 <md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded" 68 <md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
70 ng-disabled="vm.loading()" 69 ng-disabled="vm.loading()"
71 - class="md-icon-button md-primary" 70 + class="md-icon-button"
72 ng-click="vm.exportWidget($event, widget)" 71 ng-click="vm.exportWidget($event, widget)"
73 aria-label="{{ 'widget.export' | translate }}"> 72 aria-label="{{ 'widget.export' | translate }}">
74 <md-tooltip md-direction="top"> 73 <md-tooltip md-direction="top">
75 {{ 'widget.export' | translate }} 74 {{ 'widget.export' | translate }}
76 </md-tooltip> 75 </md-tooltip>
77 - <md-icon class="material-icons">  
78 - file_download  
79 - </md-icon> 76 + <ng-md-icon size="20" icon="file_download"></ng-md-icon>
80 </md-button> 77 </md-button>
81 <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded" 78 <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
82 ng-disabled="vm.loading()" 79 ng-disabled="vm.loading()"
83 - class="md-icon-button md-primary" 80 + class="md-icon-button"
84 ng-click="vm.removeWidget($event, widget)" 81 ng-click="vm.removeWidget($event, widget)"
85 aria-label="{{ 'widget.remove' | translate }}"> 82 aria-label="{{ 'widget.remove' | translate }}">
86 <md-tooltip md-direction="top"> 83 <md-tooltip md-direction="top">
87 {{ 'widget.remove' | translate }} 84 {{ 'widget.remove' | translate }}
88 </md-tooltip> 85 </md-tooltip>
89 - <md-icon class="material-icons">  
90 - close  
91 - </md-icon> 86 + <ng-md-icon size="20" icon="close"></ng-md-icon>
92 </md-button> 87 </md-button>
93 </div> 88 </div>
94 <div flex layout="column" class="tb-widget-content"> 89 <div flex layout="column" class="tb-widget-content">
95 <div flex tb-widget 90 <div flex tb-widget
96 - locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isEdit: vm.isEdit, stDiff: vm.stDiff }"> 91 + locals="{ visibleRect: vm.visibleRect,
  92 + widget: widget,
  93 + deviceAliasList: vm.deviceAliasList,
  94 + isEdit: vm.isEdit,
  95 + stDiff: vm.stDiff,
  96 + dashboardTimewindow: vm.dashboardTimewindow,
  97 + dashboardTimewindowApi: vm.dashboardTimewindowApi }">
97 </div> 98 </div>
98 </div> 99 </div>
99 </div> 100 </div>
@@ -101,11 +101,15 @@ function ExpandFullscreen($compile, $document) { @@ -101,11 +101,15 @@ function ExpandFullscreen($compile, $document) {
101 if (attrs.expandButtonId) { 101 if (attrs.expandButtonId) {
102 expandButton = $('#' + attrs.expandButtonId, element)[0]; 102 expandButton = $('#' + attrs.expandButtonId, element)[0];
103 } 103 }
  104 + var buttonSize;
  105 + if (attrs.expandButtonSize) {
  106 + buttonSize = attrs.expandButtonSize;
  107 + }
104 108
105 var html = '<md-tooltip md-direction="{{expanded ? \'bottom\' : \'top\'}}">' + 109 var html = '<md-tooltip md-direction="{{expanded ? \'bottom\' : \'top\'}}">' +
106 '{{(expanded ? \'fullscreen.exit\' : \'fullscreen.expand\') | translate}}' + 110 '{{(expanded ? \'fullscreen.exit\' : \'fullscreen.expand\') | translate}}' +
107 '</md-tooltip>' + 111 '</md-tooltip>' +
108 - '<ng-md-icon icon="{{expanded ? \'fullscreen_exit\' : \'fullscreen\'}}" ' + 112 + '<ng-md-icon ' + (buttonSize ? 'size="'+ buttonSize +'" ' : '') + 'icon="{{expanded ? \'fullscreen_exit\' : \'fullscreen\'}}" ' +
109 'options=\'{"easing": "circ-in-out", "duration": 375, "rotation": "none"}\'>' + 113 'options=\'{"easing": "circ-in-out", "duration": 375, "rotation": "none"}\'>' +
110 '</ng-md-icon>'; 114 '</ng-md-icon>';
111 115
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<md-button class="md-raised md-primary" ng-click="openEditMode($event)">  
19 - <md-icon class="material-icons">date_range</md-icon> 18 +<md-button ng-disabled="disabled" class="md-raised md-primary" ng-click="openEditMode($event)">
  19 + <ng-md-icon icon="query_builder"></ng-md-icon>
20 <span>{{model.displayValue}}</span> 20 <span>{{model.displayValue}}</span>
21 </md-button> 21 </md-button>
@@ -79,26 +79,38 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -79,26 +79,38 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
79 if (scope.asButton) { 79 if (scope.asButton) {
80 template = $templateCache.get(timewindowButtonTemplate); 80 template = $templateCache.get(timewindowButtonTemplate);
81 } else { 81 } else {
  82 + scope.direction = scope.direction || 'left';
82 template = $templateCache.get(timewindowTemplate); 83 template = $templateCache.get(timewindowTemplate);
83 } 84 }
84 element.html(template); 85 element.html(template);
85 86
86 scope.openEditMode = function (event) { 87 scope.openEditMode = function (event) {
  88 + if (scope.disabled) {
  89 + return;
  90 + }
87 var position; 91 var position;
88 var isGtSm = $mdMedia('gt-sm'); 92 var isGtSm = $mdMedia('gt-sm');
89 if (isGtSm) { 93 if (isGtSm) {
90 var panelHeight = 375; 94 var panelHeight = 375;
  95 + var panelWidth = 417;
91 var offset = element[0].getBoundingClientRect(); 96 var offset = element[0].getBoundingClientRect();
92 var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line 97 var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
  98 + var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
93 var yPosition; 99 var yPosition;
  100 + var xPosition;
94 if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line 101 if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
95 yPosition = $mdPanel.yPosition.ABOVE; 102 yPosition = $mdPanel.yPosition.ABOVE;
96 } else { 103 } else {
97 yPosition = $mdPanel.yPosition.BELOW; 104 yPosition = $mdPanel.yPosition.BELOW;
98 } 105 }
  106 + if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
  107 + xPosition = $mdPanel.xPosition.ALIGN_END;
  108 + } else {
  109 + xPosition = $mdPanel.xPosition.ALIGN_START;
  110 + }
99 position = $mdPanel.newPanelPosition() 111 position = $mdPanel.newPanelPosition()
100 .relativeTo(element) 112 .relativeTo(element)
101 - .addPanelPosition($mdPanel.xPosition.ALIGN_START, yPosition); 113 + .addPanelPosition(xPosition, yPosition);
102 } else { 114 } else {
103 position = $mdPanel.newPanelPosition() 115 position = $mdPanel.newPanelPosition()
104 .absolute() 116 .absolute()
@@ -223,7 +235,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -223,7 +235,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
223 require: "^ngModel", 235 require: "^ngModel",
224 scope: { 236 scope: {
225 asButton: '=asButton', 237 asButton: '=asButton',
226 - buttonColor: '=?' 238 + direction: '=?',
  239 + disabled:'=ngDisabled'
227 }, 240 },
228 link: linker 241 link: linker
229 }; 242 };
@@ -57,3 +57,10 @@ @@ -57,3 +57,10 @@
57 } 57 }
58 } 58 }
59 } 59 }
  60 +
  61 +tb-timewindow {
  62 + span {
  63 + pointer-events: all;
  64 + cursor: pointer;
  65 + }
  66 +}
@@ -15,9 +15,23 @@ @@ -15,9 +15,23 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<section layout='row' layout-align="start center" style="min-height: 32px;">  
19 - <span ng-click="openEditMode($event)">{{model.displayValue}}</span>  
20 - <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">  
21 - <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon> 18 +<section layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
  19 + <md-button ng-if="direction === 'left'" ng-disabled="disabled" class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
  20 + <md-tooltip md-direction="top">
  21 + {{ 'timewindow.edit' | translate }}
  22 + </md-tooltip>
  23 + <ng-md-icon aria-label="{{ 'timewindow.date-range' | translate }}" icon="query_builder"></ng-md-icon>
  24 + </md-button>
  25 + <span ng-click="openEditMode($event)">
  26 + <md-tooltip md-direction="top">
  27 + {{ 'timewindow.edit' | translate }}
  28 + </md-tooltip>
  29 + {{model.displayValue}}
  30 + </span>
  31 + <md-button ng-if="direction === 'right'" ng-disabled="disabled" class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
  32 + <md-tooltip md-direction="top">
  33 + {{ 'timewindow.edit' | translate }}
  34 + </md-tooltip>
  35 + <ng-md-icon aria-label="{{ 'timewindow.date-range' | translate }}" icon="query_builder"></ng-md-icon>
22 </md-button> 36 </md-button>
23 </section> 37 </section>
@@ -98,6 +98,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -98,6 +98,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
98 }, true); 98 }, true);
99 scope.mobileOrder = ngModelCtrl.$viewValue.mobileOrder; 99 scope.mobileOrder = ngModelCtrl.$viewValue.mobileOrder;
100 scope.mobileHeight = ngModelCtrl.$viewValue.mobileHeight; 100 scope.mobileHeight = ngModelCtrl.$viewValue.mobileHeight;
  101 + scope.useDashboardTimewindow = angular.isDefined(ngModelCtrl.$viewValue.useDashboardTimewindow) ?
  102 + ngModelCtrl.$viewValue.useDashboardTimewindow : true;
101 scope.timewindow = ngModelCtrl.$viewValue.timewindow; 103 scope.timewindow = ngModelCtrl.$viewValue.timewindow;
102 if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) { 104 if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) {
103 if (scope.datasources) { 105 if (scope.datasources) {
@@ -174,7 +176,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -174,7 +176,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
174 } 176 }
175 }; 177 };
176 178
177 - scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + padding + titleStyle + mobileOrder + mobileHeight + intervalSec', function () { 179 + scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' +
  180 + 'padding + titleStyle + mobileOrder + mobileHeight + useDashboardTimewindow', function () {
178 if (ngModelCtrl.$viewValue) { 181 if (ngModelCtrl.$viewValue) {
179 var value = ngModelCtrl.$viewValue; 182 var value = ngModelCtrl.$viewValue;
180 value.title = scope.title; 183 value.title = scope.title;
@@ -191,7 +194,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -191,7 +194,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
191 } 194 }
192 value.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined; 195 value.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined;
193 value.mobileHeight = scope.mobileHeight; 196 value.mobileHeight = scope.mobileHeight;
194 - value.intervalSec = scope.intervalSec; 197 + value.useDashboardTimewindow = scope.useDashboardTimewindow;
195 ngModelCtrl.$setViewValue(value); 198 ngModelCtrl.$setViewValue(value);
196 scope.updateValidity(); 199 scope.updateValidity();
197 } 200 }
@@ -88,10 +88,15 @@ @@ -88,10 +88,15 @@
88 <input ng-model="mobileHeight" type="number"> 88 <input ng-model="mobileHeight" type="number">
89 </md-input-container> 89 </md-input-container>
90 </div> 90 </div>
91 - <div ng-show="widgetType === types.widgetType.timeseries.value" layout="row"  
92 - layout-align="center center">  
93 - <span translate style="padding-right: 8px;">widget-config.timewindow</span>  
94 - <tb-timewindow as-button="true" aggregation flex ng-model="timewindow"></tb-timewindow> 91 + <div ng-show="widgetType === types.widgetType.timeseries.value" layout='column' layout-align="center"
  92 + layout-gt-sm='row' layout-align-gt-sm="start center">
  93 + <md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}"
  94 + ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }}
  95 + </md-checkbox>
  96 + <section flex layout="row" layout-align="start center" style="margin-bottom: 16px;">
  97 + <span ng-class="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
  98 + <tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation flex ng-model="timewindow"></tb-timewindow>
  99 + </section>
95 </div> 100 </div>
96 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" 101 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
97 ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value"> 102 ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value">
@@ -20,7 +20,8 @@ import 'javascript-detect-element-resize/detect-element-resize'; @@ -20,7 +20,8 @@ import 'javascript-detect-element-resize/detect-element-resize';
20 20
21 /*@ngInject*/ 21 /*@ngInject*/
22 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, timeService, 22 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, timeService,
23 - datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) { 23 + datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
  24 + dashboardTimewindowApi, widget, deviceAliasList, widgetType) {
24 25
25 var vm = this; 26 var vm = this;
26 27
@@ -136,6 +137,24 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -136,6 +137,24 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
136 $scope.widgetErrorData = utils.processWidgetException(e); 137 $scope.widgetErrorData = utils.processWidgetException(e);
137 } 138 }
138 139
  140 + function notifyDataLoaded(apply) {
  141 + if ($scope.loadingData === true) {
  142 + $scope.loadingData = false;
  143 + if (apply) {
  144 + $scope.$digest();
  145 + }
  146 + }
  147 + }
  148 +
  149 + function notifyDataLoading(apply) {
  150 + if ($scope.loadingData === false) {
  151 + $scope.loadingData = true;
  152 + if (apply) {
  153 + $scope.$digest();
  154 + }
  155 + }
  156 + }
  157 +
139 function onInit() { 158 function onInit() {
140 if (!widgetContext.inited) { 159 if (!widgetContext.inited) {
141 widgetContext.inited = true; 160 widgetContext.inited = true;
@@ -274,7 +293,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -274,7 +293,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
274 } 293 }
275 294
276 function initialize() { 295 function initialize() {
277 - if (widget.type !== types.widgetType.rpc.value) { 296 + if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
278 for (var i in widget.config.datasources) { 297 for (var i in widget.config.datasources) {
279 var datasource = angular.copy(widget.config.datasources[i]); 298 var datasource = angular.copy(widget.config.datasources[i]);
280 for (var a in datasource.dataKeys) { 299 for (var a in datasource.dataKeys) {
@@ -287,7 +306,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -287,7 +306,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
287 widgetContext.data.push(datasourceData); 306 widgetContext.data.push(datasourceData);
288 } 307 }
289 } 308 }
290 - } else { 309 + } else if (widget.type === types.widgetType.rpc.value) {
291 if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { 310 if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
292 targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; 311 targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
293 if (deviceAliasList[targetDeviceAliasId]) { 312 if (deviceAliasList[targetDeviceAliasId]) {
@@ -354,14 +373,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -354,14 +373,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
354 }); 373 });
355 374
356 if (widget.type === types.widgetType.timeseries.value) { 375 if (widget.type === types.widgetType.timeseries.value) {
357 - $scope.$watch(function () {  
358 - return widget.config.timewindow;  
359 - }, function (newTimewindow, prevTimewindow) {  
360 - if (!angular.equals(newTimewindow, prevTimewindow)) {  
361 - unsubscribe();  
362 - subscribe();  
363 - }  
364 - }); 376 + widgetContext.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
  377 + ? widget.config.useDashboardTimewindow : true;
  378 + if (widgetContext.useDashboardTimewindow) {
  379 + $scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
  380 + if (!angular.equals(dashboardTimewindow, newDashboardTimewindow)) {
  381 + dashboardTimewindow = newDashboardTimewindow;
  382 + unsubscribe();
  383 + subscribe();
  384 + }
  385 + });
  386 + } else {
  387 + $scope.$watch(function () {
  388 + return widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
  389 + }, function (newTimewindow, prevTimewindow) {
  390 + if (!angular.equals(newTimewindow, prevTimewindow)) {
  391 + unsubscribe();
  392 + subscribe();
  393 + }
  394 + });
  395 + }
365 } 396 }
366 subscribe(); 397 subscribe();
367 } 398 }
@@ -474,20 +505,29 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -474,20 +505,29 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
474 }*/ 505 }*/
475 506
476 function onResetTimewindow() { 507 function onResetTimewindow() {
477 - if (originalTimewindow) {  
478 - widget.config.timewindow = angular.copy(originalTimewindow);  
479 - originalTimewindow = null; 508 + if (widgetContext.useDashboardTimewindow) {
  509 + dashboardTimewindowApi.onResetTimewindow();
  510 + } else {
  511 + if (originalTimewindow) {
  512 + widget.config.timewindow = angular.copy(originalTimewindow);
  513 + originalTimewindow = null;
  514 + }
480 } 515 }
481 } 516 }
482 517
483 function onUpdateTimewindow(startTimeMs, endTimeMs) { 518 function onUpdateTimewindow(startTimeMs, endTimeMs) {
484 - if (!originalTimewindow) {  
485 - originalTimewindow = angular.copy(widget.config.timewindow); 519 + if (widgetContext.useDashboardTimewindow) {
  520 + dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
  521 + } else {
  522 + if (!originalTimewindow) {
  523 + originalTimewindow = angular.copy(widget.config.timewindow);
  524 + }
  525 + widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);
486 } 526 }
487 - widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);  
488 } 527 }
489 528
490 - function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) { 529 + function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
  530 + notifyDataLoaded(apply);
491 var update = true; 531 var update = true;
492 if (widget.type === types.widgetType.latest.value) { 532 if (widget.type === types.widgetType.latest.value) {
493 var prevData = widgetContext.data[datasourceIndex + dataKeyIndex].data; 533 var prevData = widgetContext.data[datasourceIndex + dataKeyIndex].data;
@@ -547,16 +587,28 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -547,16 +587,28 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
547 if (_subscriptionTimewindow) { 587 if (_subscriptionTimewindow) {
548 subscriptionTimewindow = _subscriptionTimewindow; 588 subscriptionTimewindow = _subscriptionTimewindow;
549 } else { 589 } else {
550 - subscriptionTimewindow = timeService.createSubscriptionTimewindow(widget.config.timewindow, widgetContext.timeWindow.stDiff); 590 + subscriptionTimewindow =
  591 + timeService.createSubscriptionTimewindow(
  592 + widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow,
  593 + widgetContext.timeWindow.stDiff);
551 } 594 }
552 updateTimewindow(); 595 updateTimewindow();
553 return subscriptionTimewindow; 596 return subscriptionTimewindow;
554 } 597 }
555 598
  599 + function hasTimewindow() {
  600 + if (widgetContext.useDashboardTimewindow) {
  601 + return angular.isDefined(dashboardTimewindow);
  602 + } else {
  603 + return angular.isDefined(widget.config.timewindow);
  604 + }
  605 + }
  606 +
556 function subscribe() { 607 function subscribe() {
557 - if (widget.type !== types.widgetType.rpc.value) { 608 + if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
  609 + notifyDataLoading();
558 if (widget.type === types.widgetType.timeseries.value && 610 if (widget.type === types.widgetType.timeseries.value &&
559 - angular.isDefined(widget.config.timewindow)) { 611 + hasTimewindow()) {
560 updateRealtimeSubscription(); 612 updateRealtimeSubscription();
561 if (subscriptionTimewindow.fixedWindow) { 613 if (subscriptionTimewindow.fixedWindow) {
562 onDataUpdated(); 614 onDataUpdated();
@@ -579,8 +631,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -579,8 +631,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
579 subscriptionTimewindow: subscriptionTimewindow, 631 subscriptionTimewindow: subscriptionTimewindow,
580 datasource: datasource, 632 datasource: datasource,
581 deviceId: deviceId, 633 deviceId: deviceId,
582 - dataUpdated: function (data, datasourceIndex, dataKeyIndex) {  
583 - dataUpdated(data, datasourceIndex, dataKeyIndex); 634 + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
  635 + dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
584 }, 636 },
585 updateRealtimeSubscription: function() { 637 updateRealtimeSubscription: function() {
586 this.subscriptionTimewindow = updateRealtimeSubscription(); 638 this.subscriptionTimewindow = updateRealtimeSubscription();
@@ -601,6 +653,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -601,6 +653,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
601 datasourceListeners.push(listener); 653 datasourceListeners.push(listener);
602 datasourceService.subscribeToDatasource(listener); 654 datasourceService.subscribeToDatasource(listener);
603 } 655 }
  656 + } else {
  657 + notifyDataLoaded();
604 } 658 }
605 } 659 }
606 660
@@ -66,6 +66,8 @@ function Widget($controller, $compile, widgetService) { @@ -66,6 +66,8 @@ function Widget($controller, $compile, widgetService) {
66 66
67 function loadFromWidgetInfo(widgetInfo) { 67 function loadFromWidgetInfo(widgetInfo) {
68 68
  69 + scope.loadingData = true;
  70 +
69 elem.addClass("tb-widget"); 71 elem.addClass("tb-widget");
70 72
71 var widgetNamespace = "widget-type-" + (widget.isSystemType ? 'sys-' : '') 73 var widgetNamespace = "widget-type-" + (widget.isSystemType ? 'sys-' : '')
@@ -73,9 +75,12 @@ function Widget($controller, $compile, widgetService) { @@ -73,9 +75,12 @@ function Widget($controller, $compile, widgetService) {
73 + widget.typeAlias; 75 + widget.typeAlias;
74 76
75 elem.addClass(widgetNamespace); 77 elem.addClass(widgetNamespace);
76 - elem.html('<div class="tb-absolute-fill tb-widget-error"" ng-if="widgetErrorData">' + 78 + elem.html('<div class="tb-absolute-fill tb-widget-error" ng-if="widgetErrorData">' +
77 '<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' + 79 '<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' +
78 '</div>' + 80 '</div>' +
  81 + '<div class="tb-absolute-fill tb-widget-loading" ng-show="loadingData" layout="column" layout-align="center center">' +
  82 + '<md-progress-circular md-mode="indeterminate" class="md-accent" md-diameter="40"></md-progress-circular>' +
  83 + '</div>' +
79 '<div id="container">' + widgetInfo.templateHtml + '</div>'); 84 '<div id="container">' + widgetInfo.templateHtml + '</div>');
80 85
81 $compile(elem.contents())(scope); 86 $compile(elem.contents())(scope);
@@ -23,4 +23,8 @@ @@ -23,4 +23,8 @@
23 color: red; 23 color: red;
24 } 24 }
25 } 25 }
  26 + .tb-widget-loading {
  27 + background: rgba(255,255,255,0.15);
  28 + z-index: 3;
  29 + }
26 } 30 }
@@ -32,6 +32,7 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti @@ -32,6 +32,7 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
32 } 32 }
33 33
34 vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; 34 vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
  35 + vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)';
35 vm.gridSettings.columns = vm.gridSettings.columns || 24; 36 vm.gridSettings.columns = vm.gridSettings.columns || 24;
36 vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; 37 vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
37 vm.hMargin = vm.gridSettings.margins[0]; 38 vm.hMargin = vm.gridSettings.margins[0];
@@ -31,10 +31,22 @@ @@ -31,10 +31,22 @@
31 <md-dialog-content> 31 <md-dialog-content>
32 <div class="md-dialog-content"> 32 <div class="md-dialog-content">
33 <fieldset ng-disabled="loading"> 33 <fieldset ng-disabled="loading">
34 - <div layout="row" layout-padding> 34 + <div layout="row" layout-align="start center">
35 <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}" 35 <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
36 ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }} 36 ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
37 </md-checkbox> 37 </md-checkbox>
  38 + <div flex
  39 + ng-required="false"
  40 + md-color-picker
  41 + ng-model="vm.gridSettings.titleColor"
  42 + label="{{ 'dashboard.title-color' | translate }}"
  43 + icon="format_color_fill"
  44 + default="rgba(0, 0, 0, 0.870588)"
  45 + md-color-clear-button="false"
  46 + open-on-input="true"
  47 + md-color-generic-palette="false"
  48 + md-color-history="false"
  49 + ></div>
38 </div> 50 </div>
39 <md-input-container class="md-block"> 51 <md-input-container class="md-block">
40 <label translate>dashboard.columns-count</label> 52 <label translate>dashboard.columns-count</label>
@@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html'; @@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
23 23
24 /*@ngInject*/ 24 /*@ngInject*/
25 export default function DashboardController(types, widgetService, userService, 25 export default function DashboardController(types, widgetService, userService,
26 - dashboardService, itembuffer, importExport, hotkeys, $window, $rootScope, 26 + dashboardService, timeService, itembuffer, importExport, hotkeys, $window, $rootScope,
27 $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { 27 $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
28 28
29 var user = userService.getCurrentUser(); 29 var user = userService.getCurrentUser();
@@ -47,6 +47,25 @@ export default function DashboardController(types, widgetService, userService, @@ -47,6 +47,25 @@ export default function DashboardController(types, widgetService, userService,
47 vm.widgets = []; 47 vm.widgets = [];
48 vm.dashboardInitComplete = false; 48 vm.dashboardInitComplete = false;
49 49
  50 + vm.isToolbarOpened = false;
  51 +
  52 + Object.defineProperty(vm, 'toolbarOpened', {
  53 + get: function() { return vm.isToolbarOpened || vm.isEdit; },
  54 + set: function() { }
  55 + });
  56 +
  57 + vm.openToolbar = function() {
  58 + $timeout(function() {
  59 + vm.isToolbarOpened = true;
  60 + });
  61 + }
  62 +
  63 + vm.closeToolbar = function() {
  64 + $timeout(function() {
  65 + vm.isToolbarOpened = false;
  66 + });
  67 + }
  68 +
50 vm.addWidget = addWidget; 69 vm.addWidget = addWidget;
51 vm.addWidgetFromType = addWidgetFromType; 70 vm.addWidgetFromType = addWidgetFromType;
52 vm.dashboardInited = dashboardInited; 71 vm.dashboardInited = dashboardInited;
@@ -154,6 +173,9 @@ export default function DashboardController(types, widgetService, userService, @@ -154,6 +173,9 @@ export default function DashboardController(types, widgetService, userService,
154 173
155 if (vm.widgetEditMode) { 174 if (vm.widgetEditMode) {
156 $timeout(function () { 175 $timeout(function () {
  176 + vm.dashboardConfiguration = {
  177 + timewindow: timeService.defaultTimewindow()
  178 + };
157 vm.widgets = [{ 179 vm.widgets = [{
158 isSystemType: true, 180 isSystemType: true,
159 bundleAlias: 'customWidgetBundle', 181 bundleAlias: 'customWidgetBundle',
@@ -186,9 +208,12 @@ export default function DashboardController(types, widgetService, userService, @@ -186,9 +208,12 @@ export default function DashboardController(types, widgetService, userService,
186 if (angular.isUndefined(vm.dashboard.configuration.deviceAliases)) { 208 if (angular.isUndefined(vm.dashboard.configuration.deviceAliases)) {
187 vm.dashboard.configuration.deviceAliases = {}; 209 vm.dashboard.configuration.deviceAliases = {};
188 } 210 }
189 - //$timeout(function () {  
190 - vm.widgets = vm.dashboard.configuration.widgets;  
191 - //}); 211 +
  212 + if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
  213 + vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
  214 + }
  215 + vm.dashboardConfiguration = vm.dashboard.configuration;
  216 + vm.widgets = vm.dashboard.configuration.widgets;
192 deferred.resolve(); 217 deferred.resolve();
193 }, function fail(e) { 218 }, function fail(e) {
194 deferred.reject(e); 219 deferred.reject(e);
@@ -607,6 +632,7 @@ export default function DashboardController(types, widgetService, userService, @@ -607,6 +632,7 @@ export default function DashboardController(types, widgetService, userService,
607 if (revert) { 632 if (revert) {
608 vm.dashboard = vm.prevDashboard; 633 vm.dashboard = vm.prevDashboard;
609 vm.widgets = vm.dashboard.configuration.widgets; 634 vm.widgets = vm.dashboard.configuration.widgets;
  635 + vm.dashboardConfiguration = vm.dashboard.configuration;
610 } 636 }
611 } 637 }
612 } 638 }
@@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +
  17 +@import "~compass-sass-mixins/lib/compass";
16 @import '../../scss/constants'; 18 @import '../../scss/constants';
17 19
18 section.tb-dashboard-title { 20 section.tb-dashboard-title {
@@ -53,3 +55,81 @@ tb-details-sidenav.tb-widget-details-sidenav { @@ -53,3 +55,81 @@ tb-details-sidenav.tb-widget-details-sidenav {
53 } 55 }
54 } 56 }
55 } 57 }
  58 +
  59 +/***********************
  60 + * dashboard toolbar
  61 + ***********************/
  62 +
  63 +section.tb-dashboard-toolbar {
  64 + position: absolute;
  65 + top: 0px;
  66 + left: -100%;
  67 + z-index: 3;
  68 + pointer-events: none;
  69 + &.tb-dashboard-toolbar-opened {
  70 + right: 0px;
  71 + @include transition(right .3s cubic-bezier(.55,0,.55,.2));
  72 + }
  73 + &.tb-dashboard-toolbar-closed {
  74 + right: 18px;
  75 + @include transition(right .3s cubic-bezier(.55,0,.55,.2) .2s);
  76 + }
  77 + md-fab-toolbar {
  78 + &.md-is-open {
  79 + md-fab-trigger {
  80 + .md-button {
  81 + &.md-fab {
  82 + opacity: 1;
  83 + @include transition(opacity .3s cubic-bezier(.55,0,.55,.2));
  84 + }
  85 + }
  86 + }
  87 + }
  88 + md-fab-trigger {
  89 + .md-button {
  90 + &.md-fab {
  91 + line-height: 36px;
  92 + width: 36px;
  93 + height: 36px;
  94 + margin: 4px 0 0 4px;
  95 + opacity: 0.5;
  96 + @include transition(opacity .3s cubic-bezier(.55,0,.55,.2) .2s);
  97 + md-icon {
  98 + margin: 0;
  99 + line-height: 18px;
  100 + height: 18px;
  101 + width: 18px;
  102 + min-height: 18px;
  103 + min-width: 18px;
  104 + }
  105 + }
  106 + }
  107 + }
  108 + .md-fab-toolbar-wrapper {
  109 + height: 40px;
  110 + md-toolbar {
  111 + min-height: 36px;
  112 + height: 36px;
  113 + md-fab-actions {
  114 + .close-action {
  115 + margin-right: -18px;
  116 + }
  117 + tb-timewindow {
  118 + font-size: 16px;
  119 + }
  120 + }
  121 + }
  122 + }
  123 + }
  124 +}
  125 +
  126 +.tb-dashboard-container {
  127 + &.tb-dashboard-toolbar-opened {
  128 + margin-top: 40px;
  129 + @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2));
  130 + }
  131 + &.tb-dashboard-toolbar-closed {
  132 + margin-top: 0px;
  133 + @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2) .2s);
  134 + }
  135 +}
@@ -15,235 +15,263 @@ @@ -15,235 +15,263 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<md-content flex tb-expand-fullscreen="vm.widgetEditMode || vm.iframeMode" hide-expand-button="vm.widgetEditMode || vm.iframeMode">  
19 - <!--section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap  
20 - class="tb-header-buttons tb-top-header-buttons md-fab" ng-style="{'right': '50px'}">  
21 - <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit" ng-disabled="loading"  
22 - class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"  
23 - aria-label="{{ 'action.apply' | translate }}"  
24 - ng-click="vm.saveDashboard()">  
25 - <md-tooltip md-direction="top">  
26 - {{ 'action.apply-changes' | translate }}  
27 - </md-tooltip>  
28 - <ng-md-icon icon="done"></ng-md-icon>  
29 - </md-button>  
30 - <md-button ng-if="vm.isTenantAdmin()" ng-disabled="loading"  
31 - class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"  
32 - aria-label="{{ 'action.edit-mode' | translate }}"  
33 - ng-click="vm.toggleDashboardEditMode()">  
34 - <md-tooltip md-direction="top">  
35 - {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}  
36 - </md-tooltip>  
37 - <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"  
38 - options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>  
39 - </md-button>  
40 - </section-->  
41 - <section ng-show="!loading && vm.noData()" layout-align="center center"  
42 - ng-class="{'tb-padded' : !vm.widgetEditMode}"  
43 - style="text-transform: uppercase; display: flex; z-index: 1;"  
44 - class="md-headline tb-absolute-fill">  
45 - <span translate ng-if="!vm.isEdit">  
46 - dashboard.no-widgets  
47 - </span>  
48 - <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">  
49 - <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>  
50 - {{ 'dashboard.add-widget' | translate }}  
51 - </md-button>  
52 - </section>  
53 - <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center">  
54 - <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>  
55 - <md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">  
56 - <label translate>dashboard.title</label>  
57 - <input class="tb-dashboard-title" required name="title" ng-model="vm.dashboard.title">  
58 - </md-input-container>  
59 - <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">  
60 - {{ 'device.aliases' | translate }}  
61 - </md-button>  
62 - <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDashboardSettings($event)">  
63 - {{ 'dashboard.settings' | translate }}  
64 - </md-button>  
65 - </section>  
66 - <div class="tb-absolute-fill"  
67 - ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">  
68 - <tb-dashboard  
69 - dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,  
70 - 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',  
71 - 'background-repeat': 'no-repeat',  
72 - 'background-attachment': 'scroll',  
73 - 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',  
74 - 'background-position': '0% 0%'}"  
75 - widgets="vm.widgets"  
76 - columns="vm.dashboard.configuration.gridSettings.columns"  
77 - margins="vm.dashboard.configuration.gridSettings.margins"  
78 - device-alias-list="vm.dashboard.configuration.deviceAliases"  
79 - is-edit="vm.isEdit"  
80 - is-mobile="vm.forceDashboardMobileMode"  
81 - is-mobile-disabled="vm.widgetEditMode"  
82 - is-edit-action-enabled="vm.isEdit"  
83 - is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"  
84 - is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"  
85 - on-edit-widget="vm.editWidget(event, widget)"  
86 - on-export-widget="vm.exportWidget(event, widget)"  
87 - on-widget-mouse-down="vm.widgetMouseDown(event, widget)"  
88 - on-widget-clicked="vm.widgetClicked(event, widget)"  
89 - on-widget-context-menu="vm.widgetContextMenu(event, widget)"  
90 - prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"  
91 - prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"  
92 - on-remove-widget="vm.removeWidget(event, widget)"  
93 - load-widgets="vm.loadDashboard()"  
94 - get-st-diff="vm.getServerTimeDiff()"  
95 - on-init="vm.dashboardInited(dashboard)"  
96 - on-init-failed="vm.dashboardInitFailed(e)">  
97 - </tb-dashboard>  
98 - </div>  
99 - <tb-details-sidenav class="tb-widget-details-sidenav"  
100 - header-title="vm.editingWidget.config.title"  
101 - header-subtitle="{{vm.editingWidgetSubtitle}}"  
102 - is-read-only="false"  
103 - is-open="vm.isEditingWidget"  
104 - is-always-edit="true"  
105 - on-close-details="vm.onEditWidgetClosed()"  
106 - on-toggle-details-edit-mode="vm.onRevertWidgetEdit(vm.widgetForm)"  
107 - on-apply-details="vm.saveWidget(vm.widgetForm)"  
108 - the-form="vm.widgetForm">  
109 - <details-buttons tb-help="vm.helpLinkIdForWidgetType()" help-container-id="help-container">  
110 - <div id="help-container"></div>  
111 - </details-buttons>  
112 - <form name="vm.widgetForm" ng-if="vm.isEditingWidget">  
113 - <tb-edit-widget  
114 - dashboard="vm.dashboard"  
115 - widget="vm.editingWidget"  
116 - the-form="vm.widgetForm">  
117 - </tb-edit-widget>  
118 - </form>  
119 - </tb-details-sidenav>  
120 - <tb-details-sidenav ng-if="!vm.widgetEditMode" class="tb-select-widget-sidenav"  
121 - header-title="'dashboard.select-widget-title' | translate"  
122 - header-height-px="120"  
123 - is-read-only="true"  
124 - is-open="vm.isAddingWidget"  
125 - is-edit="false"  
126 - on-close-details="vm.onAddWidgetClosed()">  
127 - <header-pane ng-if="vm.isAddingWidget">  
128 - <div layout="row">  
129 - <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>  
130 - <tb-widgets-bundle-select flex-offset="5"  
131 - flex  
132 - ng-model="vm.widgetsBundle"  
133 - tb-required="true"  
134 - select-first-bundle="false">  
135 - </tb-widgets-bundle-select>  
136 - </div>  
137 - </header-pane>  
138 - <div ng-if="vm.isAddingWidget">  
139 - <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||  
140 - vm.rpcWidgetTypes.length > 0 || vm.staticWidgetTypes.length > 0"  
141 - flex  
142 - class="tb-absolute-fill" md-border-bottom>  
143 - <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">  
144 - <tb-dashboard  
145 - widgets="vm.timeseriesWidgetTypes"  
146 - is-edit="false"  
147 - is-mobile="true"  
148 - is-edit-action-enabled="false"  
149 - is-remove-action-enabled="false"  
150 - on-widget-clicked="vm.addWidgetFromType(event, widget)">  
151 - </tb-dashboard>  
152 - </md-tab>  
153 - <md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">  
154 - <tb-dashboard  
155 - widgets="vm.latestWidgetTypes"  
156 - is-edit="false"  
157 - is-mobile="true"  
158 - is-edit-action-enabled="false"  
159 - is-remove-action-enabled="false"  
160 - on-widget-clicked="vm.addWidgetFromType(event, widget)">  
161 - </tb-dashboard>  
162 - </md-tab>  
163 - <md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">  
164 - <tb-dashboard  
165 - widgets="vm.rpcWidgetTypes"  
166 - is-edit="false"  
167 - is-mobile="true"  
168 - is-edit-action-enabled="false"  
169 - is-remove-action-enabled="false"  
170 - on-widget-clicked="vm.addWidgetFromType(event, widget)">  
171 - </tb-dashboard>  
172 - </md-tab>  
173 - <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">  
174 - <tb-dashboard  
175 - widgets="vm.staticWidgetTypes"  
176 - is-edit="false"  
177 - is-mobile="true"  
178 - is-edit-action-enabled="false"  
179 - is-remove-action-enabled="false"  
180 - on-widget-clicked="vm.addWidgetFromType(event, widget)">  
181 - </tb-dashboard>  
182 - </md-tab>  
183 - </md-tabs>  
184 - <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&  
185 - vm.rpcWidgetTypes.length === 0 && vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"  
186 - layout-align="center center"  
187 - style="text-transform: uppercase; display: flex;"  
188 - class="md-headline tb-absolute-fill">widgets-bundle.empty</span>  
189 - <span translate ng-if="!vm.widgetsBundle"  
190 - layout-align="center center"  
191 - style="text-transform: uppercase; display: flex;"  
192 - class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>  
193 - </div>  
194 - </tb-details-sidenav>  
195 - <!-- </section> -->  
196 - <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">  
197 - <md-fab-speed-dial ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"  
198 - md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up">  
199 - <md-fab-trigger>  
200 - <md-button ng-disabled="loading"  
201 - class="tb-btn-footer md-accent md-hue-2 md-fab"  
202 - aria-label="{{ 'dashboard.add-widget' | translate }}">  
203 - <md-tooltip md-direction="top">  
204 - {{ 'dashboard.add-widget' | translate }} 18 +<md-content flex tb-expand-fullscreen="vm.widgetEditMode || vm.iframeMode" expand-button-id="dashboard-expand-button"
  19 + hide-expand-button="vm.widgetEditMode || vm.iframeMode"
  20 + ng-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
  21 + 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
  22 + 'background-repeat': 'no-repeat',
  23 + 'background-attachment': 'scroll',
  24 + 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
  25 + 'background-position': '0% 0%'}">
  26 + <section class="tb-dashboard-toolbar"
  27 + ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
  28 + <md-fab-toolbar md-open="vm.toolbarOpened"
  29 + md-direction="left">
  30 + <md-fab-trigger class="align-with-text">
  31 + <md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.openToolbar()">
  32 + <md-tooltip ng-show="!vm.toolbarOpened" md-direction="top">
  33 + {{ 'dashboard.open-toolbar' | translate }}
205 </md-tooltip> 34 </md-tooltip>
206 - <ng-md-icon icon="add"></ng-md-icon> 35 + <md-icon aria-label="dashboard-toolbar" class="material-icons">more_horiz</md-icon>
207 </md-button> 36 </md-button>
208 </md-fab-trigger> 37 </md-fab-trigger>
209 - <md-fab-actions>  
210 - <md-button ng-disabled="loading"  
211 - class="tmd-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"  
212 - aria-label="{{ 'action.create' | translate }}">  
213 - <md-tooltip md-direction="top">  
214 - {{ 'dashboard.create-new-widget' | translate }}  
215 - </md-tooltip>  
216 - <ng-md-icon icon="insert_drive_file"></ng-md-icon>  
217 - </md-button>  
218 - <md-button ng-disabled="loading"  
219 - class="tmd-accent md-hue-2 md-fab" ng-click="vm.importWidget($event)"  
220 - aria-label="{{ 'action.import' | translate }}">  
221 - <md-tooltip md-direction="top">  
222 - {{ 'dashboard.import-widget' | translate }}  
223 - </md-tooltip>  
224 - <ng-md-icon icon="file_upload"></ng-md-icon>  
225 - </md-button>  
226 - </md-fab-actions>  
227 - </md-fab-speed-dial>  
228 - <md-button ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"  
229 - class="tb-btn-footer md-accent md-hue-2 md-fab"  
230 - aria-label="{{ 'action.apply' | translate }}"  
231 - ng-click="vm.saveDashboard()">  
232 - <md-tooltip md-direction="top">  
233 - {{ 'action.apply-changes' | translate }}  
234 - </md-tooltip>  
235 - <ng-md-icon icon="done"></ng-md-icon>  
236 - </md-button>  
237 - <md-button ng-show="!vm.isAddingWidget && !loading"  
238 - ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-disabled="loading"  
239 - class="tb-btn-footer md-accent md-hue-2 md-fab"  
240 - aria-label="{{ 'action.edit-mode' | translate }}"  
241 - ng-click="vm.toggleDashboardEditMode()">  
242 - <md-tooltip md-direction="top">  
243 - {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}  
244 - </md-tooltip>  
245 - <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"  
246 - options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>  
247 - </md-button> 38 + <md-toolbar>
  39 + <md-fab-actions class="md-toolbar-tools">
  40 + <md-button ng-show="!vm.isEdit" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
  41 + <md-tooltip md-direction="top">
  42 + {{ 'dashboard.close-toolbar' | translate }}
  43 + </md-tooltip>
  44 + <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
  45 + </md-button>
  46 + <md-button id="dashboard-expand-button"
  47 + aria-label="{{ 'fullscreen.fullscreen' | translate }}"
  48 + class="md-icon-button">
  49 + </md-button>
  50 + <tb-timewindow direction="left" aggregation ng-model="vm.dashboardConfiguration.timewindow">
  51 + </tb-timewindow>
  52 + <md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button"
  53 + ng-click="vm.openDeviceAliases($event)">
  54 + <md-tooltip md-direction="top">
  55 + {{ 'device.aliases' | translate }}
  56 + </md-tooltip>
  57 + <md-icon aria-label="{{ 'device.aliases' | translate }}" class="material-icons">devices_other</md-icon>
  58 + </md-button>
  59 + <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
  60 + ng-click="vm.openDashboardSettings($event)">
  61 + <md-tooltip md-direction="top">
  62 + {{ 'dashboard.settings' | translate }}
  63 + </md-tooltip>
  64 + <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
  65 + </md-button>
  66 + </md-fab-actions>
  67 + </md-toolbar>
  68 + </md-fab-toolbar>
  69 + </section>
  70 + <section class="tb-dashboard-container tb-absolute-fill"
  71 + ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
  72 + <section ng-show="!loading && vm.noData()" layout-align="center center"
  73 + ng-class="{'tb-padded' : !vm.widgetEditMode}"
  74 + style="text-transform: uppercase; display: flex; z-index: 1;"
  75 + class="md-headline tb-absolute-fill">
  76 + <span translate ng-if="!vm.isEdit">
  77 + dashboard.no-widgets
  78 + </span>
  79 + <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">
  80 + <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
  81 + {{ 'dashboard.add-widget' | translate }}
  82 + </md-button>
  83 + </section>
  84 + <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center"
  85 + ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
  86 + <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
  87 + <md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
  88 + <label translate ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">dashboard.title</label>
  89 + <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}" required name="title" ng-model="vm.dashboard.title">
  90 + </md-input-container>
  91 + </section>
  92 + <div class="tb-absolute-fill"
  93 + ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
  94 + <tb-dashboard
  95 + dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
  96 + 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
  97 + 'background-repeat': 'no-repeat',
  98 + 'background-attachment': 'scroll',
  99 + 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
  100 + 'background-position': '0% 0%'}"
  101 + widgets="vm.widgets"
  102 + columns="vm.dashboard.configuration.gridSettings.columns"
  103 + margins="vm.dashboard.configuration.gridSettings.margins"
  104 + device-alias-list="vm.dashboard.configuration.deviceAliases"
  105 + dashboard-timewindow="vm.dashboardConfiguration.timewindow"
  106 + is-edit="vm.isEdit"
  107 + is-mobile="vm.forceDashboardMobileMode"
  108 + is-mobile-disabled="vm.widgetEditMode"
  109 + is-edit-action-enabled="vm.isEdit"
  110 + is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
  111 + is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
  112 + on-edit-widget="vm.editWidget(event, widget)"
  113 + on-export-widget="vm.exportWidget(event, widget)"
  114 + on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
  115 + on-widget-clicked="vm.widgetClicked(event, widget)"
  116 + on-widget-context-menu="vm.widgetContextMenu(event, widget)"
  117 + prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
  118 + prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
  119 + on-remove-widget="vm.removeWidget(event, widget)"
  120 + load-widgets="vm.loadDashboard()"
  121 + get-st-diff="vm.getServerTimeDiff()"
  122 + on-init="vm.dashboardInited(dashboard)"
  123 + on-init-failed="vm.dashboardInitFailed(e)">
  124 + </tb-dashboard>
  125 + </div>
  126 + <tb-details-sidenav class="tb-widget-details-sidenav"
  127 + header-title="vm.editingWidget.config.title"
  128 + header-subtitle="{{vm.editingWidgetSubtitle}}"
  129 + is-read-only="false"
  130 + is-open="vm.isEditingWidget"
  131 + is-always-edit="true"
  132 + on-close-details="vm.onEditWidgetClosed()"
  133 + on-toggle-details-edit-mode="vm.onRevertWidgetEdit(vm.widgetForm)"
  134 + on-apply-details="vm.saveWidget(vm.widgetForm)"
  135 + the-form="vm.widgetForm">
  136 + <details-buttons tb-help="vm.helpLinkIdForWidgetType()" help-container-id="help-container">
  137 + <div id="help-container"></div>
  138 + </details-buttons>
  139 + <form name="vm.widgetForm" ng-if="vm.isEditingWidget">
  140 + <tb-edit-widget
  141 + dashboard="vm.dashboard"
  142 + widget="vm.editingWidget"
  143 + the-form="vm.widgetForm">
  144 + </tb-edit-widget>
  145 + </form>
  146 + </tb-details-sidenav>
  147 + <tb-details-sidenav ng-if="!vm.widgetEditMode" class="tb-select-widget-sidenav"
  148 + header-title="'dashboard.select-widget-title' | translate"
  149 + header-height-px="120"
  150 + is-read-only="true"
  151 + is-open="vm.isAddingWidget"
  152 + is-edit="false"
  153 + on-close-details="vm.onAddWidgetClosed()">
  154 + <header-pane ng-if="vm.isAddingWidget">
  155 + <div layout="row">
  156 + <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
  157 + <tb-widgets-bundle-select flex-offset="5"
  158 + flex
  159 + ng-model="vm.widgetsBundle"
  160 + tb-required="true"
  161 + select-first-bundle="false">
  162 + </tb-widgets-bundle-select>
  163 + </div>
  164 + </header-pane>
  165 + <div ng-if="vm.isAddingWidget">
  166 + <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||
  167 + vm.rpcWidgetTypes.length > 0 || vm.staticWidgetTypes.length > 0"
  168 + flex
  169 + class="tb-absolute-fill" md-border-bottom>
  170 + <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
  171 + <tb-dashboard
  172 + widgets="vm.timeseriesWidgetTypes"
  173 + is-edit="false"
  174 + is-mobile="true"
  175 + is-edit-action-enabled="false"
  176 + is-remove-action-enabled="false"
  177 + on-widget-clicked="vm.addWidgetFromType(event, widget)">
  178 + </tb-dashboard>
  179 + </md-tab>
  180 + <md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
  181 + <tb-dashboard
  182 + widgets="vm.latestWidgetTypes"
  183 + is-edit="false"
  184 + is-mobile="true"
  185 + is-edit-action-enabled="false"
  186 + is-remove-action-enabled="false"
  187 + on-widget-clicked="vm.addWidgetFromType(event, widget)">
  188 + </tb-dashboard>
  189 + </md-tab>
  190 + <md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
  191 + <tb-dashboard
  192 + widgets="vm.rpcWidgetTypes"
  193 + is-edit="false"
  194 + is-mobile="true"
  195 + is-edit-action-enabled="false"
  196 + is-remove-action-enabled="false"
  197 + on-widget-clicked="vm.addWidgetFromType(event, widget)">
  198 + </tb-dashboard>
  199 + </md-tab>
  200 + <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
  201 + <tb-dashboard
  202 + widgets="vm.staticWidgetTypes"
  203 + is-edit="false"
  204 + is-mobile="true"
  205 + is-edit-action-enabled="false"
  206 + is-remove-action-enabled="false"
  207 + on-widget-clicked="vm.addWidgetFromType(event, widget)">
  208 + </tb-dashboard>
  209 + </md-tab>
  210 + </md-tabs>
  211 + <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&
  212 + vm.rpcWidgetTypes.length === 0 && vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
  213 + layout-align="center center"
  214 + style="text-transform: uppercase; display: flex;"
  215 + class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
  216 + <span translate ng-if="!vm.widgetsBundle"
  217 + layout-align="center center"
  218 + style="text-transform: uppercase; display: flex;"
  219 + class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>
  220 + </div>
  221 + </tb-details-sidenav>
  222 + <!-- </section> -->
  223 + <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
  224 + <md-fab-speed-dial ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
  225 + md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up">
  226 + <md-fab-trigger>
  227 + <md-button ng-disabled="loading"
  228 + class="tb-btn-footer md-accent md-hue-2 md-fab"
  229 + aria-label="{{ 'dashboard.add-widget' | translate }}">
  230 + <md-tooltip md-direction="top">
  231 + {{ 'dashboard.add-widget' | translate }}
  232 + </md-tooltip>
  233 + <ng-md-icon icon="add"></ng-md-icon>
  234 + </md-button>
  235 + </md-fab-trigger>
  236 + <md-fab-actions>
  237 + <md-button ng-disabled="loading"
  238 + class="tmd-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
  239 + aria-label="{{ 'action.create' | translate }}">
  240 + <md-tooltip md-direction="top">
  241 + {{ 'dashboard.create-new-widget' | translate }}
  242 + </md-tooltip>
  243 + <ng-md-icon icon="insert_drive_file"></ng-md-icon>
  244 + </md-button>
  245 + <md-button ng-disabled="loading"
  246 + class="tmd-accent md-hue-2 md-fab" ng-click="vm.importWidget($event)"
  247 + aria-label="{{ 'action.import' | translate }}">
  248 + <md-tooltip md-direction="top">
  249 + {{ 'dashboard.import-widget' | translate }}
  250 + </md-tooltip>
  251 + <ng-md-icon icon="file_upload"></ng-md-icon>
  252 + </md-button>
  253 + </md-fab-actions>
  254 + </md-fab-speed-dial>
  255 + <md-button ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
  256 + class="tb-btn-footer md-accent md-hue-2 md-fab"
  257 + aria-label="{{ 'action.apply' | translate }}"
  258 + ng-click="vm.saveDashboard()">
  259 + <md-tooltip md-direction="top">
  260 + {{ 'action.apply-changes' | translate }}
  261 + </md-tooltip>
  262 + <ng-md-icon icon="done"></ng-md-icon>
  263 + </md-button>
  264 + <md-button ng-show="!vm.isAddingWidget && !loading"
  265 + ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-disabled="loading"
  266 + class="tb-btn-footer md-accent md-hue-2 md-fab"
  267 + aria-label="{{ 'action.edit-mode' | translate }}"
  268 + ng-click="vm.toggleDashboardEditMode()">
  269 + <md-tooltip md-direction="top">
  270 + {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}
  271 + </md-tooltip>
  272 + <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
  273 + options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
  274 + </md-button>
  275 + </section>
248 </section> 276 </section>
249 </md-content> 277 </md-content>
@@ -247,6 +247,7 @@ export default angular.module('thingsboard.locale', []) @@ -247,6 +247,7 @@ export default angular.module('thingsboard.locale', [])
247 "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", 247 "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
248 "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", 248 "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
249 "display-title": "Display dashboard title", 249 "display-title": "Display dashboard title",
  250 + "title-color": "Title color",
250 "import": "Import dashboard", 251 "import": "Import dashboard",
251 "export": "Export dashboard", 252 "export": "Export dashboard",
252 "export-failed-error": "Unable to export dashboard: {error}", 253 "export-failed-error": "Unable to export dashboard: {error}",
@@ -258,7 +259,9 @@ export default angular.module('thingsboard.locale', []) @@ -258,7 +259,9 @@ export default angular.module('thingsboard.locale', [])
258 "import-widget": "Import widget", 259 "import-widget": "Import widget",
259 "widget-file": "Widget file", 260 "widget-file": "Widget file",
260 "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.", 261 "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
261 - "widget-import-missing-aliases-title": "Select missing devices used by widget" 262 + "widget-import-missing-aliases-title": "Select missing devices used by widget",
  263 + "open-toolbar": "Open dashboard toolbar",
  264 + "close-toolbar": "Close toolbar"
262 }, 265 },
263 "datakey": { 266 "datakey": {
264 "settings": "Settings", 267 "settings": "Settings",
@@ -702,6 +705,7 @@ export default angular.module('thingsboard.locale', []) @@ -702,6 +705,7 @@ export default angular.module('thingsboard.locale', [])
702 "order": "Order", 705 "order": "Order",
703 "height": "Height", 706 "height": "Height",
704 "timewindow": "Timewindow", 707 "timewindow": "Timewindow",
  708 + "use-dashboard-timewindow": "Use dashboard timewindow",
705 "datasources": "Datasources", 709 "datasources": "Datasources",
706 "datasource-type": "Type", 710 "datasource-type": "Type",
707 "datasource-parameters": "Parameters", 711 "datasource-parameters": "Parameters",
@@ -360,9 +360,12 @@ export default class TbFlot { @@ -360,9 +360,12 @@ export default class TbFlot {
360 update() { 360 update() {
361 if (!this.isMouseInteraction) { 361 if (!this.isMouseInteraction) {
362 if (this.chartType === 'line' || this.chartType === 'bar') { 362 if (this.chartType === 'line' || this.chartType === 'bar') {
  363 + this.options.xaxis.min = this.ctx.timeWindow.minTime;
  364 + this.options.xaxis.max = this.ctx.timeWindow.maxTime;
363 this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime; 365 this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime;
364 this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime; 366 this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime;
365 if (this.chartType === 'bar') { 367 if (this.chartType === 'bar') {
  368 + this.options.series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
366 this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; 369 this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
367 } 370 }
368 this.ctx.plot.setData(this.ctx.data); 371 this.ctx.plot.setData(this.ctx.data);
@@ -879,6 +882,7 @@ export default class TbFlot { @@ -879,6 +882,7 @@ export default class TbFlot {
879 destroy() { 882 destroy() {
880 if (this.ctx.plot) { 883 if (this.ctx.plot) {
881 this.ctx.plot.destroy(); 884 this.ctx.plot.destroy();
  885 + this.ctx.plot = null;
882 } 886 }
883 } 887 }
884 888
@@ -193,6 +193,12 @@ md-sidenav { @@ -193,6 +193,12 @@ md-sidenav {
193 pointer-events: all; 193 pointer-events: all;
194 } 194 }
195 195
  196 +.md-color-picker-input-container {
  197 + md-input-container {
  198 + margin-bottom: 0px;
  199 + }
  200 +}
  201 +
196 /*********************** 202 /***********************
197 * THINGSBOARD SPECIFIC 203 * THINGSBOARD SPECIFIC
198 ***********************/ 204 ***********************/
@@ -201,6 +207,10 @@ md-sidenav { @@ -201,6 +207,10 @@ md-sidenav {
201 color: rgba(0,0,0,0.54); 207 color: rgba(0,0,0,0.54);
202 } 208 }
203 209
  210 +.tb-disabled-label {
  211 + color: rgba(0,0,0,0.44);
  212 +}
  213 +
204 label { 214 label {
205 &.tb-small { 215 &.tb-small {
206 pointer-events: none; 216 pointer-events: none;