From a0d7e4be05aafbc8c5cc463ed01a92e9037ed59d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 27 Feb 2017 15:07:43 +0200 Subject: [PATCH] UI: Add bars widget. Improve tooltips and aggregation. --- .../controller/DashboardController.java | 7 + .../cmd/TimeseriesSubscriptionCmd.java | 1 + .../TelemetryWebsocketMsgHandler.java | 6 +- .../telemetry/sub/SubscriptionUpdate.java | 12 +- ui/src/app/api/dashboard.service.js | 16 + ui/src/app/api/data-aggregator.js | 71 +- ui/src/app/api/datasource.service.js | 193 ++-- ui/src/app/components/dashboard.directive.js | 26 +- ui/src/app/components/dashboard.tpl.html | 2 +- ui/src/app/components/widget.controller.js | 54 +- ui/src/app/dashboard/dashboard.controller.js | 10 +- ui/src/app/dashboard/dashboard.tpl.html | 1 + .../attribute/attribute-table.directive.js | 6 +- .../device/attribute/attribute-table.tpl.html | 3 +- ui/src/app/widget/lib/flot-widget.js | 951 +++++++++++++++--- .../app/widget/widget-library.controller.js | 2 +- 16 files changed, 1044 insertions(+), 317 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 77898ceb7a..fc419b7020 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -32,6 +32,13 @@ import org.thingsboard.server.exception.ThingsboardException; @RequestMapping("/api") public class DashboardController extends BaseController { + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET) + @ResponseBody + public long getServerTime() throws ThingsboardException { + return System.currentTimeMillis(); + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) @ResponseBody diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java index f4eacf587a..20bd3e2e07 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java @@ -28,6 +28,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT @Data public class TimeseriesSubscriptionCmd extends SubscriptionCmd { + private long startTs; private long timeWindow; private int limit; private String agg; diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java index 8385bf1893..51181fda8e 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java @@ -191,8 +191,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { if (cmd.getTimeWindow() > 0) { List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); - long endTs = System.currentTimeMillis(); - startTs = endTs - cmd.getTimeWindow(); + startTs = cmd.getStartTs(); + long endTs = cmd.getStartTs() + cmd.getTimeWindow(); List queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); } else { @@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { return new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List data) { - sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), startTs, data)); + sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, startTs)); diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java index 8a9e7b2b24..4d8cf5310e 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java @@ -26,16 +26,10 @@ public class SubscriptionUpdate { private int errorCode; private String errorMsg; private Map> data; - private long serverStartTs; public SubscriptionUpdate(int subscriptionId, List data) { - this(subscriptionId, 0L, data); - } - - public SubscriptionUpdate(int subscriptionId, long serverStartTs, List data) { super(); this.subscriptionId = subscriptionId; - this.serverStartTs = serverStartTs; this.data = new TreeMap<>(); for (TsKvEntry tsEntry : data) { List values = this.data.get(tsEntry.getKey()); @@ -95,13 +89,9 @@ public class SubscriptionUpdate { return errorMsg; } - public long getServerStartTs() { - return serverStartTs; - } - @Override public String toString() { return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" - + data + ", serverStartTs=" + serverStartTs+ "]"; + + data + "]"; } } diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js index 9eb7329b17..be450ed477 100644 --- a/ui/src/app/api/dashboard.service.js +++ b/ui/src/app/api/dashboard.service.js @@ -22,6 +22,7 @@ function DashboardService($http, $q) { var service = { assignDashboardToCustomer: assignDashboardToCustomer, getCustomerDashboards: getCustomerDashboards, + getServerTimeDiff: getServerTimeDiff, getDashboard: getDashboard, getTenantDashboards: getTenantDashboards, deleteDashboard: deleteDashboard, @@ -71,6 +72,21 @@ function DashboardService($http, $q) { return deferred.promise; } + function getServerTimeDiff() { + var deferred = $q.defer(); + var url = '/api/dashboard/serverTime'; + var ct1 = Date.now(); + $http.get(url, null).then(function success(response) { + var ct2 = Date.now(); + var st = response.data; + var stDiff = Math.ceil(st - (ct1+ct2)/2); + deferred.resolve(stDiff); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + function getDashboard(dashboardId) { var deferred = $q.defer(); var url = '/api/dashboard/' + dashboardId; diff --git a/ui/src/app/api/data-aggregator.js b/ui/src/app/api/data-aggregator.js index 314ba64ba7..e273a9d513 100644 --- a/ui/src/app/api/data-aggregator.js +++ b/ui/src/app/api/data-aggregator.js @@ -16,31 +16,26 @@ export default class DataAggregator { - constructor(onDataCb, limit, aggregationType, timeWindow, types, $timeout, $filter) { + constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, types, $timeout, $filter) { this.onDataCb = onDataCb; + this.tsKeyNames = tsKeyNames; + this.startTs = startTs; this.aggregationType = aggregationType; this.types = types; this.$timeout = $timeout; this.$filter = $filter; this.dataReceived = false; this.noAggregation = aggregationType === types.aggregation.none.value; - var interval = Math.floor(timeWindow / limit); - if (!this.noAggregation) { - this.interval = Math.max(interval, 1000); - this.limit = Math.ceil(interval/this.interval * limit); - this.timeWindow = this.interval * this.limit; - } else { - this.limit = limit; - this.timeWindow = interval * this.limit; - this.interval = 1000; - } + this.limit = limit; + this.timeWindow = timeWindow; + this.interval = interval; this.aggregationTimeout = this.interval; switch (aggregationType) { case types.aggregation.min.value: this.aggFunction = min; break; case types.aggregation.max.value: - this.aggFunction = max + this.aggFunction = max; break; case types.aggregation.avg.value: this.aggFunction = avg; @@ -59,42 +54,56 @@ export default class DataAggregator { } } - onData(data) { + onData(data, update, history) { if (!this.dataReceived) { this.elapsed = 0; this.dataReceived = true; - this.startTs = data.serverStartTs; this.endTs = this.startTs + this.timeWindow; - this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); - this.onInterval(currentTime()); + if (update) { + this.aggregationMap = {}; + updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, + this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); + } else { + this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); + } + this.onInterval(currentTime(), history); } else { updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); + if (history) { + this.onInterval(currentTime(), history); + } } } - onInterval(startedTime) { + onInterval(startedTime, history) { var now = currentTime(); this.elapsed += now - startedTime; if (this.intervalTimeoutHandle) { this.$timeout.cancel(this.intervalTimeoutHandle); this.intervalTimeoutHandle = null; } - var delta = Math.floor(this.elapsed / this.interval); - if (delta || !this.data) { - this.startTs += delta * this.interval; - this.endTs += delta * this.interval; - this.data = toData(this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit); - this.elapsed = this.elapsed - delta * this.interval; + if (!history) { + var delta = Math.floor(this.elapsed / this.interval); + if (delta || !this.data) { + this.startTs += delta * this.interval; + this.endTs += delta * this.interval; + this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit); + this.elapsed = this.elapsed - delta * this.interval; + } + } else { + this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit); } if (this.onDataCb) { this.onDataCb(this.data, this.startTs, this.endTs); } var self = this; - this.intervalTimeoutHandle = this.$timeout(function() { - self.onInterval(now); - }, this.aggregationTimeout, false); + if (!history) { + this.intervalTimeoutHandle = this.$timeout(function() { + self.onInterval(now); + }, this.aggregationTimeout, false); + } } reset() { @@ -172,12 +181,12 @@ function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunctio } } -function toData(aggregationMap, startTs, endTs, $filter, limit) { +function toData(tsKeyNames, aggregationMap, startTs, endTs, $filter, limit) { var data = {}; + for (var k in tsKeyNames) { + data[tsKeyNames[k]] = []; + } for (var key in aggregationMap) { - if (!data[key]) { - data[key] = []; - } var aggKeyData = aggregationMap[key]; var keyData = data[key]; for (var aggTimestamp in aggKeyData) { @@ -185,7 +194,7 @@ function toData(aggregationMap, startTs, endTs, $filter, limit) { delete aggKeyData[aggTimestamp]; } else if (aggTimestamp <= endTs) { var aggData = aggKeyData[aggTimestamp]; - var kvPair = [aggTimestamp, aggData.aggValue]; + var kvPair = [Number(aggTimestamp), aggData.aggValue]; keyData.push(kvPair); } } diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js index acfe1249d2..b44f85d18d 100644 --- a/ui/src/app/api/datasource.service.js +++ b/ui/src/app/api/datasource.service.js @@ -108,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic datasourceSubscription.subscriptionTimewindow.fixedWindow; var realtime = datasourceSubscription.subscriptionTimewindow && datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; - var dataGenFunction = null; var timer; var frequency; + var dataAggregator; var subscription = { addListener: addListener, @@ -131,19 +131,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic dataKey.index = i; var key; if (datasourceType === types.datasourceType.function) { - key = utils.objectHashCode(dataKey); if (!dataKey.func) { dataKey.func = new Function("time", "prevValue", dataKey.funcBody); } - datasourceData[key] = { - data: [] - }; - dataKeys[key] = dataKey; - } else if (datasourceType === types.datasourceType.device) { - key = dataKey.name + '_' + dataKey.type; + } else { if (dataKey.postFuncBody && !dataKey.postFunc) { dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody); } + } + if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) { + if (datasourceType === types.datasourceType.function) { + key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type; + } else { + key = dataKey.name + '_' + dataKey.type; + } var dataKeysList = dataKeys[key]; if (!dataKeysList) { dataKeysList = []; @@ -153,24 +154,19 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic datasourceData[key + '_' + index] = { data: [] }; + } else { + key = utils.objectHashCode(dataKey); + datasourceData[key] = { + data: [] + }; + dataKeys[key] = dataKey; } dataKey.key = key; } if (datasourceType === types.datasourceType.function) { frequency = 1000; if (datasourceSubscription.type === types.widgetType.timeseries.value) { - dataGenFunction = generateSeries; - var window; - if (realtime) { - window = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; - } else { - window = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs - - datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs; - } - frequency = window / 1000 * 20; - } else if (datasourceSubscription.type === types.widgetType.latest.value) { - dataGenFunction = generateLatest; - frequency = 1000; + frequency = Math.min(datasourceSubscription.subscriptionTimewindow.aggregation.interval, 5000); } } } @@ -193,14 +189,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic function syncListener(listener) { var key; var dataKey; - if (datasourceType === types.datasourceType.function) { - for (key in dataKeys) { - dataKey = dataKeys[key]; - listener.dataUpdated(datasourceData[key], - listener.datasourceIndex, - dataKey.index); - } - } else if (datasourceType === types.datasourceType.device) { + if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) { for (key in dataKeys) { var dataKeysList = dataKeys[key]; for (var i = 0; i < dataKeysList.length; i++) { @@ -211,6 +200,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic dataKey.index); } } + } else { + for (key in dataKeys) { + dataKey = dataKeys[key]; + listener.dataUpdated(datasourceData[key], + listener.datasourceIndex, + dataKey.index); + } } } @@ -218,7 +214,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic if (history && !hasListeners()) { return; } - //$log.debug("started!"); + var subsTw = datasourceSubscription.subscriptionTimewindow; + var tsKeyNames = []; + var dataKey; + if (datasourceType === types.datasourceType.device) { //send subscribe command @@ -228,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic for (var key in dataKeys) { var dataKeysList = dataKeys[key]; - var dataKey = dataKeysList[0]; + dataKey = dataKeysList[0]; if (dataKey.type === types.dataKeyType.timeseries) { if (tsKeys.length > 0) { tsKeys += ','; } tsKeys += dataKey.name; + tsKeyNames.push(dataKey.name); } else if (dataKey.type === types.dataKeyType.attribute) { if (attrKeys.length > 0) { attrKeys += ','; @@ -252,10 +252,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic var historyCommand = { deviceId: datasourceSubscription.deviceId, keys: tsKeys, - startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs, - endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs, - limit: datasourceSubscription.subscriptionTimewindow.aggregation.limit, - agg: datasourceSubscription.subscriptionTimewindow.aggregation.type + startTs: subsTw.fixedWindow.startTimeMs, + endTs: subsTw.fixedWindow.endTimeMs, + limit: subsTw.aggregation.limit, + agg: subsTw.aggregation.type }; subscriber = { @@ -287,16 +287,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic }; if (datasourceSubscription.type === types.widgetType.timeseries.value) { - subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; - subscriptionCommand.limit = datasourceSubscription.subscriptionTimewindow.aggregation.limit; - subscriptionCommand.agg = datasourceSubscription.subscriptionTimewindow.aggregation.type; - var dataAggregator = new DataAggregator( + subscriptionCommand.startTs = subsTw.startTs; + subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow; + subscriptionCommand.limit = subsTw.aggregation.limit; + subscriptionCommand.agg = subsTw.aggregation.type; + dataAggregator = new DataAggregator( function(data, startTs, endTs) { onData(data, types.dataKeyType.timeseries, startTs, endTs); }, - subscriptionCommand.limit, - subscriptionCommand.agg, - subscriptionCommand.timeWindow, + tsKeyNames, + subsTw.startTs, + subsTw.aggregation.limit, + subsTw.aggregation.type, + subsTw.aggregation.timeWindow, + subsTw.aggregation.interval, types, $timeout, $filter @@ -308,9 +312,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic dataAggregator.reset(); onReconnected(); } - subscriber.onDestroy = function() { - dataAggregator.destroy(); - } } else { subscriber.onReconnected = function() { onReconnected(); @@ -353,7 +354,30 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic } - } else if (dataGenFunction) { + } else if (datasourceType === types.datasourceType.function) { + if (datasourceSubscription.type === types.widgetType.timeseries.value) { + for (key in dataKeys) { + var dataKeyList = dataKeys[key]; + for (var index = 0; index < dataKeyList.length; index++) { + dataKey = dataKeyList[index]; + tsKeyNames.push(dataKey.name+'_'+dataKey.index); + } + } + dataAggregator = new DataAggregator( + function (data, startTs, endTs) { + onData(data, types.dataKeyType.function, startTs, endTs); + }, + tsKeyNames, + subsTw.startTs, + subsTw.aggregation.limit, + subsTw.aggregation.type, + subsTw.aggregation.timeWindow, + subsTw.aggregation.interval, + types, + $timeout, + $filter + ); + } if (history) { onTick(); } else { @@ -377,30 +401,17 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic } subscribers = {}; } - } - - function boundToInterval(data, timewindowMs) { - if (data.length > 1) { - var start = data[0][0]; - var end = data[data.length - 1][0]; - var i = 0; - var currentInterval = end - start; - while (currentInterval > timewindowMs && i < data.length - 2) { - i++; - start = data[i][0]; - currentInterval = end - start; - } - if (i > 1) { - data.splice(0, i - 1); - } + if (dataAggregator) { + dataAggregator.destroy(); + dataAggregator = null; } - return data; } - function generateSeries(dataKey, startTime, endTime) { + function generateSeries(dataKey, index, startTime, endTime) { var data = []; var prevSeries; - var datasourceKeyData = datasourceData[dataKey.key].data; + var datasourceDataKey = dataKey.key + '_' + index; + var datasourceKeyData = datasourceData[datasourceDataKey].data; if (datasourceKeyData.length > 0) { prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; } else { @@ -417,18 +428,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic if (data.length > 0) { dataKey.lastUpdateTime = data[data.length - 1][0]; } - if (realtime) { - datasourceData[dataKey.key].data = boundToInterval(datasourceKeyData.concat(data), - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs); - } else { - datasourceData[dataKey.key].data = data; - } - for (var i in listeners) { - var listener = listeners[i]; - listener.dataUpdated(datasourceData[dataKey.key], - listener.datasourceIndex, - dataKey.index); - } + return data; } function generateLatest(dataKey) { @@ -458,23 +458,32 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic if (datasourceSubscription.type === types.widgetType.timeseries.value) { var startTime; var endTime; - for (key in dataKeys) { - var dataKey = dataKeys[key]; - if (!startTime) { - if (realtime) { - endTime = (new Date).getTime(); - if (dataKey.lastUpdateTime) { - startTime = dataKey.lastUpdateTime + frequency; - } else { - startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; - } - } else { - startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs; - endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs; - } + var generatedData = { + data: { + } + }; + for (key in dataKeys) { + var dataKeyList = dataKeys[key]; + for (var index = 0; index < dataKeyList.length; index ++) { + var dataKey = dataKeyList[index]; + if (!startTime) { + if (realtime) { + if (dataKey.lastUpdateTime) { + startTime = dataKey.lastUpdateTime + frequency + } else { + startTime = datasourceSubscription.subscriptionTimewindow.startTs; + } + endTime = startTime + datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; + } else { + startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs; + endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs; + } + } + var data = generateSeries(dataKey, index, startTime, endTime); + generatedData.data[dataKey.name+'_'+dataKey.index] = data; } - generateSeries(dataKey, startTime, endTime); } + dataAggregator.onData(generatedData, true, history); } else if (datasourceSubscription.type === types.widgetType.latest.value) { for (key in dataKeys) { generateLatest(dataKeys[key]); @@ -568,8 +577,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic } if (data.length > 0 || (startTs && endTs)) { datasourceData[datasourceKey].data = data; - datasourceData[datasourceKey].startTs = startTs; - datasourceData[datasourceKey].endTs = endTs; for (var i2 in listeners) { var listener = listeners[i2]; listener.dataUpdated(datasourceData[datasourceKey], diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index e67a444d70..3889339a23 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -68,6 +68,7 @@ function Dashboard() { prepareDashboardContextMenu: '&?', prepareWidgetContextMenu: '&?', loadWidgets: '&?', + getStDiff: '&?', onInit: '&?', onInitFailed: '&?', dashboardStyle: '=?' @@ -94,6 +95,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ vm.gridster = null; + vm.stDiff = 0; + vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; vm.dashboardLoading = true; @@ -302,7 +305,28 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ }); }); - loadDashboard(); + loadStDiff(); + + function loadStDiff() { + if (vm.getStDiff) { + var promise = vm.getStDiff(); + if (promise) { + promise.then(function (stDiff) { + vm.stDiff = stDiff; + loadDashboard(); + }, function () { + vm.stDiff = 0; + loadDashboard(); + }); + } else { + vm.stDiff = 0; + loadDashboard(); + } + } else { + vm.stDiff = 0; + loadDashboard(); + } + } function loadDashboard() { resetWidgetClick(); diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html index 0e367e9a2f..dc46e35472 100644 --- a/ui/src/app/components/dashboard.tpl.html +++ b/ui/src/app/components/dashboard.tpl.html @@ -93,7 +93,7 @@
+ locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isEdit: vm.isEdit, stDiff: vm.stDiff }">
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index cdcdbfb687..f7e9498f7e 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; /*@ngInject*/ export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, - datasourceService, deviceService, visibleRect, isEdit, widget, deviceAliasList, widgetType) { + datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) { var vm = this; @@ -46,7 +46,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q realtimeWindowMs: null, aggregation: null }; - var dataUpdateTimer = null; var dataUpdateCaf = null; /* @@ -72,7 +71,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q settings: widget.config.settings, datasources: widget.config.datasources, data: [], - timeWindow: {}, + timeWindow: { + stDiff: stDiff + }, timewindowFunctions: { onUpdateTimewindow: onUpdateTimewindow, onResetTimewindow: onResetTimewindow @@ -154,10 +155,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } } - function updateTimewindow(startTs, endTs) { + function updateTimewindow() { + widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000; if (subscriptionTimewindow.realtimeWindowMs) { - widgetContext.timeWindow.maxTime = endTs || (new Date).getTime(); - widgetContext.timeWindow.minTime = startTs || (widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs); + widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff; + widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs; } else if (subscriptionTimewindow.fixedWindow) { widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs; widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs; @@ -165,10 +167,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } function onDataUpdated() { - if (dataUpdateTimer) { - $timeout.cancel(dataUpdateTimer); - dataUpdateTimer = null; - } if (widgetContext.inited) { if (dataUpdateCaf) { dataUpdateCaf(); @@ -496,7 +494,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q startTimeMs: startTimeMs, endTimeMs: endTimeMs } - } + }, + aggregation: angular.copy(widget.config.timewindow.aggregation) }; } @@ -513,14 +512,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } if (update) { if (subscriptionTimewindow.realtimeWindowMs) { - updateTimewindow(sourceData.startTs, sourceData.endTs); + updateTimewindow(); } widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data; - if (widgetContext.data.length > 1 && !dataUpdateTimer) { - dataUpdateTimer = $timeout(onDataUpdated, 300, false); - } else { - onDataUpdated(); - } + onDataUpdated(); } } @@ -552,10 +547,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q function unsubscribe() { if (widget.type !== types.widgetType.rpc.value) { - if (dataUpdateTimer) { - $timeout.cancel(dataUpdateTimer); - dataUpdateTimer = null; - } for (var i in datasourceListeners) { var listener = datasourceListeners[i]; datasourceService.unsubscribeFromDatasource(listener); @@ -575,7 +566,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q }; if (widget.type === types.widgetType.timeseries.value && angular.isDefined(widget.config.timewindow)) { - + var timeWindow = 0; if (angular.isDefined(widget.config.timewindow.aggregation)) { subscriptionTimewindow.aggregation = { limit: widget.config.timewindow.aggregation.limit || 200, @@ -585,6 +576,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q if (angular.isDefined(widget.config.timewindow.realtime)) { subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs; + subscriptionTimewindow.startTs = (new Date).getTime() + widgetContext.timeWindow.stDiff - subscriptionTimewindow.realtimeWindowMs; + timeWindow = subscriptionTimewindow.realtimeWindowMs; } else if (angular.isDefined(widget.config.timewindow.history)) { if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) { var currentTime = (new Date).getTime(); @@ -592,14 +585,31 @@ export default function WidgetController($scope, $timeout, $window, $element, $q startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs, endTimeMs: currentTime } + timeWindow = widget.config.timewindow.history.timewindowMs; } else { subscriptionTimewindow.fixedWindow = { startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs, endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs } + timeWindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; } + subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; + } + var aggregation = subscriptionTimewindow.aggregation; + var noAggregation = aggregation.type === types.aggregation.none.value; + var interval = Math.floor(timeWindow / aggregation.limit); + if (!noAggregation) { + aggregation.interval = Math.max(interval, 1000); + aggregation.limit = Math.ceil(interval/aggregation.interval * aggregation.limit); + aggregation.timeWindow = aggregation.interval * aggregation.limit; + } else { + aggregation.timeWindow = interval * aggregation.limit; + aggregation.interval = 1000; } updateTimewindow(); + if (subscriptionTimewindow.fixedWindow) { + onDataUpdated(); + } } for (var i in widget.config.datasources) { var datasource = widget.config.datasources[i]; diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index c557ed3286..c2a9cf316a 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -61,6 +61,7 @@ export default function DashboardController(types, widgetService, userService, vm.isTenantAdmin = isTenantAdmin; vm.isSystemAdmin = isSystemAdmin; vm.loadDashboard = loadDashboard; + vm.getServerTimeDiff = getServerTimeDiff; vm.noData = noData; vm.onAddWidgetClosed = onAddWidgetClosed; vm.onEditWidgetClosed = onEditWidgetClosed; @@ -94,10 +95,9 @@ export default function DashboardController(types, widgetService, userService, widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( function (widgetTypes) { - widgetTypes = $filter('orderBy')(widgetTypes, ['-name']); + widgetTypes = $filter('orderBy')(widgetTypes, ['-createdTime']); var top = 0; - var sizeY = 0; if (widgetTypes.length > 0) { loadNext(0); @@ -135,7 +135,7 @@ export default function DashboardController(types, widgetService, userService, } else if (widgetTypeInfo.type === types.widgetType.static.value) { vm.staticWidgetTypes.push(widget); } - top += sizeY; + top += widget.sizeY; loadNextOrComplete(i); } @@ -144,6 +144,10 @@ export default function DashboardController(types, widgetService, userService, } } + function getServerTimeDiff() { + return dashboardService.getServerTimeDiff(); + } + function loadDashboard() { var deferred = $q.defer(); diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index 130a37bb67..298d365bb8 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -91,6 +91,7 @@ prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)" on-remove-widget="vm.removeWidget(event, widget)" load-widgets="vm.loadDashboard()" + get-st-diff="vm.getServerTimeDiff()" on-init="vm.dashboardInited(dashboard)" on-init-failed="vm.dashboardInitFailed(e)"> diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js index d167928429..ba3e4662a7 100644 --- a/ui/src/app/device/attribute/attribute-table.directive.js +++ b/ui/src/app/device/attribute/attribute-table.directive.js @@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller'; /*@ngInject*/ export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, - $document, $translate, utils, types, deviceService, widgetService) { + $document, $translate, utils, types, dashboardService, deviceService, widgetService) { var linker = function (scope, element, attrs) { @@ -357,6 +357,10 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.getDeviceAttributes(true); } + scope.getServerTimeDiff = function() { + return dashboardService.getServerTimeDiff(); + } + scope.addWidgetToDashboard = function($event) { if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) { var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0]; diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html index 2f88a272a1..e2efe36bf8 100644 --- a/ui/src/app/device/attribute/attribute-table.tpl.html +++ b/ui/src/app/device/attribute/attribute-table.tpl.html @@ -158,8 +158,9 @@ diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 72401f9aee..c35b363fc8 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -22,6 +22,8 @@ import 'flot/src/jquery.flot'; import 'flot/src/plugins/jquery.flot.time'; import 'flot/src/plugins/jquery.flot.selection'; import 'flot/src/plugins/jquery.flot.pie'; +import 'flot/src/plugins/jquery.flot.crosshair'; +import 'flot/src/plugins/jquery.flot.stack'; /* eslint-disable angular/angularelement */ export default class TbFlot { @@ -38,8 +40,8 @@ export default class TbFlot { var keySettings = series.dataKey.settings; series.lines = { - fill: keySettings.fillLines || false, - show: keySettings.showLines || true + fill: keySettings.fillLines === true, + show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true }; series.points = { @@ -58,36 +60,34 @@ export default class TbFlot { series.highlightColor = lineColor.toRgbString(); } - - var tbFlot = this; - ctx.tooltip = $('#flot-series-tooltip'); if (ctx.tooltip.length === 0) { ctx.tooltip = $("
"); ctx.tooltip.css({ fontSize: "12px", fontFamily: "Roboto", - lineHeight: "24px", + fontWeight: "300", + lineHeight: "18px", opacity: "1", backgroundColor: "rgba(0,0,0,0.7)", - color: "#fff", + color: "#D9DADB", position: "absolute", display: "none", zIndex: "100", - padding: "2px 8px", + padding: "4px 10px", borderRadius: "4px" }).appendTo("body"); } - ctx.tooltipFormatter = function(item) { - var label = item.series.label; - var color = item.series.color; - var content = ''; - if (tbFlot.chartType === 'line') { - var timestamp = parseInt(item.datapoint[0]); - var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); - content += '' + date + '
'; - } + var tbFlot = this; + + function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent) { + var divElement = $('
'); + divElement.css({ + display: "flex", + alignItems: "center", + justifyContent: "center" + }); var lineSpan = $(''); lineSpan.css({ backgroundColor: color, @@ -97,27 +97,76 @@ export default class TbFlot { verticalAlign: "middle", marginRight: "5px" }); - content += lineSpan.prop('outerHTML'); - + divElement.append(lineSpan); var labelSpan = $('' + label + ':'); labelSpan.css({ marginRight: "10px" }); - content += labelSpan.prop('outerHTML'); - var value = tbFlot.chartType === 'line' ? item.datapoint[1] : item.datapoint[1][0][1]; - content += ' ' + value.toFixed(ctx.trackDecimals); - if (settings.units) { - content += ' ' + settings.units; + if (active) { + labelSpan.css({ + color: "#FFF", + fontWeight: "700" + }); } - if (tbFlot.chartType === 'pie') { - content += ' (' + Math.round(item.series.percent) + ' %)'; + divElement.append(labelSpan); + var valueContent = value.toFixed(trackDecimals); + if (units) { + valueContent += ' ' + units; } - content += ''; - return content; - }; + if (angular.isNumber(percent)) { + valueContent += ' (' + Math.round(percent) + ' %)'; + } + var valueSpan = $('' + valueContent + ''); + valueSpan.css({ + marginLeft: "auto", + fontWeight: "700" + }); + if (active) { + valueSpan.css({ + color: "#FFF" + }); + } + divElement.append(valueSpan); + + return divElement; + } + + if (this.chartType === 'pie') { + ctx.tooltipFormatter = function(item) { + var divElement = seriesInfoDiv(item.series.label, item.series.color, + item.datapoint[1][0][1], tbFlot.ctx.settings.units, tbFlot.ctx.trackDecimals, true, item.series.percent); + return divElement.prop('outerHTML'); + }; + } else { + ctx.tooltipFormatter = function(hoverInfo, seriesIndex) { + var content = ''; + var timestamp = parseInt(hoverInfo.time); + var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); + var dateDiv = $('
' + date + '
'); + dateDiv.css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "4px", + fontWeight: "700" + }); + content += dateDiv.prop('outerHTML'); + for (var i in hoverInfo.seriesHover) { + var seriesHoverInfo = hoverInfo.seriesHover[i]; + if (tbFlot.ctx.tooltipIndividual && seriesHoverInfo.index !== seriesIndex) { + continue; + } + var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color, + seriesHoverInfo.value, tbFlot.ctx.settings.units, tbFlot.ctx.trackDecimals, seriesHoverInfo.index === seriesIndex); + content += divElement.prop('outerHTML'); + } + return content; + }; + } var settings = ctx.settings; ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1; + ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false); var font = { color: settings.fontColor || "#545454", @@ -134,7 +183,7 @@ export default class TbFlot { grid: { hoverable: true, mouseActiveRadius: 10, - autoHighlight: true + autoHighlight: ctx.tooltipIndividual === true }, selection : { mode : ctx.isMobile ? null : 'x' }, legend : { @@ -155,7 +204,7 @@ export default class TbFlot { settings.legend.backgroundOpacity : 0.85; } - if (this.chartType === 'line') { + if (this.chartType === 'line' || this.chartType === 'bar') { options.xaxis = { mode: 'time', timezone: 'browser', @@ -208,6 +257,28 @@ export default class TbFlot { } } + options.crosshair = { + mode: 'x' + } + + options.series = { + stack: settings.stack === true + } + + if (this.chartType === 'bar') { + options.series.lines = { + show: false, + fill: false, + steps: false + } + options.series.bars ={ + show: true, + barWidth: ctx.timeWindow.interval * 0.6, + lineWidth: 0, + fill: 0.9 + } + } + options.xaxis.min = ctx.timeWindow.minTime; options.xaxis.max = ctx.timeWindow.maxTime; } else if (this.chartType === 'pie') { @@ -271,11 +342,12 @@ export default class TbFlot { update() { if (!this.isMouseInteraction) { - if (this.chartType === 'line') { + if (this.chartType === 'line' || this.chartType === 'bar') { this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime; this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime; - } - if (this.chartType === 'line') { + if (this.chartType === 'bar') { + this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; + } this.ctx.plot.setData(this.ctx.data); this.ctx.plot.setupGrid(); this.ctx.plot.draw(); @@ -290,6 +362,702 @@ export default class TbFlot { } } + resize() { + this.ctx.plot.resize(); + if (this.chartType !== 'pie') { + this.ctx.plot.setupGrid(); + } + this.ctx.plot.draw(); + } + + static get pieSettingsSchema() { + return { + "schema": { + "type": "object", + "title": "Settings", + "properties": { + "radius": { + "title": "Radius", + "type": "number", + "default": 1 + }, + "innerRadius": { + "title": "Inner radius", + "type": "number", + "default": 0 + }, + "tilt": { + "title": "Tilt", + "type": "number", + "default": 1 + }, + "animatedPie": { + "title": "Enable pie animation (experimental)", + "type": "boolean", + "default": false + }, + "stroke": { + "title": "Stroke", + "type": "object", + "properties": { + "color": { + "title": "Color", + "type": "string", + "default": "" + }, + "width": { + "title": "Width (pixels)", + "type": "number", + "default": 0 + } + } + }, + "showLabels": { + "title": "Show labels", + "type": "boolean", + "default": false + }, + "fontColor": { + "title": "Font color", + "type": "string", + "default": "#545454" + }, + "fontSize": { + "title": "Font size", + "type": "number", + "default": 10 + }, + "decimals": { + "title": "Number of digits after floating point", + "type": "number", + "default": 1 + }, + "units": { + "title": "Special symbol to show next to value", + "type": "string", + "default": "" + }, + "legend": { + "title": "Legend settings", + "type": "object", + "properties": { + "show": { + "title": "Show legend", + "type": "boolean", + "default": true + }, + "position": { + "title": "Position", + "type": "string", + "default": "nw" + }, + "labelBoxBorderColor": { + "title": "Label box border color", + "type": "string", + "default": "#CCCCCC" + }, + "backgroundColor": { + "title": "Background color", + "type": "string", + "default": "#F0F0F0" + }, + "backgroundOpacity": { + "title": "Background opacity", + "type": "number", + "default": 0.85 + } + } + } + }, + "required": [] + }, + "form": [ + "radius", + "innerRadius", + "animatedPie", + "tilt", + { + "key": "stroke", + "items": [ + { + "key": "stroke.color", + "type": "color" + }, + "stroke.width" + ] + }, + "showLabels", + { + "key": "fontColor", + "type": "color" + }, + "fontSize", + "decimals", + "units", + { + "key": "legend", + "items": [ + "legend.show", + { + "key": "legend.position", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "nw", + "label": "North-west" + }, + { + "value": "ne", + "label": "North-east" + }, + { + "value": "sw", + "label": "South-west" + }, + { + "value": "se", + "label": "Soth-east" + } + ] + }, + { + "key": "legend.labelBoxBorderColor", + "type": "color" + }, + { + "key": "legend.backgroundColor", + "type": "color" + }, + "legend.backgroundOpacity" + ] + } + ] + } + } + + static get settingsSchema() { + return { + "schema": { + "type": "object", + "title": "Settings", + "properties": { + "stack": { + "title": "Stacking", + "type": "boolean", + "default": false + }, + "shadowSize": { + "title": "Shadow size", + "type": "number", + "default": 4 + }, + "fontColor": { + "title": "Font color", + "type": "string", + "default": "#545454" + }, + "fontSize": { + "title": "Font size", + "type": "number", + "default": 10 + }, + "decimals": { + "title": "Number of digits after floating point", + "type": "number", + "default": 1 + }, + "units": { + "title": "Special symbol to show next to value", + "type": "string", + "default": "" + }, + "tooltipIndividual": { + "title": "Hover individual points", + "type": "boolean", + "default": false + }, + "grid": { + "title": "Grid settings", + "type": "object", + "properties": { + "color": { + "title": "Primary color", + "type": "string", + "default": "#545454" + }, + "backgroundColor": { + "title": "Background color", + "type": "string", + "default": null + }, + "tickColor": { + "title": "Ticks color", + "type": "string", + "default": "#DDDDDD" + }, + "outlineWidth": { + "title": "Grid outline/border width (px)", + "type": "number", + "default": 1 + }, + "verticalLines": { + "title": "Show vertical lines", + "type": "boolean", + "default": true + }, + "horizontalLines": { + "title": "Show horizontal lines", + "type": "boolean", + "default": true + } + } + }, + "legend": { + "title": "Legend settings", + "type": "object", + "properties": { + "show": { + "title": "Show legend", + "type": "boolean", + "default": true + }, + "position": { + "title": "Position", + "type": "string", + "default": "nw" + }, + "labelBoxBorderColor": { + "title": "Label box border color", + "type": "string", + "default": "#CCCCCC" + }, + "backgroundColor": { + "title": "Background color", + "type": "string", + "default": "#F0F0F0" + }, + "backgroundOpacity": { + "title": "Background opacity", + "type": "number", + "default": 0.85 + } + } + }, + "xaxis": { + "title": "X axis settings", + "type": "object", + "properties": { + "showLabels": { + "title": "Show labels", + "type": "boolean", + "default": true + }, + "title": { + "title": "Axis title", + "type": "string", + "default": null + }, + "titleAngle": { + "title": "Axis title's angle in degrees", + "type": "number", + "default": 0 + }, + "color": { + "title": "Ticks color", + "type": "string", + "default": null + } + } + }, + "yaxis": { + "title": "Y axis settings", + "type": "object", + "properties": { + "showLabels": { + "title": "Show labels", + "type": "boolean", + "default": true + }, + "title": { + "title": "Axis title", + "type": "string", + "default": null + }, + "titleAngle": { + "title": "Axis title's angle in degrees", + "type": "number", + "default": 0 + }, + "color": { + "title": "Ticks color", + "type": "string", + "default": null + } + } + } + }, + "required": [] + }, + "form": [ + "stack", + "shadowSize", + { + "key": "fontColor", + "type": "color" + }, + "fontSize", + "decimals", + "units", + "tooltipIndividual", + { + "key": "grid", + "items": [ + { + "key": "grid.color", + "type": "color" + }, + { + "key": "grid.backgroundColor", + "type": "color" + }, + { + "key": "grid.tickColor", + "type": "color" + }, + "grid.outlineWidth", + "grid.verticalLines", + "grid.horizontalLines" + ] + }, + { + "key": "legend", + "items": [ + "legend.show", + { + "key": "legend.position", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "nw", + "label": "North-west" + }, + { + "value": "ne", + "label": "North-east" + }, + { + "value": "sw", + "label": "South-west" + }, + { + "value": "se", + "label": "Soth-east" + } + ] + }, + { + "key": "legend.labelBoxBorderColor", + "type": "color" + }, + { + "key": "legend.backgroundColor", + "type": "color" + }, + "legend.backgroundOpacity" + ] + }, + { + "key": "xaxis", + "items": [ + "xaxis.showLabels", + "xaxis.title", + "xaxis.titleAngle", + { + "key": "xaxis.color", + "type": "color" + } + ] + }, + { + "key": "yaxis", + "items": [ + "yaxis.showLabels", + "yaxis.title", + "yaxis.titleAngle", + { + "key": "yaxis.color", + "type": "color" + } + ] + } + + ] + } + } + + static get pieDatakeySettingsSchema() { + return {} + } + + static datakeySettingsSchema(defaultShowLines) { + return { + "schema": { + "type": "object", + "title": "DataKeySettings", + "properties": { + "showLines": { + "title": "Show lines", + "type": "boolean", + "default": defaultShowLines + }, + "fillLines": { + "title": "Fill lines", + "type": "boolean", + "default": false + }, + "showPoints": { + "title": "Show points", + "type": "boolean", + "default": false + } + }, + "required": ["showLines", "fillLines", "showPoints"] + }, + "form": [ + "showLines", + "fillLines", + "showPoints" + ] + } + } + + checkMouseEvents() { + if (this.ctx.isMobile || this.ctx.isEdit) { + this.disableMouseEvents(); + } else if (!this.ctx.isEdit) { + this.enableMouseEvents(); + } + } + + enableMouseEvents() { + this.ctx.$container.css('pointer-events',''); + this.ctx.$container.addClass('mouse-events'); + this.options.selection = { mode : 'x' }; + + var tbFlot = this; + + if (!this.flotHoverHandler) { + this.flotHoverHandler = function (event, pos, item) { + if (!tbFlot.ctx.tooltipIndividual || item) { + + var multipleModeTooltip = !tbFlot.ctx.tooltipIndividual; + + if (multipleModeTooltip) { + tbFlot.ctx.plot.unhighlight(); + } + + var pageX = pos.pageX; + var pageY = pos.pageY; + + var tooltipHtml; + + if (tbFlot.chartType === 'pie') { + tooltipHtml = tbFlot.ctx.tooltipFormatter(item); + } else { + var hoverInfo = tbFlot.getHoverInfo(tbFlot.ctx.plot.getData(), pos); + if (angular.isNumber(hoverInfo.time)) { + hoverInfo.seriesHover.sort(function (a, b) { + return b.value - a.value; + }); + tooltipHtml = tbFlot.ctx.tooltipFormatter(hoverInfo, item ? item.seriesIndex : -1); + } + } + + if (tooltipHtml) { + tbFlot.ctx.tooltip.html(tooltipHtml) + .css({top: pageY+5, left: 0}) + .fadeIn(200); + + var windowWidth = $( window ).width(); //eslint-disable-line + var tooltipWidth = tbFlot.ctx.tooltip.width(); + var left = pageX+5; + if (windowWidth - pageX < tooltipWidth + 50) { + left = pageX - tooltipWidth - 10; + } + tbFlot.ctx.tooltip.css({ + left: left + }); + + if (multipleModeTooltip) { + for (var i = 0; i < hoverInfo.seriesHover.length; i++) { + var seriesHoverInfo = hoverInfo.seriesHover[i]; + tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex); + } + } + } + + } else { + tbFlot.ctx.tooltip.stop(true); + tbFlot.ctx.tooltip.hide(); + tbFlot.ctx.plot.unhighlight(); + } + }; + this.ctx.$container.bind('plothover', this.flotHoverHandler); + } + + if (!this.flotSelectHandler) { + this.flotSelectHandler = function (event, ranges) { + tbFlot.ctx.plot.clearSelection(); + tbFlot.ctx.timewindowFunctions.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); + }; + this.ctx.$container.bind('plotselected', this.flotSelectHandler); + } + if (!this.dblclickHandler) { + this.dblclickHandler = function () { + tbFlot.ctx.timewindowFunctions.onResetTimewindow(); + }; + this.ctx.$container.bind('dblclick', this.dblclickHandler); + } + if (!this.mousedownHandler) { + this.mousedownHandler = function () { + tbFlot.isMouseInteraction = true; + }; + this.ctx.$container.bind('mousedown', this.mousedownHandler); + } + if (!this.mouseupHandler) { + this.mouseupHandler = function () { + tbFlot.isMouseInteraction = false; + }; + this.ctx.$container.bind('mouseup', this.mouseupHandler); + } + if (!this.mouseleaveHandler) { + this.mouseleaveHandler = function () { + tbFlot.ctx.tooltip.stop(true); + tbFlot.ctx.tooltip.hide(); + tbFlot.ctx.plot.unhighlight(); + tbFlot.isMouseInteraction = false; + }; + this.ctx.$container.bind('mouseleave', this.mouseleaveHandler); + } + } + + disableMouseEvents() { + this.ctx.$container.css('pointer-events','none'); + this.ctx.$container.removeClass('mouse-events'); + this.options.selection = { mode : null }; + + if (this.flotHoverHandler) { + this.ctx.$container.unbind('plothover', this.flotHoverHandler); + this.flotHoverHandler = null; + } + + if (this.flotSelectHandler) { + this.ctx.$container.unbind('plotselected', this.flotSelectHandler); + this.flotSelectHandler = null; + } + if (this.dblclickHandler) { + this.ctx.$container.unbind('dblclick', this.dblclickHandler); + this.dblclickHandler = null; + } + if (this.mousedownHandler) { + this.ctx.$container.unbind('mousedown', this.mousedownHandler); + this.mousedownHandler = null; + } + if (this.mouseupHandler) { + this.ctx.$container.unbind('mouseup', this.mouseupHandler); + this.mouseupHandler = null; + } + if (this.mouseleaveHandler) { + this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler); + this.mouseleaveHandler = null; + } + } + + + findHoverIndexFromData (posX, series) { + var lower = 0; + var upper = series.data.length - 1; + var middle; + var index = null; + while (index === null) { + if (lower > upper) { + return Math.max(upper, 0); + } + middle = Math.floor((lower + upper) / 2); + if (series.data[middle][0] === posX) { + return middle; + } else if (series.data[middle][0] < posX) { + lower = middle + 1; + } else { + upper = middle - 1; + } + } + } + + findHoverIndexFromDataPoints (posX, series, last) { + var ps = series.datapoints.pointsize; + var initial = last*ps; + var len = series.datapoints.points.length; + for (var j = initial; j < len; j += ps) { + if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) + || series.datapoints.points[j] > posX) { + return Math.max(j - ps, 0)/ps; + } + } + return j/ps - 1; + } + + + getHoverInfo (seriesList, pos) { + var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime; + var last_value = 0; + var results = { + seriesHover: [] + }; + for (i = 0; i < seriesList.length; i++) { + series = seriesList[i]; + hoverIndex = this.findHoverIndexFromData(pos.x, series); + if (series.data[hoverIndex] && series.data[hoverIndex][0]) { + hoverDistance = pos.x - series.data[hoverIndex][0]; + pointTime = series.data[hoverIndex][0]; + + if (!minDistance + || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) + || (hoverDistance < 0 && hoverDistance > minDistance)) { + minDistance = hoverDistance; + minTime = pointTime; + } + if (series.stack) { + if (this.ctx.tooltipIndividual) { + value = series.data[hoverIndex][1]; + } else { + last_value += series.data[hoverIndex][1]; + value = last_value; + } + } else { + value = series.data[hoverIndex][1]; + } + + if (series.stack) { + hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); + } + results.seriesHover.push({ + value: value, + hoverIndex: hoverIndex, + color: series.dataKey.color, + label: series.label, + time: pointTime, + distance: hoverDistance, + index: i + }); + } + } + results.time = minTime; + return results; + } + pieDataRendered() { for (var i in this.ctx.pieTargetData) { var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0; @@ -352,121 +1120,6 @@ export default class TbFlot { this.ctx.plot.setData(this.ctx.pieData); this.ctx.plot.draw(); } - - resize() { - this.ctx.plot.resize(); - if (this.chartType === 'line') { - this.ctx.plot.setupGrid(); - } - this.ctx.plot.draw(); - } - - checkMouseEvents() { - if (this.ctx.isMobile || this.ctx.isEdit) { - this.disableMouseEvents(); - } else if (!this.ctx.isEdit) { - this.enableMouseEvents(); - } - } - - enableMouseEvents() { - this.ctx.$container.css('pointer-events',''); - this.ctx.$container.addClass('mouse-events'); - this.options.selection = { mode : 'x' }; - - var tbFlot = this; - - if (!this.flotHoverHandler) { - this.flotHoverHandler = function (event, pos, item) { - if (item) { - var pageX = item.pageX || pos.pageX; - var pageY = item.pageY || pos.pageY; - tbFlot.ctx.tooltip.html(tbFlot.ctx.tooltipFormatter(item)) - .css({top: pageY+5, left: 0}) - .fadeIn(200); - var windowWidth = $( window ).width(); //eslint-disable-line - var tooltipWidth = tbFlot.ctx.tooltip.width(); - var left = pageX+5; - if (windowWidth - pageX < tooltipWidth + 50) { - left = pageX - tooltipWidth - 10; - } - tbFlot.ctx.tooltip.css({ - left: left - }); - } else { - tbFlot.ctx.tooltip.stop(true); - tbFlot.ctx.tooltip.hide(); - } - }; - this.ctx.$container.bind('plothover', this.flotHoverHandler); - } - - if (!this.flotSelectHandler) { - this.flotSelectHandler = function (event, ranges) { - tbFlot.ctx.plot.clearSelection(); - tbFlot.ctx.timewindowFunctions.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); - }; - this.ctx.$container.bind('plotselected', this.flotSelectHandler); - } - if (!this.dblclickHandler) { - this.dblclickHandler = function () { - tbFlot.ctx.timewindowFunctions.onResetTimewindow(); - }; - this.ctx.$container.bind('dblclick', this.dblclickHandler); - } - if (!this.mousedownHandler) { - this.mousedownHandler = function () { - tbFlot.isMouseInteraction = true; - }; - this.ctx.$container.bind('mousedown', this.mousedownHandler); - } - if (!this.mouseupHandler) { - this.mouseupHandler = function () { - tbFlot.isMouseInteraction = false; - }; - this.ctx.$container.bind('mouseup', this.mouseupHandler); - } - if (!this.mouseleaveHandler) { - this.mouseleaveHandler = function () { - tbFlot.ctx.tooltip.stop(true); - tbFlot.ctx.tooltip.hide(); - tbFlot.isMouseInteraction = false; - }; - this.ctx.$container.bind('mouseleave', this.mouseleaveHandler); - } - } - - disableMouseEvents() { - this.ctx.$container.css('pointer-events','none'); - this.ctx.$container.removeClass('mouse-events'); - this.options.selection = { mode : null }; - - if (this.flotHoverHandler) { - this.ctx.$container.unbind('plothover', this.flotHoverHandler); - this.flotHoverHandler = null; - } - - if (this.flotSelectHandler) { - this.ctx.$container.unbind('plotselected', this.flotSelectHandler); - this.flotSelectHandler = null; - } - if (this.dblclickHandler) { - this.ctx.$container.unbind('dblclick', this.dblclickHandler); - this.dblclickHandler = null; - } - if (this.mousedownHandler) { - this.ctx.$container.unbind('mousedown', this.mousedownHandler); - this.mousedownHandler = null; - } - if (this.mouseupHandler) { - this.ctx.$container.unbind('mouseup', this.mouseupHandler); - this.mouseupHandler = null; - } - if (this.mouseleaveHandler) { - this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler); - this.mouseleaveHandler = null; - } - } } /* eslint-enable angular/angularelement */ \ No newline at end of file diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js index f7047e4e79..d07233ab96 100644 --- a/ui/src/app/widget/widget-library.controller.js +++ b/ui/src/app/widget/widget-library.controller.js @@ -54,7 +54,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( function (widgetTypes) { - widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','name']); + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']); var top = 0; var lastTop = [0, 0, 0];