Browse Source

UI: Improve aggregation interval configuration. Minor bug fixes.

pull/56/head
Igor Kulikov 10 years ago
parent
commit
087cd95bb5
  1. 6
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
  2. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
  3. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
  4. 6
      dao/src/main/resources/system-data.cql
  5. 12
      dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
  6. 1
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
  7. 1
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
  8. 3
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
  9. 4
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
  10. 57
      ui/src/app/api/data-aggregator.js
  11. 91
      ui/src/app/api/datasource.service.js
  12. 330
      ui/src/app/api/time.service.js
  13. 4
      ui/src/app/api/widget.service.js
  14. 2
      ui/src/app/app.js
  15. 131
      ui/src/app/components/timeinterval.directive.js
  16. 10
      ui/src/app/components/timeinterval.scss
  17. 66
      ui/src/app/components/timeinterval.tpl.html
  18. 50
      ui/src/app/components/timewindow-panel.controller.js
  19. 115
      ui/src/app/components/timewindow-panel.tpl.html
  20. 50
      ui/src/app/components/timewindow.directive.js
  21. 27
      ui/src/app/components/timewindow.scss
  22. 4
      ui/src/app/components/timewindow.tpl.html
  23. 87
      ui/src/app/components/widget.controller.js
  24. 5
      ui/src/app/device/attribute/attribute-table.directive.js
  25. 4
      ui/src/app/locale/locale.constant.js
  26. 53
      ui/src/app/widget/lib/flot-widget.js
  27. 8
      ui/src/scss/main.scss

6
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);
}
}

2
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();

4
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;
}

6
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 **/

12
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());

1
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;

1
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;

3
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) {

4
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) {

57
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);

91
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;
}

330
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));
}
}
}

4
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' +

2
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,

131
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
};

10
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;
}
}

66
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>

50
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;
}
}
}

115
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>

50
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();

27
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;
}
}
}
}

4
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>

87
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
};

5
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);

4
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 } }",

53
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];

8
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
***********************/

Loading…
Cancel
Save