thingsboard-aplcache
Changes
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java 1(+1 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 1(+1 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 3(+2 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 4(+2 -2)
ui/src/app/api/data-aggregator.js 57(+42 -15)
ui/src/app/api/datasource.service.js 91(+41 -50)
ui/src/app/api/time.service.js 330(+330 -0)
ui/src/app/api/widget.service.js 4(+2 -2)
ui/src/app/app.js 2(+2 -0)
ui/src/app/components/timeinterval.directive.js 131(+74 -57)
ui/src/app/components/timeinterval.scss 10(+7 -3)
ui/src/app/components/timeinterval.tpl.html 66(+37 -29)
ui/src/app/components/timewindow.directive.js 50(+14 -36)
ui/src/app/components/timewindow.scss 27(+26 -1)
ui/src/app/components/timewindow-panel.tpl.html 115(+62 -53)
ui/src/app/components/widget.controller.js 87(+23 -64)
ui/src/app/locale/locale.constant.js 4(+3 -1)
ui/src/app/widget/lib/flot-widget.js 53(+47 -6)
ui/src/scss/main.scss 8(+8 -0)
Details
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
index ed48206..e95496b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
@@ -23,19 +23,21 @@ public class BaseTsKvQuery implements TsKvQuery {
private final String key;
private final long startTs;
private final long endTs;
+ private final long interval;
private final int limit;
private final Aggregation aggregation;
- public BaseTsKvQuery(String key, long startTs, long endTs, int limit, Aggregation aggregation) {
+ public BaseTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation) {
this.key = key;
this.startTs = startTs;
this.endTs = endTs;
+ this.interval = interval;
this.limit = limit;
this.aggregation = aggregation;
}
public BaseTsKvQuery(String key, long startTs, long endTs) {
- this(key, startTs, endTs, 1, Aggregation.AVG);
+ this(key, startTs, endTs, endTs-startTs, 1, Aggregation.AVG);
}
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
index 10a13ce..8d60f52 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
@@ -25,6 +25,8 @@ public interface TsKvQuery {
long getEndTs();
+ long getInterval();
+
int getLimit();
Aggregation getAggregation();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
index 10651cb..c7584fe 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
@@ -112,13 +112,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityType, entityId, query);
} else {
- long step = Math.max((query.getEndTs() - query.getStartTs()) / query.getLimit(), minAggregationStepMs);
+ long step = Math.max(query.getInterval(), minAggregationStepMs);
long stepTs = query.getStartTs();
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
while (stepTs < query.getEndTs()) {
long startTs = stepTs;
long endTs = stepTs + step;
- TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, 1, query.getAggregation());
+ TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation());
futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
stepTs = endTs;
}
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index e7ccc56..cb274a5 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie',
-'{"type":"latest","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: ''Roboto'';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''pie''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n}\n","settingsSchema":"{}\n","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"}',
+'{"type":"latest","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: ''Roboto'';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''pie''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}\n","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"}',
'Pie - Flot' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'timeseries_bars_flot',
-'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''bar''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":true,\"tooltipIndividual\":false},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
+'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''bar''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":true,\"tooltipIndividual\":false},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
'Timeseries Bars - Flot' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
-'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
+'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
'Timeseries - Flot' );
/** System plugins and rules **/
diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
index fd16b75..134f471 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
@@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
entries.add(save(deviceId, 55000, 600));
List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
- 60000, 3, Aggregation.NONE))).get();
+ 60000, 20000, 3, Aggregation.NONE))).get();
assertEquals(3, list.size());
assertEquals(55000, list.get(0).getTs());
assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue());
@@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
- 60000, 3, Aggregation.AVG))).get();
+ 60000, 20000, 3, Aggregation.AVG))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue());
@@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
- 60000, 3, Aggregation.SUM))).get();
+ 60000, 20000, 3, Aggregation.SUM))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
@@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
- 60000, 3, Aggregation.MIN))).get();
+ 60000, 20000, 3, Aggregation.MIN))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
@@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
- 60000, 3, Aggregation.MAX))).get();
+ 60000, 20000, 3, Aggregation.MAX))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
@@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
- 60000, 3, Aggregation.COUNT))).get();
+ 60000, 20000, 3, Aggregation.COUNT))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
index 9f06895..145f8c4 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
@@ -32,6 +32,7 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
private String keys;
private long startTs;
private long endTs;
+ private long interval;
private int limit;
private String agg;
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 20bd3e2..9f3f7ec 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
@@ -30,6 +30,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long startTs;
private long timeWindow;
+ private long interval;
private int limit;
private String agg;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index 78fa4ad..fb50484 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
String keysStr = request.getParameter("keys");
Optional<Long> startTs = request.getLongParamValue("startTs");
Optional<Long> endTs = request.getLongParamValue("endTs");
+ Optional<Long> interval = request.getLongParamValue("interval");
Optional<Integer> limit = request.getIntParamValue("limit");
Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
List<String> keys = Arrays.asList(keysStr.split(","));
- List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)).collect(Collectors.toList());
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)).collect(Collectors.toList());
ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
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 51181fd..f018aaa 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
@@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
startTs = cmd.getStartTs();
long endTs = cmd.getStartTs() + cmd.getTimeWindow();
- List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
} else {
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
@@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
}
DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
- List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
ui/src/app/api/data-aggregator.js 57(+42 -15)
diff --git a/ui/src/app/api/data-aggregator.js b/ui/src/app/api/data-aggregator.js
index e273a9d..31baff1 100644
--- a/ui/src/app/api/data-aggregator.js
+++ b/ui/src/app/api/data-aggregator.js
@@ -25,11 +25,12 @@ export default class DataAggregator {
this.$timeout = $timeout;
this.$filter = $filter;
this.dataReceived = false;
+ this.resetPending = false;
this.noAggregation = aggregationType === types.aggregation.none.value;
this.limit = limit;
this.timeWindow = timeWindow;
this.interval = interval;
- this.aggregationTimeout = this.interval;
+ this.aggregationTimeout = Math.max(this.interval, 1000);
switch (aggregationType) {
case types.aggregation.min.value:
this.aggFunction = min;
@@ -54,11 +55,37 @@ export default class DataAggregator {
}
}
+ reset(startTs, timeWindow, interval) {
+ if (this.intervalTimeoutHandle) {
+ this.$timeout.cancel(this.intervalTimeoutHandle);
+ this.intervalTimeoutHandle = null;
+ }
+ this.intervalScheduledTime = currentTime();
+ this.startTs = startTs;
+ this.timeWindow = timeWindow;
+ this.interval = interval;
+ this.endTs = this.startTs + this.timeWindow;
+ this.elapsed = 0;
+ this.aggregationTimeout = Math.max(this.interval, 1000);
+ this.resetPending = true;
+ var self = this;
+ this.intervalTimeoutHandle = this.$timeout(function() {
+ self.onInterval();
+ }, this.aggregationTimeout, false);
+ }
+
onData(data, update, history) {
- if (!this.dataReceived) {
- this.elapsed = 0;
- this.dataReceived = true;
- this.endTs = this.startTs + this.timeWindow;
+ if (!this.dataReceived || this.resetPending) {
+ var updateIntervalScheduledTime = true;
+ if (!this.dataReceived) {
+ this.elapsed = 0;
+ this.dataReceived = true;
+ this.endTs = this.startTs + this.timeWindow;
+ }
+ if (this.resetPending) {
+ this.resetPending = false;
+ updateIntervalScheduledTime = false;
+ }
if (update) {
this.aggregationMap = {};
updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
@@ -66,19 +93,24 @@ export default class DataAggregator {
} else {
this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
}
- this.onInterval(currentTime(), history);
+ if (updateIntervalScheduledTime) {
+ this.intervalScheduledTime = currentTime();
+ }
+ this.onInterval(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);
+ this.intervalScheduledTime = currentTime();
+ this.onInterval(history);
}
}
}
- onInterval(startedTime, history) {
+ onInterval(history) {
var now = currentTime();
- this.elapsed += now - startedTime;
+ this.elapsed += now - this.intervalScheduledTime;
+ this.intervalScheduledTime = now;
if (this.intervalTimeoutHandle) {
this.$timeout.cancel(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
@@ -101,16 +133,11 @@ export default class DataAggregator {
var self = this;
if (!history) {
this.intervalTimeoutHandle = this.$timeout(function() {
- self.onInterval(now);
+ self.onInterval();
}, this.aggregationTimeout, false);
}
}
- reset() {
- this.destroy();
- this.dataReceived = false;
- }
-
destroy() {
if (this.intervalTimeoutHandle) {
this.$timeout.cancel(this.intervalTimeoutHandle);
ui/src/app/api/datasource.service.js 91(+41 -50)
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index b44f85d..7d2c1be 100644
--- a/ui/src/app/api/datasource.service.js
+++ b/ui/src/app/api/datasource.service.js
@@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
keys: tsKeys,
startTs: subsTw.fixedWindow.startTimeMs,
endTs: subsTw.fixedWindow.endTimeMs,
+ interval: subsTw.aggregation.interval,
limit: subsTw.aggregation.limit,
agg: subsTw.aggregation.type
};
@@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
onData(data.data, types.dataKeyType.timeseries);
}
},
- onReconnected: function() {
- onReconnected();
- }
+ onReconnected: function() {}
};
telemetryWebsocketService.subscribe(subscriber);
@@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
};
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
- 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);
- },
- tsKeyNames,
- subsTw.startTs,
- subsTw.aggregation.limit,
- subsTw.aggregation.type,
- subsTw.aggregation.timeWindow,
- subsTw.aggregation.interval,
- types,
- $timeout,
- $filter
- );
+ updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
+ dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames);
subscriber.onData = function(data) {
dataAggregator.onData(data);
}
subscriber.onReconnected = function() {
- dataAggregator.reset();
- onReconnected();
+ var newSubsTw = null;
+ for (var i2 in listeners) {
+ var listener = listeners[i2];
+ if (!newSubsTw) {
+ newSubsTw = listener.updateRealtimeSubscription();
+ } else {
+ listener.setRealtimeSubscription(newSubsTw);
+ }
+ }
+ updateRealtimeSubscriptionCommand(this.subscriptionCommand, newSubsTw);
+ dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
}
} else {
- subscriber.onReconnected = function() {
- onReconnected();
- }
+ subscriber.onReconnected = function() {}
subscriber.onData = function(data) {
if (data.data) {
onData(data.data, types.dataKeyType.timeseries);
@@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
onData(data.data, types.dataKeyType.attribute);
}
},
- onReconnected: function() {
- onReconnected();
- }
+ onReconnected: function() {}
};
telemetryWebsocketService.subscribe(subscriber);
@@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
timer = $timeout(onTick, 0, false);
}
}
+ }
+
+ function createRealtimeDataAggregator(subsTw, tsKeyNames) {
+ return new DataAggregator(
+ function(data, startTs, endTs) {
+ onData(data, types.dataKeyType.timeseries, startTs, endTs);
+ },
+ tsKeyNames,
+ subsTw.startTs,
+ subsTw.aggregation.limit,
+ subsTw.aggregation.type,
+ subsTw.aggregation.timeWindow,
+ subsTw.aggregation.interval,
+ types,
+ $timeout,
+ $filter
+ );
+ }
+ function updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw) {
+ subscriptionCommand.startTs = subsTw.startTs;
+ subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow;
+ subscriptionCommand.interval = subsTw.aggregation.interval;
+ subscriptionCommand.limit = subsTw.aggregation.limit;
+ subscriptionCommand.agg = subsTw.aggregation.type;
}
function unsubscribe() {
@@ -495,27 +507,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
}
}
- function onReconnected() {
- if (datasourceType === types.datasourceType.device) {
- for (var key in dataKeys) {
- var dataKeysList = dataKeys[key];
- for (var i = 0; i < dataKeysList.length; i++) {
- var dataKey = dataKeysList[i];
- var datasourceKey = key + '_' + i;
- datasourceData[datasourceKey] = {
- data: []
- };
- for (var l in listeners) {
- var listener = listeners[l];
- listener.dataUpdated(datasourceData[datasourceKey],
- listener.datasourceIndex,
- dataKey.index);
- }
- }
- }
- }
- }
-
function isNumeric(val) {
return (val - parseFloat( val ) + 1) >= 0;
}
ui/src/app/api/time.service.js 330(+330 -0)
diff --git a/ui/src/app/api/time.service.js b/ui/src/app/api/time.service.js
new file mode 100644
index 0000000..a4bb571
--- /dev/null
+++ b/ui/src/app/api/time.service.js
@@ -0,0 +1,330 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.time', [])
+ .factory('timeService', TimeService)
+ .name;
+
+const SECOND = 1000;
+const MINUTE = 60 * SECOND;
+const HOUR = 60 * MINUTE;
+const DAY = 24 * HOUR;
+
+const MIN_INTERVAL = SECOND;
+const MAX_INTERVAL = 365 * 20 * DAY;
+
+const MIN_LIMIT = 10;
+const AVG_LIMIT = 200;
+const MAX_LIMIT = 500;
+
+/*@ngInject*/
+function TimeService($translate, types) {
+
+ var predefIntervals = [
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'),
+ value: 1 * SECOND
+ },
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'),
+ value: 5 * SECOND
+ },
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
+ value: 10 * SECOND
+ },
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'),
+ value: 15 * SECOND
+ },
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
+ value: 30 * SECOND
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
+ value: 1 * MINUTE
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
+ value: 2 * MINUTE
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
+ value: 5 * MINUTE
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
+ value: 10 * MINUTE
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'),
+ value: 15 * MINUTE
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
+ value: 30 * MINUTE
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
+ value: 1 * HOUR
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
+ value: 2 * HOUR
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'),
+ value: 5 * HOUR
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
+ value: 10 * HOUR
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'),
+ value: 12 * HOUR
+ },
+ {
+ name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
+ value: 1 * DAY
+ },
+ {
+ name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
+ value: 7 * DAY
+ },
+ {
+ name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
+ value: 30 * DAY
+ }
+ ];
+
+ var service = {
+ minIntervalLimit: minIntervalLimit,
+ maxIntervalLimit: maxIntervalLimit,
+ boundMinInterval: boundMinInterval,
+ boundMaxInterval: boundMaxInterval,
+ getIntervals: getIntervals,
+ matchesExistingInterval: matchesExistingInterval,
+ boundToPredefinedInterval: boundToPredefinedInterval,
+ defaultTimewindow: defaultTimewindow,
+ toHistoryTimewindow: toHistoryTimewindow,
+ createSubscriptionTimewindow: createSubscriptionTimewindow,
+ avgAggregationLimit: function () {
+ return AVG_LIMIT;
+ }
+ }
+
+ return service;
+
+ function minIntervalLimit(timewindow) {
+ var min = timewindow / MAX_LIMIT;
+ return boundMinInterval(min);
+ }
+
+ function avgInterval(timewindow) {
+ var avg = timewindow / AVG_LIMIT;
+ return boundMinInterval(avg);
+ }
+
+ function maxIntervalLimit(timewindow) {
+ var max = timewindow / MIN_LIMIT;
+ return boundMaxInterval(max);
+ }
+
+ function boundMinInterval(min) {
+ return toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
+ }
+
+ function boundMaxInterval(max) {
+ return toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL);
+ }
+
+ function toBound(value, min, max, defValue) {
+ if (angular.isDefined(value)) {
+ value = Math.max(value, min);
+ value = Math.min(value, max);
+ return value;
+ } else {
+ return defValue;
+ }
+ }
+
+ function getIntervals(min, max) {
+ min = boundMinInterval(min);
+ max = boundMaxInterval(max);
+ var intervals = [];
+ for (var i in predefIntervals) {
+ var interval = predefIntervals[i];
+ if (interval.value >= min && interval.value <= max) {
+ intervals.push(interval);
+ }
+ }
+ return intervals;
+ }
+
+ function matchesExistingInterval(min, max, intervalMs) {
+ var intervals = getIntervals(min, max);
+ for (var i in intervals) {
+ var interval = intervals[i];
+ if (intervalMs === interval.value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function boundToPredefinedInterval(min, max, intervalMs) {
+ var intervals = getIntervals(min, max);
+ var minDelta = MAX_INTERVAL;
+ var boundedInterval = intervalMs || min;
+ var matchedInterval;
+ for (var i in intervals) {
+ var interval = intervals[i];
+ var delta = Math.abs(interval.value - boundedInterval);
+ if (delta < minDelta) {
+ matchedInterval = interval;
+ minDelta = delta;
+ }
+ }
+ boundedInterval = matchedInterval.value;
+ return boundedInterval;
+ }
+
+ function defaultTimewindow() {
+ var currentTime = (new Date).getTime();
+ var timewindow = {
+ displayValue: "",
+ selectedTab: 0,
+ realtime: {
+ interval: SECOND,
+ timewindowMs: MINUTE // 1 min by default
+ },
+ history: {
+ historyType: 0,
+ interval: SECOND,
+ timewindowMs: MINUTE, // 1 min by default
+ fixedTimewindow: {
+ startTimeMs: currentTime - DAY, // 1 day by default
+ endTimeMs: currentTime
+ }
+ },
+ aggregation: {
+ type: types.aggregation.avg.value,
+ limit: AVG_LIMIT
+ }
+ }
+ return timewindow;
+ }
+
+ function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs) {
+
+ var interval = 0;
+ if (timewindow.history) {
+ interval = timewindow.history.interval;
+ } else if (timewindow.realtime) {
+ interval = timewindow.realtime.interval;
+ }
+
+ var historyTimewindow = {
+ history: {
+ fixedTimewindow: {
+ startTimeMs: startTimeMs,
+ endTimeMs: endTimeMs
+ },
+ interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval)
+ },
+ aggregation: {
+
+ }
+ }
+ if (timewindow.aggregation) {
+ historyTimewindow.aggregation.type = timewindow.aggregation.type || types.aggregation.avg.value;
+ } else {
+ historyTimewindow.aggregation.type = types.aggregation.avg.value;
+ }
+
+ return historyTimewindow;
+ }
+
+ function createSubscriptionTimewindow(timewindow, stDiff) {
+
+ var subscriptionTimewindow = {
+ fixedWindow: null,
+ realtimeWindowMs: null,
+ aggregation: {
+ interval: SECOND,
+ limit: AVG_LIMIT,
+ type: types.aggregation.avg.value
+ }
+ };
+ var aggTimewindow = 0;
+
+ if (angular.isDefined(timewindow.aggregation)) {
+ subscriptionTimewindow.aggregation = {
+ type: timewindow.aggregation.type || types.aggregation.avg.value,
+ limit: timewindow.aggregation.limit || AVG_LIMIT
+ };
+ }
+ if (angular.isDefined(timewindow.realtime)) {
+ subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
+ subscriptionTimewindow.aggregation.interval =
+ boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval);
+ subscriptionTimewindow.startTs = (new Date).getTime() + stDiff - subscriptionTimewindow.realtimeWindowMs;
+ var startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
+ aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
+ if (startDiff) {
+ subscriptionTimewindow.startTs -= startDiff;
+ aggTimewindow += subscriptionTimewindow.aggregation.interval;
+ }
+ } else if (angular.isDefined(timewindow.history)) {
+ if (angular.isDefined(timewindow.history.timewindowMs)) {
+ var currentTime = (new Date).getTime();
+ subscriptionTimewindow.fixedWindow = {
+ startTimeMs: currentTime - timewindow.history.timewindowMs,
+ endTimeMs: currentTime
+ }
+ aggTimewindow = timewindow.history.timewindowMs;
+
+ } else {
+ subscriptionTimewindow.fixedWindow = {
+ startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
+ endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
+ }
+ aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
+ }
+ subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
+ subscriptionTimewindow.aggregation.interval = boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval);
+ }
+ var aggregation = subscriptionTimewindow.aggregation;
+ aggregation.timeWindow = aggTimewindow;
+ if (aggregation.type !== types.aggregation.none.value) {
+ aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval);
+ }
+ return subscriptionTimewindow;
+ }
+
+ function boundIntervalToTimewindow(timewindow, intervalMs) {
+ var min = minIntervalLimit(timewindow);
+ var max = maxIntervalLimit(timewindow);
+ if (intervalMs) {
+ return toBound(intervalMs, min, max, intervalMs);
+ } else {
+ return boundToPredefinedInterval(min, max, avgInterval(timewindow));
+ }
+ }
+
+
+}
\ No newline at end of file
ui/src/app/api/widget.service.js 4(+2 -2)
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 3d750e3..8d6359a 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
resources: [],
templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>',
templateCss: '',
- controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};',
+ controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n',
defaultConfig: '{\n' +
@@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
resources: [],
templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
templateCss: '',
- controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};',
+ controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n',
defaultConfig: '{\n' +
ui/src/app/app.js 2(+2 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 3acb1c0..5e09b38 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service';
import thingsboardRaf from './common/raf.provider';
import thingsboardUtils from './common/utils.service';
import thingsboardTypes from './common/types.constant';
+import thingsboardApiTime from './api/time.service';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast';
@@ -101,6 +102,7 @@ angular.module('thingsboard', [
thingsboardRaf,
thingsboardUtils,
thingsboardTypes,
+ thingsboardApiTime,
thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,
ui/src/app/components/timeinterval.directive.js 131(+74 -57)
diff --git a/ui/src/app/components/timeinterval.directive.js b/ui/src/app/components/timeinterval.directive.js
index 47251b0..eaaa4a1 100644
--- a/ui/src/app/components/timeinterval.directive.js
+++ b/ui/src/app/components/timeinterval.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', [])
.name;
/*@ngInject*/
-function Timeinterval($compile, $templateCache, $translate) {
+function Timeinterval($compile, $templateCache, timeService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
@@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) {
scope.mins = 1;
scope.secs = 0;
- scope.predefIntervals = [
- {
- name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
- value: 10 * 1000
- },
- {
- name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
- value: 30 * 1000
- },
- {
- name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
- value: 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
- value: 2 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
- value: 5 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
- value: 10 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
- value: 30 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
- value: 60 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
- value: 2 * 60 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
- value: 10 * 60 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
- value: 24 * 60 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
- value: 7 * 24 * 60 * 60 * 1000
- },
- {
- name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
- value: 30 * 24 * 60 * 60 * 1000
- }
- ];
+ scope.advanced = false;
+
+ scope.boundInterval = function() {
+ var min = timeService.boundMinInterval(scope.min);
+ var max = timeService.boundMaxInterval(scope.max);
+ scope.intervals = timeService.getIntervals(scope.min, scope.max);
+ if (scope.rendered) {
+ var newIntervalMs = ngModelCtrl.$viewValue;
+ if (newIntervalMs < min) {
+ newIntervalMs = min;
+ } else if (newIntervalMs > max) {
+ newIntervalMs = max;
+ }
+ if (!scope.advanced) {
+ newIntervalMs = timeService.boundToPredefinedInterval(min, max, newIntervalMs);
+ }
+ if (newIntervalMs !== ngModelCtrl.$viewValue) {
+ scope.setIntervalMs(newIntervalMs);
+ scope.updateView();
+ }
+ }
+ }
scope.setIntervalMs = function (intervalMs) {
+ if (!scope.advanced) {
+ scope.intervalMs = intervalMs;
+ }
var intervalSeconds = Math.floor(intervalMs / 1000);
scope.days = Math.floor(intervalSeconds / 86400);
scope.hours = Math.floor((intervalSeconds % 86400) / 3600);
@@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) {
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var intervalMs = ngModelCtrl.$viewValue;
+ if (!scope.rendered) {
+ scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs);
+ }
scope.setIntervalMs(intervalMs);
}
scope.rendered = true;
@@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) {
return;
}
var value = null;
- var intervalMs = (scope.days * 86400 +
+ var intervalMs;
+ if (!scope.advanced) {
+ intervalMs = scope.intervalMs;
+ } else {
+ intervalMs = (scope.days * 86400 +
scope.hours * 3600 +
scope.mins * 60 +
scope.secs) * 1000;
+ }
if (!isNaN(intervalMs) && intervalMs > 0) {
value = intervalMs;
ngModelCtrl.$setValidity('tb-timeinterval', true);
@@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) {
ngModelCtrl.$setValidity('tb-timeinterval', !scope.required);
}
ngModelCtrl.$setViewValue(value);
+ scope.boundInterval();
}
scope.$watch('required', function (newRequired, prevRequired) {
@@ -134,6 +114,38 @@ function Timeinterval($compile, $templateCache, $translate) {
}
});
+ scope.$watch('min', function (newMin, prevMin) {
+ if (angular.isDefined(newMin) && newMin !== prevMin) {
+ scope.updateView();
+ }
+ });
+
+ scope.$watch('max', function (newMax, prevMax) {
+ if (angular.isDefined(newMax) && newMax !== prevMax) {
+ scope.updateView();
+ }
+ });
+
+ scope.$watch('intervalMs', function (newIntervalMs, prevIntervalMs) {
+ if (angular.isDefined(newIntervalMs) && newIntervalMs !== prevIntervalMs) {
+ scope.updateView();
+ }
+ });
+
+ scope.$watch('advanced', function (newAdvanced, prevAdvanced) {
+ if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) {
+ if (!scope.advanced) {
+ scope.intervalMs = (scope.days * 86400 +
+ scope.hours * 3600 +
+ scope.mins * 60 +
+ scope.secs) * 1000;
+ } else {
+ scope.setIntervalMs(scope.intervalMs);
+ }
+ scope.updateView();
+ }
+ });
+
scope.$watch('secs', function (newSecs) {
if (angular.isUndefined(newSecs)) {
return;
@@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) {
scope.updateView();
});
+ scope.boundInterval();
+
$compile(element.contents())(scope);
}
@@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) {
restrict: "E",
require: "^ngModel",
scope: {
- required: '=ngRequired'
+ required: '=ngRequired',
+ min: '=?',
+ max: '=?',
+ predefinedName: '=?'
},
link: linker
};
ui/src/app/components/timeinterval.scss 10(+7 -3)
diff --git a/ui/src/app/components/timeinterval.scss b/ui/src/app/components/timeinterval.scss
index 2d7af71..525bfd1 100644
--- a/ui/src/app/components/timeinterval.scss
+++ b/ui/src/app/components/timeinterval.scss
@@ -14,6 +14,7 @@
* limitations under the License.
*/
tb-timeinterval {
+ min-width: 355px;
md-input-container {
margin-bottom: 0px;
.md-errors-spacer {
@@ -25,10 +26,13 @@ tb-timeinterval {
width: 150px;
}
}
-}
-
-tb-timeinterval {
.md-input {
width: 70px !important;
}
+ .advanced-switch {
+ margin-top: 0;
+ }
+ .advanced-label {
+ margin: 5px 0;
+ }
}
ui/src/app/components/timeinterval.tpl.html 66(+37 -29)
diff --git a/ui/src/app/components/timeinterval.tpl.html b/ui/src/app/components/timeinterval.tpl.html
index 75ecd30..e6719f2 100644
--- a/ui/src/app/components/timeinterval.tpl.html
+++ b/ui/src/app/components/timeinterval.tpl.html
@@ -15,33 +15,41 @@
limitations under the License.
-->
-<section layout="row" layout-align="start start">
- <md-input-container>
- <label translate>timeinterval.days</label>
- <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
- </md-input-container>
- <md-input-container>
- <label translate>timeinterval.hours</label>
- <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
- </md-input-container>
- <md-input-container>
- <label translate>timeinterval.minutes</label>
- <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
- </md-input-container>
- <md-input-container>
- <label translate>timeinterval.seconds</label>
- <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
- </md-input-container>
- <md-menu md-position-mode="target-right target">
- <md-button class="md-icon-button" aria-label="Open intervals" ng-click="$mdOpenMenu($event)">
- <md-icon md-menu-origin aria-label="arrow_drop_down" class="material-icons">arrow_drop_down</md-icon>
- </md-button>
- <md-menu-content width="4">
- <md-menu-item ng-repeat="interval in predefIntervals" >
- <md-button ng-click="setIntervalMs(interval.value)">
- <span>{{interval.name}}</span>
- </md-button>
- </md-menu-item>
- </md-menu-content>
- </md-menu>
+<section layout="row">
+ <section layout="column" flex ng-show="advanced">
+ <label class="tb-small" translate>{{ predefinedName }}</label>
+ <section layout="row" layout-align="start start" flex>
+ <md-input-container>
+ <label translate>timeinterval.days</label>
+ <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <label translate>timeinterval.hours</label>
+ <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <label translate>timeinterval.minutes</label>
+ <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <label translate>timeinterval.seconds</label>
+ <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
+ </md-input-container>
+ </section>
+ </section>
+ <section layout="row" flex ng-show="!advanced">
+ <md-input-container flex>
+ <label translate>{{ predefinedName }}</label>
+ <md-select ng-model="intervalMs" style="min-width: 150px;" aria-label="predefined-interval">
+ <md-option ng-repeat="interval in intervals" ng-value="interval.value">
+ {{interval.name}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ </section>
+ <section layout="column" layout-align="center center">
+ <label class="tb-small advanced-label" translate>timeinterval.advanced</label>
+ <md-switch class="advanced-switch" ng-model="advanced" aria-label="predefined-switcher">
+ </md-switch>
+ </section>
</section>
ui/src/app/components/timewindow.directive.js 50(+14 -36)
diff --git a/ui/src/app/components/timewindow.directive.js b/ui/src/app/components/timewindow.directive.js
index f06c119..14fff62 100644
--- a/ui/src/app/components/timewindow.directive.js
+++ b/ui/src/app/components/timewindow.directive.js
@@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT
/* eslint-disable angular/angularelement */
/*@ngInject*/
-function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, types) {
+function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, timeService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
/* tbTimewindow (ng-model)
* {
* realtime: {
+ * interval: 0,
* timewindowMs: 0
* },
* history: {
+ * interval: 0,
* timewindowMs: 0,
* fixedTimewindow: {
* startTimeMs: 0,
@@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
* }
* },
* aggregation: {
- * limit: 200,
- * type: types.aggregation.avg.value
+ * type: types.aggregation.avg.value,
+ * limit: 200
* }
* }
*/
@@ -81,16 +83,6 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
}
element.html(template);
- scope.isHovered = false;
-
- scope.onHoverIn = function () {
- scope.isHovered = true;
- }
-
- scope.onHoverOut = function () {
- scope.isHovered = false;
- }
-
scope.openEditMode = function (event) {
var position;
var isGtSm = $mdMedia('gt-sm');
@@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
var model = scope.model;
if (model.selectedTab === 0) {
value.realtime = {
+ interval: model.realtime.interval,
timewindowMs: model.realtime.timewindowMs
};
} else {
if (model.history.historyType === 0) {
value.history = {
+ interval: model.history.interval,
timewindowMs: model.history.timewindowMs
};
} else {
value.history = {
+ interval: model.history.interval,
fixedTimewindow: {
startTimeMs: model.history.fixedTimewindow.startTimeMs,
endTimeMs: model.history.fixedTimewindow.endTimeMs
@@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
}
}
value.aggregation = {
- limit: model.aggregation.limit,
- type: model.aggregation.type
+ type: model.aggregation.type,
+ limit: model.aggregation.limit
};
ngModelCtrl.$setViewValue(value);
scope.updateDisplayValue();
@@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
}
ngModelCtrl.$render = function () {
- var currentTime = (new Date).getTime();
- scope.model = {
- displayValue: "",
- selectedTab: 0,
- realtime: {
- timewindowMs: 60000 // 1 min by default
- },
- history: {
- historyType: 0,
- timewindowMs: 60000, // 1 min by default
- fixedTimewindow: {
- startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default
- endTimeMs: currentTime
- }
- },
- aggregation: {
- limit: 200,
- type: types.aggregation.avg.value
- }
- };
+ scope.model = timeService.defaultTimewindow();
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var model = scope.model;
if (angular.isDefined(value.realtime)) {
model.selectedTab = 0;
+ model.realtime.interval = value.realtime.interval;
model.realtime.timewindowMs = value.realtime.timewindowMs;
} else {
model.selectedTab = 1;
+ model.history.interval = value.history.interval;
if (angular.isDefined(value.history.timewindowMs)) {
model.history.historyType = 0;
model.history.timewindowMs = value.history.timewindowMs;
@@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
}
}
if (angular.isDefined(value.aggregation)) {
- model.aggregation.limit = value.aggregation.limit || 200;
if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) {
model.aggregation.type = value.aggregation.type;
}
+ model.aggregation.limit = value.aggregation.limit || timeService.avgAggregationLimit();
}
}
scope.updateDisplayValue();
ui/src/app/components/timewindow.scss 27(+26 -1)
diff --git a/ui/src/app/components/timewindow.scss b/ui/src/app/components/timewindow.scss
index 16c89e8..0c85d52 100644
--- a/ui/src/app/components/timewindow.scss
+++ b/ui/src/app/components/timewindow.scss
@@ -21,14 +21,39 @@
}
.tb-timewindow-panel {
- min-height: 375px;
+ max-height: 440px;
+ min-width: 417px;
background: white;
border-radius: 4px;
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
overflow: hidden;
+ form, fieldset {
+ height: 100%;
+ }
md-content {
background-color: #fff;
+ overflow: hidden;
+ }
+ .md-padding {
+ padding: 0 16px;
+ }
+ .md-radio-interactive {
+ md-select, md-switch {
+ pointer-events: all;
+ }
+ }
+ md-radio-button {
+ .md-label {
+ width: 100%;
+ }
+ tb-timeinterval {
+ width: 355px;
+ .advanced-switch {
+ min-height: 30px;
+ max-width: 44px;
+ }
+ }
}
}
diff --git a/ui/src/app/components/timewindow.tpl.html b/ui/src/app/components/timewindow.tpl.html
index e428365..0de81e2 100644
--- a/ui/src/app/components/timewindow.tpl.html
+++ b/ui/src/app/components/timewindow.tpl.html
@@ -15,9 +15,9 @@
limitations under the License.
-->
-<section ng-mouseover="onHoverIn()" ng-mouseleave="onHoverOut()" layout='row' layout-align="start center" style="min-height: 32px;">
+<section layout='row' layout-align="start center" style="min-height: 32px;">
<span ng-click="openEditMode($event)">{{model.displayValue}}</span>
- <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-show="isHovered" ng-click="openEditMode($event)">
+ <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
<md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon>
</md-button>
</section>
\ No newline at end of file
diff --git a/ui/src/app/components/timewindow-panel.controller.js b/ui/src/app/components/timewindow-panel.controller.js
index ab81e9d..e6af0b0 100644
--- a/ui/src/app/components/timewindow-panel.controller.js
+++ b/ui/src/app/components/timewindow-panel.controller.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
/*@ngInject*/
-export default function TimewindowPanelController(mdPanelRef, $scope, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) {
+export default function TimewindowPanelController(mdPanelRef, $scope, timeService, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) {
var vm = this;
@@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim
vm.aggregation = aggregation;
vm.onTimewindowUpdate = onTimewindowUpdate;
vm.aggregationTypes = types.aggregation;
+ vm.showLimit = showLimit;
+ vm.showRealtimeAggInterval = showRealtimeAggInterval;
+ vm.showHistoryAggInterval = showHistoryAggInterval;
+ vm.minRealtimeAggInterval = minRealtimeAggInterval;
+ vm.maxRealtimeAggInterval = maxRealtimeAggInterval;
+ vm.minHistoryAggInterval = minHistoryAggInterval;
+ vm.maxHistoryAggInterval = maxHistoryAggInterval;
if (vm.historyOnly) {
vm.timewindow.selectedTab = 1;
@@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim
vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow);
});
};
+
+ function showLimit() {
+ return vm.timewindow.aggregation.type === vm.aggregationTypes.none.value;
+ }
+
+ function showRealtimeAggInterval() {
+ return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value &&
+ vm.timewindow.selectedTab === 0;
+ }
+
+ function showHistoryAggInterval() {
+ return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value &&
+ vm.timewindow.selectedTab === 1;
+ }
+
+ function minRealtimeAggInterval () {
+ return timeService.minIntervalLimit(vm.timewindow.realtime.timewindowMs);
+ }
+
+ function maxRealtimeAggInterval () {
+ return timeService.maxIntervalLimit(vm.timewindow.realtime.timewindowMs);
+ }
+
+ function minHistoryAggInterval () {
+ return timeService.minIntervalLimit(currentHistoryTimewindow());
+ }
+
+ function maxHistoryAggInterval () {
+ return timeService.maxIntervalLimit(currentHistoryTimewindow());
+ }
+
+ function currentHistoryTimewindow() {
+ if (vm.timewindow.history.historyType === 0) {
+ return vm.timewindow.history.timewindowMs;
+ } else {
+ return vm.timewindow.history.fixedTimewindow.endTimeMs -
+ vm.timewindow.history.fixedTimewindow.startTimeMs;
+ }
+ }
+
}
+
ui/src/app/components/timewindow-panel.tpl.html 115(+62 -53)
diff --git a/ui/src/app/components/timewindow-panel.tpl.html b/ui/src/app/components/timewindow-panel.tpl.html
index 89825b0..b7550e8 100644
--- a/ui/src/app/components/timewindow-panel.tpl.html
+++ b/ui/src/app/components/timewindow-panel.tpl.html
@@ -17,61 +17,70 @@
-->
<form name="theForm" ng-submit="vm.update()">
<fieldset ng-disabled="loading">
- <md-content layout="column">
- <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
- <md-tab label="{{ 'timewindow.realtime' | translate }}">
- <md-content class="md-padding" layout="column">
- <span translate>timewindow.last</span>
- <tb-timeinterval
- ng-required="vm.timewindow.selectedTab === 0"
- ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
- </md-content>
- </md-tab>
- <md-tab label="{{ 'timewindow.history' | translate }}">
- <md-content class="md-padding" layout="column">
- <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary">
- <md-radio-button ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
- <section layout="column">
- <span translate>timewindow.last</span>
- <tb-timeinterval
- ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0"
- ng-show="vm.timewindow.history.historyType === 0"
- ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
- </section>
- </md-radio-button>
- <md-radio-button ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
- <section layout="column">
- <span translate>timewindow.time-period</span>
- <tb-datetime-period
- ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1"
- ng-show="vm.timewindow.history.historyType === 1"
- ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period>
- </section>
- </md-radio-button>
- </md-radio-group>
- </md-content>
- </md-tab>
- </md-tabs>
- <md-content ng-if="vm.aggregation" class="md-padding" layout="column">
- <md-input-container>
- <label translate>aggregation.function</label>
- <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;">
- <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value">
- {{type.name | translate}}
- </md-option>
- </md-select>
- </md-input-container>
- <md-slider-container>
- <span translate>aggregation.limit</span>
- <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider">
- </md-slider>
+ <md-content style="height: 100%" flex layout="column">
+ <section layout="column">
+ <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
+ <md-tab label="{{ 'timewindow.realtime' | translate }}">
+ <md-content class="md-padding" layout="column">
+ <tb-timeinterval predefined-name="'timewindow.last'"
+ ng-required="vm.timewindow.selectedTab === 0"
+ ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
+ </md-content>
+ </md-tab>
+ <md-tab label="{{ 'timewindow.history' | translate }}">
+ <md-content class="md-padding" layout="column" style="padding-top: 8px;">
+ <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary">
+ <md-radio-button ng-value=0 aria-label="{{ 'timewindow.last' | translate }}" class="md-primary md-align-top-left md-radio-interactive">
+ <section layout="column">
+ <tb-timeinterval predefined-name="'timewindow.last'"
+ ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0"
+ ng-show="vm.timewindow.history.historyType === 0"
+ ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
+ </section>
+ </md-radio-button>
+ <md-radio-button ng-value=1 aria-label="{{ 'timewindow.time-period' | translate }}" class="md-primary md-align-top-left md-radio-interactive">
+ <section layout="column">
+ <span translate>timewindow.time-period</span>
+ <tb-datetime-period
+ ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1"
+ ng-show="vm.timewindow.history.historyType === 1"
+ ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period>
+ </section>
+ </md-radio-button>
+ </md-radio-group>
+ </md-content>
+ </md-tab>
+ </md-tabs>
+ <md-content ng-if="vm.aggregation" class="md-padding" layout="column">
<md-input-container>
- <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider">
+ <label translate>aggregation.function</label>
+ <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;">
+ <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value">
+ {{type.name | translate}}
+ </md-option>
+ </md-select>
</md-input-container>
- </md-slider-container>
- </md-content>
+ <md-slider-container ng-show="vm.showLimit()">
+ <span translate>aggregation.limit</span>
+ <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider">
+ </md-slider>
+ <md-input-container>
+ <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider">
+ </md-input-container>
+ </md-slider-container>
+ <tb-timeinterval ng-show="vm.showRealtimeAggInterval()" min="vm.minRealtimeAggInterval()" max="vm.maxRealtimeAggInterval()"
+ predefined-name="'aggregation.group-interval'"
+ ng-model="vm.timewindow.realtime.interval">
+ </tb-timeinterval>
+ <tb-timeinterval ng-show="vm.showHistoryAggInterval()" min="vm.minHistoryAggInterval()" max="vm.maxHistoryAggInterval()"
+ predefined-name="'aggregation.group-interval'"
+ ng-model="vm.timewindow.history.interval">
+ </tb-timeinterval>
+ </md-content>
+ </section>
+ <span flex></span>
<section layout="row" layout-alignment="start center">
- <span flex></span>
+ <span flex></span>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
{{ 'action.update' | translate }}
</md-button>
@@ -79,6 +88,6 @@
{{ 'action.cancel' | translate }}
</md-button>
</section>
- </section>
+ </md-content>
</fieldset>
</form>
\ No newline at end of file
ui/src/app/components/widget.controller.js 87(+23 -64)
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index f7e9498..a2056e0 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
/* eslint-disable angular/angularelement */
/*@ngInject*/
-export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils,
+export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, timeService,
datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) {
var vm = this;
@@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var targetDeviceAliasId = null;
var targetDeviceId = null;
var originalTimewindow = null;
- var subscriptionTimewindow = {
- fixedWindow: null,
- realtimeWindowMs: null,
- aggregation: null
- };
+ var subscriptionTimewindow = null;
var dataUpdateCaf = null;
/*
@@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
if (!originalTimewindow) {
originalTimewindow = angular.copy(widget.config.timewindow);
}
- widget.config.timewindow = {
- history: {
- fixedTimewindow: {
- startTimeMs: startTimeMs,
- endTimeMs: endTimeMs
- }
- },
- aggregation: angular.copy(widget.config.timewindow.aggregation)
- };
+ widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);
}
function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) {
@@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
if (update) {
- if (subscriptionTimewindow.realtimeWindowMs) {
+ if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) {
updateTimewindow();
}
widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data;
@@ -555,62 +543,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
+ function updateRealtimeSubscription(_subscriptionTimewindow) {
+ if (_subscriptionTimewindow) {
+ subscriptionTimewindow = _subscriptionTimewindow;
+ } else {
+ subscriptionTimewindow = timeService.createSubscriptionTimewindow(widget.config.timewindow, widgetContext.timeWindow.stDiff);
+ }
+ updateTimewindow();
+ return subscriptionTimewindow;
+ }
+
function subscribe() {
if (widget.type !== types.widgetType.rpc.value) {
- var index = 0;
- subscriptionTimewindow.fixedWindow = null;
- subscriptionTimewindow.realtimeWindowMs = null;
- subscriptionTimewindow.aggregation = {
- limit: 200,
- type: types.aggregation.avg.value
- };
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,
- type: widget.config.timewindow.aggregation.type || types.aggregation.avg.value
- };
- }
-
- 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();
- subscriptionTimewindow.fixedWindow = {
- 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();
+ updateRealtimeSubscription();
if (subscriptionTimewindow.fixedWindow) {
onDataUpdated();
}
}
+ var index = 0;
for (var i in widget.config.datasources) {
var datasource = widget.config.datasources[i];
var deviceId = null;
@@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
dataUpdated: function (data, datasourceIndex, dataKeyIndex) {
dataUpdated(data, datasourceIndex, dataKeyIndex);
},
+ updateRealtimeSubscription: function() {
+ this.subscriptionTimewindow = updateRealtimeSubscription();
+ return this.subscriptionTimewindow;
+ },
+ setRealtimeSubscription: function(subscriptionTimewindow) {
+ updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
+ },
datasourceIndex: index
};
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index ba3e466..c25c6f2 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, dashboardService, deviceService, widgetService) {
+ $document, $translate, $filter, utils, types, dashboardService, deviceService, widgetService) {
var linker = function (scope, element, attrs) {
@@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
function success(widgetTypes) {
+
+ widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
+
for (var i = 0; i < widgetTypes.length; i++) {
var widgetType = widgetTypes[i];
var widgetInfo = widgetService.toWidgetInfo(widgetType);
ui/src/app/locale/locale.constant.js 4(+3 -1)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 525c307..05411d7 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', [])
"aggregation": "Aggregation",
"function": "Data aggregation function",
"limit": "Max values",
+ "group-interval": "Grouping interval",
"min": "Min",
"max": "Max",
"avg": "Average",
@@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', [])
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
- "seconds": "Seconds"
+ "seconds": "Seconds",
+ "advanced": "Advanced"
},
"timewindow": {
"days": "{ days, select, 1 { day } other {# days } }",
ui/src/app/widget/lib/flot-widget.js 53(+47 -6)
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index c35b363..2888b0f 100644
--- a/ui/src/app/widget/lib/flot-widget.js
+++ b/ui/src/app/widget/lib/flot-widget.js
@@ -167,6 +167,7 @@ export default class TbFlot {
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);
+ ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false;
var font = {
color: settings.fontColor || "#545454",
@@ -232,6 +233,21 @@ export default class TbFlot {
options.yaxis.tickFormatter = function() {
return '';
};
+ } else if (settings.units && settings.units.length > 0) {
+ options.yaxis.tickFormatter = function(value, axis) {
+ var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1,
+ formatted = "" + Math.round(value * factor) / factor;
+ if (axis.tickDecimals != null) {
+ var decimal = formatted.indexOf("."),
+ precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
+
+ if (precision < axis.tickDecimals) {
+ formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
+ }
+ }
+ formatted += ' ' + tbFlot.ctx.settings.units;
+ return formatted;
+ };
}
options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color;
options.yaxis.label = settings.yaxis.title || null;
@@ -323,6 +339,8 @@ export default class TbFlot {
this.options = options;
+ this.checkMouseEvents();
+
if (this.chartType === 'pie' && this.ctx.animatedPie) {
this.ctx.pieDataAnimationDuration = 250;
this.ctx.pieData = angular.copy(this.ctx.data);
@@ -337,7 +355,6 @@ export default class TbFlot {
} else {
this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
}
- this.checkMouseEvents();
}
update() {
@@ -577,6 +594,11 @@ export default class TbFlot {
"type": "boolean",
"default": false
},
+ "tooltipCumulative": {
+ "title": "Show cumulative values in stacking mode",
+ "type": "boolean",
+ "default": false
+ },
"grid": {
"title": "Grid settings",
"type": "object",
@@ -710,6 +732,7 @@ export default class TbFlot {
"decimals",
"units",
"tooltipIndividual",
+ "tooltipCumulative",
{
"key": "grid",
"items": [
@@ -834,10 +857,28 @@ export default class TbFlot {
}
checkMouseEvents() {
- if (this.ctx.isMobile || this.ctx.isEdit) {
- this.disableMouseEvents();
- } else if (!this.ctx.isEdit) {
- this.enableMouseEvents();
+ var enabled = !this.ctx.isMobile && !this.ctx.isEdit;
+ if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) {
+ this.mouseEventsEnabled = enabled;
+ if (enabled) {
+ this.enableMouseEvents();
+ } else {
+ this.disableMouseEvents();
+ }
+ if (this.ctx.plot) {
+ this.ctx.plot.destroy();
+ if (this.chartType === 'pie' && this.ctx.animatedPie) {
+ this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options);
+ } else {
+ this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
+ }
+ }
+ }
+ }
+
+ destroy() {
+ if (this.ctx.plot) {
+ this.ctx.plot.destroy();
}
}
@@ -1030,7 +1071,7 @@ export default class TbFlot {
minTime = pointTime;
}
if (series.stack) {
- if (this.ctx.tooltipIndividual) {
+ if (this.ctx.tooltipIndividual || !this.ctx.tooltipCumulative) {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
ui/src/scss/main.scss 8(+8 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 606b1d9..31c256d 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -201,6 +201,14 @@ md-sidenav {
color: rgba(0,0,0,0.54);
}
+label {
+ &.tb-small {
+ pointer-events: none;
+ color: rgba(0,0,0,0.54);
+ font-size: 12px;
+ }
+}
+
/***********************
* Prompt
***********************/