Commit 087cd95bb50fd3c25408645caba7c0ef4b539443
1 parent
7769dd1c
UI: Improve aggregation interval configuration. Minor bug fixes.
Showing
27 changed files
with
796 additions
and
337 deletions
@@ -23,19 +23,21 @@ public class BaseTsKvQuery implements TsKvQuery { | @@ -23,19 +23,21 @@ public class BaseTsKvQuery implements TsKvQuery { | ||
23 | private final String key; | 23 | private final String key; |
24 | private final long startTs; | 24 | private final long startTs; |
25 | private final long endTs; | 25 | private final long endTs; |
26 | + private final long interval; | ||
26 | private final int limit; | 27 | private final int limit; |
27 | private final Aggregation aggregation; | 28 | private final Aggregation aggregation; |
28 | 29 | ||
29 | - public BaseTsKvQuery(String key, long startTs, long endTs, int limit, Aggregation aggregation) { | 30 | + public BaseTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation) { |
30 | this.key = key; | 31 | this.key = key; |
31 | this.startTs = startTs; | 32 | this.startTs = startTs; |
32 | this.endTs = endTs; | 33 | this.endTs = endTs; |
34 | + this.interval = interval; | ||
33 | this.limit = limit; | 35 | this.limit = limit; |
34 | this.aggregation = aggregation; | 36 | this.aggregation = aggregation; |
35 | } | 37 | } |
36 | 38 | ||
37 | public BaseTsKvQuery(String key, long startTs, long endTs) { | 39 | public BaseTsKvQuery(String key, long startTs, long endTs) { |
38 | - this(key, startTs, endTs, 1, Aggregation.AVG); | 40 | + this(key, startTs, endTs, endTs-startTs, 1, Aggregation.AVG); |
39 | } | 41 | } |
40 | 42 | ||
41 | } | 43 | } |
@@ -112,13 +112,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao | @@ -112,13 +112,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao | ||
112 | if (query.getAggregation() == Aggregation.NONE) { | 112 | if (query.getAggregation() == Aggregation.NONE) { |
113 | return findAllAsyncWithLimit(entityType, entityId, query); | 113 | return findAllAsyncWithLimit(entityType, entityId, query); |
114 | } else { | 114 | } else { |
115 | - long step = Math.max((query.getEndTs() - query.getStartTs()) / query.getLimit(), minAggregationStepMs); | 115 | + long step = Math.max(query.getInterval(), minAggregationStepMs); |
116 | long stepTs = query.getStartTs(); | 116 | long stepTs = query.getStartTs(); |
117 | List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); | 117 | List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); |
118 | while (stepTs < query.getEndTs()) { | 118 | while (stepTs < query.getEndTs()) { |
119 | long startTs = stepTs; | 119 | long startTs = stepTs; |
120 | long endTs = stepTs + step; | 120 | long endTs = stepTs + step; |
121 | - TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, 1, query.getAggregation()); | 121 | + TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation()); |
122 | futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); | 122 | futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); |
123 | stepTs = endTs; | 123 | stepTs = endTs; |
124 | } | 124 | } |
@@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map', | @@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map', | ||
272 | 272 | ||
273 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) | 273 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
274 | VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie', | 274 | VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie', |
275 | -'{"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}}"}', | 275 | +'{"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}}"}', |
276 | 'Pie - Flot' ); | 276 | 'Pie - Flot' ); |
277 | 277 | ||
278 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) | 278 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
279 | VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'timeseries_bars_flot', | 279 | VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'timeseries_bars_flot', |
280 | -'{"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}"}', | 280 | +'{"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}"}', |
281 | 'Timeseries Bars - Flot' ); | 281 | 'Timeseries Bars - Flot' ); |
282 | 282 | ||
283 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) | 283 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
284 | VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries', | 284 | VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries', |
285 | -'{"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}"}', | 285 | +'{"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}"}', |
286 | 'Timeseries - Flot' ); | 286 | 'Timeseries - Flot' ); |
287 | 287 | ||
288 | /** System plugins and rules **/ | 288 | /** System plugins and rules **/ |
@@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | @@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | ||
115 | entries.add(save(deviceId, 55000, 600)); | 115 | entries.add(save(deviceId, 55000, 600)); |
116 | 116 | ||
117 | List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, | 117 | List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, |
118 | - 60000, 3, Aggregation.NONE))).get(); | 118 | + 60000, 20000, 3, Aggregation.NONE))).get(); |
119 | assertEquals(3, list.size()); | 119 | assertEquals(3, list.size()); |
120 | assertEquals(55000, list.get(0).getTs()); | 120 | assertEquals(55000, list.get(0).getTs()); |
121 | assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue()); | 121 | assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue()); |
@@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | @@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | ||
127 | assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue()); | 127 | assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue()); |
128 | 128 | ||
129 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, | 129 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, |
130 | - 60000, 3, Aggregation.AVG))).get(); | 130 | + 60000, 20000, 3, Aggregation.AVG))).get(); |
131 | assertEquals(3, list.size()); | 131 | assertEquals(3, list.size()); |
132 | assertEquals(10000, list.get(0).getTs()); | 132 | assertEquals(10000, list.get(0).getTs()); |
133 | assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue()); | 133 | assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue()); |
@@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | @@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | ||
139 | assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue()); | 139 | assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue()); |
140 | 140 | ||
141 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, | 141 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, |
142 | - 60000, 3, Aggregation.SUM))).get(); | 142 | + 60000, 20000, 3, Aggregation.SUM))).get(); |
143 | 143 | ||
144 | assertEquals(3, list.size()); | 144 | assertEquals(3, list.size()); |
145 | assertEquals(10000, list.get(0).getTs()); | 145 | assertEquals(10000, list.get(0).getTs()); |
@@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | @@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | ||
152 | assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue()); | 152 | assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue()); |
153 | 153 | ||
154 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, | 154 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, |
155 | - 60000, 3, Aggregation.MIN))).get(); | 155 | + 60000, 20000, 3, Aggregation.MIN))).get(); |
156 | 156 | ||
157 | assertEquals(3, list.size()); | 157 | assertEquals(3, list.size()); |
158 | assertEquals(10000, list.get(0).getTs()); | 158 | assertEquals(10000, list.get(0).getTs()); |
@@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | @@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | ||
165 | assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue()); | 165 | assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue()); |
166 | 166 | ||
167 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, | 167 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, |
168 | - 60000, 3, Aggregation.MAX))).get(); | 168 | + 60000, 20000, 3, Aggregation.MAX))).get(); |
169 | 169 | ||
170 | assertEquals(3, list.size()); | 170 | assertEquals(3, list.size()); |
171 | assertEquals(10000, list.get(0).getTs()); | 171 | assertEquals(10000, list.get(0).getTs()); |
@@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | @@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { | ||
178 | assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue()); | 178 | assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue()); |
179 | 179 | ||
180 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, | 180 | list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, |
181 | - 60000, 3, Aggregation.COUNT))).get(); | 181 | + 60000, 20000, 3, Aggregation.COUNT))).get(); |
182 | 182 | ||
183 | assertEquals(3, list.size()); | 183 | assertEquals(3, list.size()); |
184 | assertEquals(10000, list.get(0).getTs()); | 184 | assertEquals(10000, list.get(0).getTs()); |
@@ -32,6 +32,7 @@ public class GetHistoryCmd implements TelemetryPluginCmd { | @@ -32,6 +32,7 @@ public class GetHistoryCmd implements TelemetryPluginCmd { | ||
32 | private String keys; | 32 | private String keys; |
33 | private long startTs; | 33 | private long startTs; |
34 | private long endTs; | 34 | private long endTs; |
35 | + private long interval; | ||
35 | private int limit; | 36 | private int limit; |
36 | private String agg; | 37 | private String agg; |
37 | 38 |
@@ -30,6 +30,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd { | @@ -30,6 +30,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd { | ||
30 | 30 | ||
31 | private long startTs; | 31 | private long startTs; |
32 | private long timeWindow; | 32 | private long timeWindow; |
33 | + private long interval; | ||
33 | private int limit; | 34 | private int limit; |
34 | private String agg; | 35 | private String agg; |
35 | 36 |
@@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { | @@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { | ||
89 | String keysStr = request.getParameter("keys"); | 89 | String keysStr = request.getParameter("keys"); |
90 | Optional<Long> startTs = request.getLongParamValue("startTs"); | 90 | Optional<Long> startTs = request.getLongParamValue("startTs"); |
91 | Optional<Long> endTs = request.getLongParamValue("endTs"); | 91 | Optional<Long> endTs = request.getLongParamValue("endTs"); |
92 | + Optional<Long> interval = request.getLongParamValue("interval"); | ||
92 | Optional<Integer> limit = request.getIntParamValue("limit"); | 93 | Optional<Integer> limit = request.getIntParamValue("limit"); |
93 | Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); | 94 | Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); |
94 | 95 | ||
95 | List<String> keys = Arrays.asList(keysStr.split(",")); | 96 | List<String> keys = Arrays.asList(keysStr.split(",")); |
96 | - List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)).collect(Collectors.toList()); | 97 | + 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()); |
97 | ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { | 98 | ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { |
98 | @Override | 99 | @Override |
99 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { | 100 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { |
@@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { | @@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { | ||
193 | log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); | 193 | log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); |
194 | startTs = cmd.getStartTs(); | 194 | startTs = cmd.getStartTs(); |
195 | long endTs = cmd.getStartTs() + cmd.getTimeWindow(); | 195 | long endTs = cmd.getStartTs() + cmd.getTimeWindow(); |
196 | - List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); | 196 | + List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); |
197 | ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); | 197 | ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); |
198 | } else { | 198 | } else { |
199 | List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); | 199 | List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); |
@@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { | @@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { | ||
277 | } | 277 | } |
278 | DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); | 278 | DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); |
279 | List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); | 279 | List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); |
280 | - List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); | 280 | + 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()); |
281 | ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { | 281 | ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { |
282 | @Override | 282 | @Override |
283 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { | 283 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { |
@@ -25,11 +25,12 @@ export default class DataAggregator { | @@ -25,11 +25,12 @@ export default class DataAggregator { | ||
25 | this.$timeout = $timeout; | 25 | this.$timeout = $timeout; |
26 | this.$filter = $filter; | 26 | this.$filter = $filter; |
27 | this.dataReceived = false; | 27 | this.dataReceived = false; |
28 | + this.resetPending = false; | ||
28 | this.noAggregation = aggregationType === types.aggregation.none.value; | 29 | this.noAggregation = aggregationType === types.aggregation.none.value; |
29 | this.limit = limit; | 30 | this.limit = limit; |
30 | this.timeWindow = timeWindow; | 31 | this.timeWindow = timeWindow; |
31 | this.interval = interval; | 32 | this.interval = interval; |
32 | - this.aggregationTimeout = this.interval; | 33 | + this.aggregationTimeout = Math.max(this.interval, 1000); |
33 | switch (aggregationType) { | 34 | switch (aggregationType) { |
34 | case types.aggregation.min.value: | 35 | case types.aggregation.min.value: |
35 | this.aggFunction = min; | 36 | this.aggFunction = min; |
@@ -54,11 +55,37 @@ export default class DataAggregator { | @@ -54,11 +55,37 @@ export default class DataAggregator { | ||
54 | } | 55 | } |
55 | } | 56 | } |
56 | 57 | ||
58 | + reset(startTs, timeWindow, interval) { | ||
59 | + if (this.intervalTimeoutHandle) { | ||
60 | + this.$timeout.cancel(this.intervalTimeoutHandle); | ||
61 | + this.intervalTimeoutHandle = null; | ||
62 | + } | ||
63 | + this.intervalScheduledTime = currentTime(); | ||
64 | + this.startTs = startTs; | ||
65 | + this.timeWindow = timeWindow; | ||
66 | + this.interval = interval; | ||
67 | + this.endTs = this.startTs + this.timeWindow; | ||
68 | + this.elapsed = 0; | ||
69 | + this.aggregationTimeout = Math.max(this.interval, 1000); | ||
70 | + this.resetPending = true; | ||
71 | + var self = this; | ||
72 | + this.intervalTimeoutHandle = this.$timeout(function() { | ||
73 | + self.onInterval(); | ||
74 | + }, this.aggregationTimeout, false); | ||
75 | + } | ||
76 | + | ||
57 | onData(data, update, history) { | 77 | onData(data, update, history) { |
58 | - if (!this.dataReceived) { | ||
59 | - this.elapsed = 0; | ||
60 | - this.dataReceived = true; | ||
61 | - this.endTs = this.startTs + this.timeWindow; | 78 | + if (!this.dataReceived || this.resetPending) { |
79 | + var updateIntervalScheduledTime = true; | ||
80 | + if (!this.dataReceived) { | ||
81 | + this.elapsed = 0; | ||
82 | + this.dataReceived = true; | ||
83 | + this.endTs = this.startTs + this.timeWindow; | ||
84 | + } | ||
85 | + if (this.resetPending) { | ||
86 | + this.resetPending = false; | ||
87 | + updateIntervalScheduledTime = false; | ||
88 | + } | ||
62 | if (update) { | 89 | if (update) { |
63 | this.aggregationMap = {}; | 90 | this.aggregationMap = {}; |
64 | updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, | 91 | updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, |
@@ -66,19 +93,24 @@ export default class DataAggregator { | @@ -66,19 +93,24 @@ export default class DataAggregator { | ||
66 | } else { | 93 | } else { |
67 | this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); | 94 | this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); |
68 | } | 95 | } |
69 | - this.onInterval(currentTime(), history); | 96 | + if (updateIntervalScheduledTime) { |
97 | + this.intervalScheduledTime = currentTime(); | ||
98 | + } | ||
99 | + this.onInterval(history); | ||
70 | } else { | 100 | } else { |
71 | updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, | 101 | updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, |
72 | this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); | 102 | this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); |
73 | if (history) { | 103 | if (history) { |
74 | - this.onInterval(currentTime(), history); | 104 | + this.intervalScheduledTime = currentTime(); |
105 | + this.onInterval(history); | ||
75 | } | 106 | } |
76 | } | 107 | } |
77 | } | 108 | } |
78 | 109 | ||
79 | - onInterval(startedTime, history) { | 110 | + onInterval(history) { |
80 | var now = currentTime(); | 111 | var now = currentTime(); |
81 | - this.elapsed += now - startedTime; | 112 | + this.elapsed += now - this.intervalScheduledTime; |
113 | + this.intervalScheduledTime = now; | ||
82 | if (this.intervalTimeoutHandle) { | 114 | if (this.intervalTimeoutHandle) { |
83 | this.$timeout.cancel(this.intervalTimeoutHandle); | 115 | this.$timeout.cancel(this.intervalTimeoutHandle); |
84 | this.intervalTimeoutHandle = null; | 116 | this.intervalTimeoutHandle = null; |
@@ -101,16 +133,11 @@ export default class DataAggregator { | @@ -101,16 +133,11 @@ export default class DataAggregator { | ||
101 | var self = this; | 133 | var self = this; |
102 | if (!history) { | 134 | if (!history) { |
103 | this.intervalTimeoutHandle = this.$timeout(function() { | 135 | this.intervalTimeoutHandle = this.$timeout(function() { |
104 | - self.onInterval(now); | 136 | + self.onInterval(); |
105 | }, this.aggregationTimeout, false); | 137 | }, this.aggregationTimeout, false); |
106 | } | 138 | } |
107 | } | 139 | } |
108 | 140 | ||
109 | - reset() { | ||
110 | - this.destroy(); | ||
111 | - this.dataReceived = false; | ||
112 | - } | ||
113 | - | ||
114 | destroy() { | 141 | destroy() { |
115 | if (this.intervalTimeoutHandle) { | 142 | if (this.intervalTimeoutHandle) { |
116 | this.$timeout.cancel(this.intervalTimeoutHandle); | 143 | this.$timeout.cancel(this.intervalTimeoutHandle); |
@@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | @@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | ||
254 | keys: tsKeys, | 254 | keys: tsKeys, |
255 | startTs: subsTw.fixedWindow.startTimeMs, | 255 | startTs: subsTw.fixedWindow.startTimeMs, |
256 | endTs: subsTw.fixedWindow.endTimeMs, | 256 | endTs: subsTw.fixedWindow.endTimeMs, |
257 | + interval: subsTw.aggregation.interval, | ||
257 | limit: subsTw.aggregation.limit, | 258 | limit: subsTw.aggregation.limit, |
258 | agg: subsTw.aggregation.type | 259 | agg: subsTw.aggregation.type |
259 | }; | 260 | }; |
@@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | @@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | ||
266 | onData(data.data, types.dataKeyType.timeseries); | 267 | onData(data.data, types.dataKeyType.timeseries); |
267 | } | 268 | } |
268 | }, | 269 | }, |
269 | - onReconnected: function() { | ||
270 | - onReconnected(); | ||
271 | - } | 270 | + onReconnected: function() {} |
272 | }; | 271 | }; |
273 | 272 | ||
274 | telemetryWebsocketService.subscribe(subscriber); | 273 | telemetryWebsocketService.subscribe(subscriber); |
@@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | @@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | ||
287 | }; | 286 | }; |
288 | 287 | ||
289 | if (datasourceSubscription.type === types.widgetType.timeseries.value) { | 288 | if (datasourceSubscription.type === types.widgetType.timeseries.value) { |
290 | - subscriptionCommand.startTs = subsTw.startTs; | ||
291 | - subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow; | ||
292 | - subscriptionCommand.limit = subsTw.aggregation.limit; | ||
293 | - subscriptionCommand.agg = subsTw.aggregation.type; | ||
294 | - dataAggregator = new DataAggregator( | ||
295 | - function(data, startTs, endTs) { | ||
296 | - onData(data, types.dataKeyType.timeseries, startTs, endTs); | ||
297 | - }, | ||
298 | - tsKeyNames, | ||
299 | - subsTw.startTs, | ||
300 | - subsTw.aggregation.limit, | ||
301 | - subsTw.aggregation.type, | ||
302 | - subsTw.aggregation.timeWindow, | ||
303 | - subsTw.aggregation.interval, | ||
304 | - types, | ||
305 | - $timeout, | ||
306 | - $filter | ||
307 | - ); | 289 | + updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw); |
290 | + dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames); | ||
308 | subscriber.onData = function(data) { | 291 | subscriber.onData = function(data) { |
309 | dataAggregator.onData(data); | 292 | dataAggregator.onData(data); |
310 | } | 293 | } |
311 | subscriber.onReconnected = function() { | 294 | subscriber.onReconnected = function() { |
312 | - dataAggregator.reset(); | ||
313 | - onReconnected(); | 295 | + var newSubsTw = null; |
296 | + for (var i2 in listeners) { | ||
297 | + var listener = listeners[i2]; | ||
298 | + if (!newSubsTw) { | ||
299 | + newSubsTw = listener.updateRealtimeSubscription(); | ||
300 | + } else { | ||
301 | + listener.setRealtimeSubscription(newSubsTw); | ||
302 | + } | ||
303 | + } | ||
304 | + updateRealtimeSubscriptionCommand(this.subscriptionCommand, newSubsTw); | ||
305 | + dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); | ||
314 | } | 306 | } |
315 | } else { | 307 | } else { |
316 | - subscriber.onReconnected = function() { | ||
317 | - onReconnected(); | ||
318 | - } | 308 | + subscriber.onReconnected = function() {} |
319 | subscriber.onData = function(data) { | 309 | subscriber.onData = function(data) { |
320 | if (data.data) { | 310 | if (data.data) { |
321 | onData(data.data, types.dataKeyType.timeseries); | 311 | onData(data.data, types.dataKeyType.timeseries); |
@@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | @@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | ||
344 | onData(data.data, types.dataKeyType.attribute); | 334 | onData(data.data, types.dataKeyType.attribute); |
345 | } | 335 | } |
346 | }, | 336 | }, |
347 | - onReconnected: function() { | ||
348 | - onReconnected(); | ||
349 | - } | 337 | + onReconnected: function() {} |
350 | }; | 338 | }; |
351 | 339 | ||
352 | telemetryWebsocketService.subscribe(subscriber); | 340 | telemetryWebsocketService.subscribe(subscriber); |
@@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | @@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | ||
384 | timer = $timeout(onTick, 0, false); | 372 | timer = $timeout(onTick, 0, false); |
385 | } | 373 | } |
386 | } | 374 | } |
375 | + } | ||
376 | + | ||
377 | + function createRealtimeDataAggregator(subsTw, tsKeyNames) { | ||
378 | + return new DataAggregator( | ||
379 | + function(data, startTs, endTs) { | ||
380 | + onData(data, types.dataKeyType.timeseries, startTs, endTs); | ||
381 | + }, | ||
382 | + tsKeyNames, | ||
383 | + subsTw.startTs, | ||
384 | + subsTw.aggregation.limit, | ||
385 | + subsTw.aggregation.type, | ||
386 | + subsTw.aggregation.timeWindow, | ||
387 | + subsTw.aggregation.interval, | ||
388 | + types, | ||
389 | + $timeout, | ||
390 | + $filter | ||
391 | + ); | ||
392 | + } | ||
387 | 393 | ||
394 | + function updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw) { | ||
395 | + subscriptionCommand.startTs = subsTw.startTs; | ||
396 | + subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow; | ||
397 | + subscriptionCommand.interval = subsTw.aggregation.interval; | ||
398 | + subscriptionCommand.limit = subsTw.aggregation.limit; | ||
399 | + subscriptionCommand.agg = subsTw.aggregation.type; | ||
388 | } | 400 | } |
389 | 401 | ||
390 | function unsubscribe() { | 402 | function unsubscribe() { |
@@ -495,27 +507,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | @@ -495,27 +507,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic | ||
495 | } | 507 | } |
496 | } | 508 | } |
497 | 509 | ||
498 | - function onReconnected() { | ||
499 | - if (datasourceType === types.datasourceType.device) { | ||
500 | - for (var key in dataKeys) { | ||
501 | - var dataKeysList = dataKeys[key]; | ||
502 | - for (var i = 0; i < dataKeysList.length; i++) { | ||
503 | - var dataKey = dataKeysList[i]; | ||
504 | - var datasourceKey = key + '_' + i; | ||
505 | - datasourceData[datasourceKey] = { | ||
506 | - data: [] | ||
507 | - }; | ||
508 | - for (var l in listeners) { | ||
509 | - var listener = listeners[l]; | ||
510 | - listener.dataUpdated(datasourceData[datasourceKey], | ||
511 | - listener.datasourceIndex, | ||
512 | - dataKey.index); | ||
513 | - } | ||
514 | - } | ||
515 | - } | ||
516 | - } | ||
517 | - } | ||
518 | - | ||
519 | function isNumeric(val) { | 510 | function isNumeric(val) { |
520 | return (val - parseFloat( val ) + 1) >= 0; | 511 | return (val - parseFloat( val ) + 1) >= 0; |
521 | } | 512 | } |
ui/src/app/api/time.service.js
0 → 100644
1 | +/* | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +export default angular.module('thingsboard.api.time', []) | ||
17 | + .factory('timeService', TimeService) | ||
18 | + .name; | ||
19 | + | ||
20 | +const SECOND = 1000; | ||
21 | +const MINUTE = 60 * SECOND; | ||
22 | +const HOUR = 60 * MINUTE; | ||
23 | +const DAY = 24 * HOUR; | ||
24 | + | ||
25 | +const MIN_INTERVAL = SECOND; | ||
26 | +const MAX_INTERVAL = 365 * 20 * DAY; | ||
27 | + | ||
28 | +const MIN_LIMIT = 10; | ||
29 | +const AVG_LIMIT = 200; | ||
30 | +const MAX_LIMIT = 500; | ||
31 | + | ||
32 | +/*@ngInject*/ | ||
33 | +function TimeService($translate, types) { | ||
34 | + | ||
35 | + var predefIntervals = [ | ||
36 | + { | ||
37 | + name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'), | ||
38 | + value: 1 * SECOND | ||
39 | + }, | ||
40 | + { | ||
41 | + name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'), | ||
42 | + value: 5 * SECOND | ||
43 | + }, | ||
44 | + { | ||
45 | + name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'), | ||
46 | + value: 10 * SECOND | ||
47 | + }, | ||
48 | + { | ||
49 | + name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'), | ||
50 | + value: 15 * SECOND | ||
51 | + }, | ||
52 | + { | ||
53 | + name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'), | ||
54 | + value: 30 * SECOND | ||
55 | + }, | ||
56 | + { | ||
57 | + name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'), | ||
58 | + value: 1 * MINUTE | ||
59 | + }, | ||
60 | + { | ||
61 | + name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'), | ||
62 | + value: 2 * MINUTE | ||
63 | + }, | ||
64 | + { | ||
65 | + name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'), | ||
66 | + value: 5 * MINUTE | ||
67 | + }, | ||
68 | + { | ||
69 | + name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'), | ||
70 | + value: 10 * MINUTE | ||
71 | + }, | ||
72 | + { | ||
73 | + name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'), | ||
74 | + value: 15 * MINUTE | ||
75 | + }, | ||
76 | + { | ||
77 | + name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'), | ||
78 | + value: 30 * MINUTE | ||
79 | + }, | ||
80 | + { | ||
81 | + name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'), | ||
82 | + value: 1 * HOUR | ||
83 | + }, | ||
84 | + { | ||
85 | + name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'), | ||
86 | + value: 2 * HOUR | ||
87 | + }, | ||
88 | + { | ||
89 | + name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'), | ||
90 | + value: 5 * HOUR | ||
91 | + }, | ||
92 | + { | ||
93 | + name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'), | ||
94 | + value: 10 * HOUR | ||
95 | + }, | ||
96 | + { | ||
97 | + name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'), | ||
98 | + value: 12 * HOUR | ||
99 | + }, | ||
100 | + { | ||
101 | + name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'), | ||
102 | + value: 1 * DAY | ||
103 | + }, | ||
104 | + { | ||
105 | + name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'), | ||
106 | + value: 7 * DAY | ||
107 | + }, | ||
108 | + { | ||
109 | + name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'), | ||
110 | + value: 30 * DAY | ||
111 | + } | ||
112 | + ]; | ||
113 | + | ||
114 | + var service = { | ||
115 | + minIntervalLimit: minIntervalLimit, | ||
116 | + maxIntervalLimit: maxIntervalLimit, | ||
117 | + boundMinInterval: boundMinInterval, | ||
118 | + boundMaxInterval: boundMaxInterval, | ||
119 | + getIntervals: getIntervals, | ||
120 | + matchesExistingInterval: matchesExistingInterval, | ||
121 | + boundToPredefinedInterval: boundToPredefinedInterval, | ||
122 | + defaultTimewindow: defaultTimewindow, | ||
123 | + toHistoryTimewindow: toHistoryTimewindow, | ||
124 | + createSubscriptionTimewindow: createSubscriptionTimewindow, | ||
125 | + avgAggregationLimit: function () { | ||
126 | + return AVG_LIMIT; | ||
127 | + } | ||
128 | + } | ||
129 | + | ||
130 | + return service; | ||
131 | + | ||
132 | + function minIntervalLimit(timewindow) { | ||
133 | + var min = timewindow / MAX_LIMIT; | ||
134 | + return boundMinInterval(min); | ||
135 | + } | ||
136 | + | ||
137 | + function avgInterval(timewindow) { | ||
138 | + var avg = timewindow / AVG_LIMIT; | ||
139 | + return boundMinInterval(avg); | ||
140 | + } | ||
141 | + | ||
142 | + function maxIntervalLimit(timewindow) { | ||
143 | + var max = timewindow / MIN_LIMIT; | ||
144 | + return boundMaxInterval(max); | ||
145 | + } | ||
146 | + | ||
147 | + function boundMinInterval(min) { | ||
148 | + return toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); | ||
149 | + } | ||
150 | + | ||
151 | + function boundMaxInterval(max) { | ||
152 | + return toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL); | ||
153 | + } | ||
154 | + | ||
155 | + function toBound(value, min, max, defValue) { | ||
156 | + if (angular.isDefined(value)) { | ||
157 | + value = Math.max(value, min); | ||
158 | + value = Math.min(value, max); | ||
159 | + return value; | ||
160 | + } else { | ||
161 | + return defValue; | ||
162 | + } | ||
163 | + } | ||
164 | + | ||
165 | + function getIntervals(min, max) { | ||
166 | + min = boundMinInterval(min); | ||
167 | + max = boundMaxInterval(max); | ||
168 | + var intervals = []; | ||
169 | + for (var i in predefIntervals) { | ||
170 | + var interval = predefIntervals[i]; | ||
171 | + if (interval.value >= min && interval.value <= max) { | ||
172 | + intervals.push(interval); | ||
173 | + } | ||
174 | + } | ||
175 | + return intervals; | ||
176 | + } | ||
177 | + | ||
178 | + function matchesExistingInterval(min, max, intervalMs) { | ||
179 | + var intervals = getIntervals(min, max); | ||
180 | + for (var i in intervals) { | ||
181 | + var interval = intervals[i]; | ||
182 | + if (intervalMs === interval.value) { | ||
183 | + return true; | ||
184 | + } | ||
185 | + } | ||
186 | + return false; | ||
187 | + } | ||
188 | + | ||
189 | + function boundToPredefinedInterval(min, max, intervalMs) { | ||
190 | + var intervals = getIntervals(min, max); | ||
191 | + var minDelta = MAX_INTERVAL; | ||
192 | + var boundedInterval = intervalMs || min; | ||
193 | + var matchedInterval; | ||
194 | + for (var i in intervals) { | ||
195 | + var interval = intervals[i]; | ||
196 | + var delta = Math.abs(interval.value - boundedInterval); | ||
197 | + if (delta < minDelta) { | ||
198 | + matchedInterval = interval; | ||
199 | + minDelta = delta; | ||
200 | + } | ||
201 | + } | ||
202 | + boundedInterval = matchedInterval.value; | ||
203 | + return boundedInterval; | ||
204 | + } | ||
205 | + | ||
206 | + function defaultTimewindow() { | ||
207 | + var currentTime = (new Date).getTime(); | ||
208 | + var timewindow = { | ||
209 | + displayValue: "", | ||
210 | + selectedTab: 0, | ||
211 | + realtime: { | ||
212 | + interval: SECOND, | ||
213 | + timewindowMs: MINUTE // 1 min by default | ||
214 | + }, | ||
215 | + history: { | ||
216 | + historyType: 0, | ||
217 | + interval: SECOND, | ||
218 | + timewindowMs: MINUTE, // 1 min by default | ||
219 | + fixedTimewindow: { | ||
220 | + startTimeMs: currentTime - DAY, // 1 day by default | ||
221 | + endTimeMs: currentTime | ||
222 | + } | ||
223 | + }, | ||
224 | + aggregation: { | ||
225 | + type: types.aggregation.avg.value, | ||
226 | + limit: AVG_LIMIT | ||
227 | + } | ||
228 | + } | ||
229 | + return timewindow; | ||
230 | + } | ||
231 | + | ||
232 | + function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs) { | ||
233 | + | ||
234 | + var interval = 0; | ||
235 | + if (timewindow.history) { | ||
236 | + interval = timewindow.history.interval; | ||
237 | + } else if (timewindow.realtime) { | ||
238 | + interval = timewindow.realtime.interval; | ||
239 | + } | ||
240 | + | ||
241 | + var historyTimewindow = { | ||
242 | + history: { | ||
243 | + fixedTimewindow: { | ||
244 | + startTimeMs: startTimeMs, | ||
245 | + endTimeMs: endTimeMs | ||
246 | + }, | ||
247 | + interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval) | ||
248 | + }, | ||
249 | + aggregation: { | ||
250 | + | ||
251 | + } | ||
252 | + } | ||
253 | + if (timewindow.aggregation) { | ||
254 | + historyTimewindow.aggregation.type = timewindow.aggregation.type || types.aggregation.avg.value; | ||
255 | + } else { | ||
256 | + historyTimewindow.aggregation.type = types.aggregation.avg.value; | ||
257 | + } | ||
258 | + | ||
259 | + return historyTimewindow; | ||
260 | + } | ||
261 | + | ||
262 | + function createSubscriptionTimewindow(timewindow, stDiff) { | ||
263 | + | ||
264 | + var subscriptionTimewindow = { | ||
265 | + fixedWindow: null, | ||
266 | + realtimeWindowMs: null, | ||
267 | + aggregation: { | ||
268 | + interval: SECOND, | ||
269 | + limit: AVG_LIMIT, | ||
270 | + type: types.aggregation.avg.value | ||
271 | + } | ||
272 | + }; | ||
273 | + var aggTimewindow = 0; | ||
274 | + | ||
275 | + if (angular.isDefined(timewindow.aggregation)) { | ||
276 | + subscriptionTimewindow.aggregation = { | ||
277 | + type: timewindow.aggregation.type || types.aggregation.avg.value, | ||
278 | + limit: timewindow.aggregation.limit || AVG_LIMIT | ||
279 | + }; | ||
280 | + } | ||
281 | + if (angular.isDefined(timewindow.realtime)) { | ||
282 | + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; | ||
283 | + subscriptionTimewindow.aggregation.interval = | ||
284 | + boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval); | ||
285 | + subscriptionTimewindow.startTs = (new Date).getTime() + stDiff - subscriptionTimewindow.realtimeWindowMs; | ||
286 | + var startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; | ||
287 | + aggTimewindow = subscriptionTimewindow.realtimeWindowMs; | ||
288 | + if (startDiff) { | ||
289 | + subscriptionTimewindow.startTs -= startDiff; | ||
290 | + aggTimewindow += subscriptionTimewindow.aggregation.interval; | ||
291 | + } | ||
292 | + } else if (angular.isDefined(timewindow.history)) { | ||
293 | + if (angular.isDefined(timewindow.history.timewindowMs)) { | ||
294 | + var currentTime = (new Date).getTime(); | ||
295 | + subscriptionTimewindow.fixedWindow = { | ||
296 | + startTimeMs: currentTime - timewindow.history.timewindowMs, | ||
297 | + endTimeMs: currentTime | ||
298 | + } | ||
299 | + aggTimewindow = timewindow.history.timewindowMs; | ||
300 | + | ||
301 | + } else { | ||
302 | + subscriptionTimewindow.fixedWindow = { | ||
303 | + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs, | ||
304 | + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs | ||
305 | + } | ||
306 | + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; | ||
307 | + } | ||
308 | + subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; | ||
309 | + subscriptionTimewindow.aggregation.interval = boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval); | ||
310 | + } | ||
311 | + var aggregation = subscriptionTimewindow.aggregation; | ||
312 | + aggregation.timeWindow = aggTimewindow; | ||
313 | + if (aggregation.type !== types.aggregation.none.value) { | ||
314 | + aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); | ||
315 | + } | ||
316 | + return subscriptionTimewindow; | ||
317 | + } | ||
318 | + | ||
319 | + function boundIntervalToTimewindow(timewindow, intervalMs) { | ||
320 | + var min = minIntervalLimit(timewindow); | ||
321 | + var max = maxIntervalLimit(timewindow); | ||
322 | + if (intervalMs) { | ||
323 | + return toBound(intervalMs, min, max, intervalMs); | ||
324 | + } else { | ||
325 | + return boundToPredefinedInterval(min, max, avgInterval(timewindow)); | ||
326 | + } | ||
327 | + } | ||
328 | + | ||
329 | + | ||
330 | +} |
@@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ | @@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ | ||
129 | resources: [], | 129 | resources: [], |
130 | templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>', | 130 | templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>', |
131 | templateCss: '', | 131 | templateCss: '', |
132 | - controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};', | 132 | + controllerScript: 'self.onInit = function() {}', |
133 | settingsSchema: '{}\n', | 133 | settingsSchema: '{}\n', |
134 | dataKeySettingsSchema: '{}\n', | 134 | dataKeySettingsSchema: '{}\n', |
135 | defaultConfig: '{\n' + | 135 | defaultConfig: '{\n' + |
@@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ | @@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ | ||
147 | resources: [], | 147 | resources: [], |
148 | templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', | 148 | templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', |
149 | templateCss: '', | 149 | templateCss: '', |
150 | - controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};', | 150 | + controllerScript: 'self.onInit = function() {}', |
151 | settingsSchema: '{}\n', | 151 | settingsSchema: '{}\n', |
152 | dataKeySettingsSchema: '{}\n', | 152 | dataKeySettingsSchema: '{}\n', |
153 | defaultConfig: '{\n' + | 153 | defaultConfig: '{\n' + |
@@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service'; | @@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service'; | ||
51 | import thingsboardRaf from './common/raf.provider'; | 51 | import thingsboardRaf from './common/raf.provider'; |
52 | import thingsboardUtils from './common/utils.service'; | 52 | import thingsboardUtils from './common/utils.service'; |
53 | import thingsboardTypes from './common/types.constant'; | 53 | import thingsboardTypes from './common/types.constant'; |
54 | +import thingsboardApiTime from './api/time.service'; | ||
54 | import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; | 55 | import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; |
55 | import thingsboardHelp from './help/help.directive'; | 56 | import thingsboardHelp from './help/help.directive'; |
56 | import thingsboardToast from './services/toast'; | 57 | import thingsboardToast from './services/toast'; |
@@ -101,6 +102,7 @@ angular.module('thingsboard', [ | @@ -101,6 +102,7 @@ angular.module('thingsboard', [ | ||
101 | thingsboardRaf, | 102 | thingsboardRaf, |
102 | thingsboardUtils, | 103 | thingsboardUtils, |
103 | thingsboardTypes, | 104 | thingsboardTypes, |
105 | + thingsboardApiTime, | ||
104 | thingsboardKeyboardShortcut, | 106 | thingsboardKeyboardShortcut, |
105 | thingsboardHelp, | 107 | thingsboardHelp, |
106 | thingsboardToast, | 108 | thingsboardToast, |
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', []) | @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', []) | ||
26 | .name; | 26 | .name; |
27 | 27 | ||
28 | /*@ngInject*/ | 28 | /*@ngInject*/ |
29 | -function Timeinterval($compile, $templateCache, $translate) { | 29 | +function Timeinterval($compile, $templateCache, timeService) { |
30 | 30 | ||
31 | var linker = function (scope, element, attrs, ngModelCtrl) { | 31 | var linker = function (scope, element, attrs, ngModelCtrl) { |
32 | 32 | ||
@@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
39 | scope.mins = 1; | 39 | scope.mins = 1; |
40 | scope.secs = 0; | 40 | scope.secs = 0; |
41 | 41 | ||
42 | - scope.predefIntervals = [ | ||
43 | - { | ||
44 | - name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'), | ||
45 | - value: 10 * 1000 | ||
46 | - }, | ||
47 | - { | ||
48 | - name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'), | ||
49 | - value: 30 * 1000 | ||
50 | - }, | ||
51 | - { | ||
52 | - name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'), | ||
53 | - value: 60 * 1000 | ||
54 | - }, | ||
55 | - { | ||
56 | - name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'), | ||
57 | - value: 2 * 60 * 1000 | ||
58 | - }, | ||
59 | - { | ||
60 | - name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'), | ||
61 | - value: 5 * 60 * 1000 | ||
62 | - }, | ||
63 | - { | ||
64 | - name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'), | ||
65 | - value: 10 * 60 * 1000 | ||
66 | - }, | ||
67 | - { | ||
68 | - name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'), | ||
69 | - value: 30 * 60 * 1000 | ||
70 | - }, | ||
71 | - { | ||
72 | - name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'), | ||
73 | - value: 60 * 60 * 1000 | ||
74 | - }, | ||
75 | - { | ||
76 | - name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'), | ||
77 | - value: 2 * 60 * 60 * 1000 | ||
78 | - }, | ||
79 | - { | ||
80 | - name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'), | ||
81 | - value: 10 * 60 * 60 * 1000 | ||
82 | - }, | ||
83 | - { | ||
84 | - name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'), | ||
85 | - value: 24 * 60 * 60 * 1000 | ||
86 | - }, | ||
87 | - { | ||
88 | - name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'), | ||
89 | - value: 7 * 24 * 60 * 60 * 1000 | ||
90 | - }, | ||
91 | - { | ||
92 | - name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'), | ||
93 | - value: 30 * 24 * 60 * 60 * 1000 | ||
94 | - } | ||
95 | - ]; | 42 | + scope.advanced = false; |
43 | + | ||
44 | + scope.boundInterval = function() { | ||
45 | + var min = timeService.boundMinInterval(scope.min); | ||
46 | + var max = timeService.boundMaxInterval(scope.max); | ||
47 | + scope.intervals = timeService.getIntervals(scope.min, scope.max); | ||
48 | + if (scope.rendered) { | ||
49 | + var newIntervalMs = ngModelCtrl.$viewValue; | ||
50 | + if (newIntervalMs < min) { | ||
51 | + newIntervalMs = min; | ||
52 | + } else if (newIntervalMs > max) { | ||
53 | + newIntervalMs = max; | ||
54 | + } | ||
55 | + if (!scope.advanced) { | ||
56 | + newIntervalMs = timeService.boundToPredefinedInterval(min, max, newIntervalMs); | ||
57 | + } | ||
58 | + if (newIntervalMs !== ngModelCtrl.$viewValue) { | ||
59 | + scope.setIntervalMs(newIntervalMs); | ||
60 | + scope.updateView(); | ||
61 | + } | ||
62 | + } | ||
63 | + } | ||
96 | 64 | ||
97 | scope.setIntervalMs = function (intervalMs) { | 65 | scope.setIntervalMs = function (intervalMs) { |
66 | + if (!scope.advanced) { | ||
67 | + scope.intervalMs = intervalMs; | ||
68 | + } | ||
98 | var intervalSeconds = Math.floor(intervalMs / 1000); | 69 | var intervalSeconds = Math.floor(intervalMs / 1000); |
99 | scope.days = Math.floor(intervalSeconds / 86400); | 70 | scope.days = Math.floor(intervalSeconds / 86400); |
100 | scope.hours = Math.floor((intervalSeconds % 86400) / 3600); | 71 | scope.hours = Math.floor((intervalSeconds % 86400) / 3600); |
@@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
105 | ngModelCtrl.$render = function () { | 76 | ngModelCtrl.$render = function () { |
106 | if (ngModelCtrl.$viewValue) { | 77 | if (ngModelCtrl.$viewValue) { |
107 | var intervalMs = ngModelCtrl.$viewValue; | 78 | var intervalMs = ngModelCtrl.$viewValue; |
79 | + if (!scope.rendered) { | ||
80 | + scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs); | ||
81 | + } | ||
108 | scope.setIntervalMs(intervalMs); | 82 | scope.setIntervalMs(intervalMs); |
109 | } | 83 | } |
110 | scope.rendered = true; | 84 | scope.rendered = true; |
@@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
115 | return; | 89 | return; |
116 | } | 90 | } |
117 | var value = null; | 91 | var value = null; |
118 | - var intervalMs = (scope.days * 86400 + | 92 | + var intervalMs; |
93 | + if (!scope.advanced) { | ||
94 | + intervalMs = scope.intervalMs; | ||
95 | + } else { | ||
96 | + intervalMs = (scope.days * 86400 + | ||
119 | scope.hours * 3600 + | 97 | scope.hours * 3600 + |
120 | scope.mins * 60 + | 98 | scope.mins * 60 + |
121 | scope.secs) * 1000; | 99 | scope.secs) * 1000; |
100 | + } | ||
122 | if (!isNaN(intervalMs) && intervalMs > 0) { | 101 | if (!isNaN(intervalMs) && intervalMs > 0) { |
123 | value = intervalMs; | 102 | value = intervalMs; |
124 | ngModelCtrl.$setValidity('tb-timeinterval', true); | 103 | ngModelCtrl.$setValidity('tb-timeinterval', true); |
@@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
126 | ngModelCtrl.$setValidity('tb-timeinterval', !scope.required); | 105 | ngModelCtrl.$setValidity('tb-timeinterval', !scope.required); |
127 | } | 106 | } |
128 | ngModelCtrl.$setViewValue(value); | 107 | ngModelCtrl.$setViewValue(value); |
108 | + scope.boundInterval(); | ||
129 | } | 109 | } |
130 | 110 | ||
131 | scope.$watch('required', function (newRequired, prevRequired) { | 111 | scope.$watch('required', function (newRequired, prevRequired) { |
@@ -134,6 +114,38 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -134,6 +114,38 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
134 | } | 114 | } |
135 | }); | 115 | }); |
136 | 116 | ||
117 | + scope.$watch('min', function (newMin, prevMin) { | ||
118 | + if (angular.isDefined(newMin) && newMin !== prevMin) { | ||
119 | + scope.updateView(); | ||
120 | + } | ||
121 | + }); | ||
122 | + | ||
123 | + scope.$watch('max', function (newMax, prevMax) { | ||
124 | + if (angular.isDefined(newMax) && newMax !== prevMax) { | ||
125 | + scope.updateView(); | ||
126 | + } | ||
127 | + }); | ||
128 | + | ||
129 | + scope.$watch('intervalMs', function (newIntervalMs, prevIntervalMs) { | ||
130 | + if (angular.isDefined(newIntervalMs) && newIntervalMs !== prevIntervalMs) { | ||
131 | + scope.updateView(); | ||
132 | + } | ||
133 | + }); | ||
134 | + | ||
135 | + scope.$watch('advanced', function (newAdvanced, prevAdvanced) { | ||
136 | + if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) { | ||
137 | + if (!scope.advanced) { | ||
138 | + scope.intervalMs = (scope.days * 86400 + | ||
139 | + scope.hours * 3600 + | ||
140 | + scope.mins * 60 + | ||
141 | + scope.secs) * 1000; | ||
142 | + } else { | ||
143 | + scope.setIntervalMs(scope.intervalMs); | ||
144 | + } | ||
145 | + scope.updateView(); | ||
146 | + } | ||
147 | + }); | ||
148 | + | ||
137 | scope.$watch('secs', function (newSecs) { | 149 | scope.$watch('secs', function (newSecs) { |
138 | if (angular.isUndefined(newSecs)) { | 150 | if (angular.isUndefined(newSecs)) { |
139 | return; | 151 | return; |
@@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
198 | scope.updateView(); | 210 | scope.updateView(); |
199 | }); | 211 | }); |
200 | 212 | ||
213 | + scope.boundInterval(); | ||
214 | + | ||
201 | $compile(element.contents())(scope); | 215 | $compile(element.contents())(scope); |
202 | 216 | ||
203 | } | 217 | } |
@@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) { | @@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) { | ||
206 | restrict: "E", | 220 | restrict: "E", |
207 | require: "^ngModel", | 221 | require: "^ngModel", |
208 | scope: { | 222 | scope: { |
209 | - required: '=ngRequired' | 223 | + required: '=ngRequired', |
224 | + min: '=?', | ||
225 | + max: '=?', | ||
226 | + predefinedName: '=?' | ||
210 | }, | 227 | }, |
211 | link: linker | 228 | link: linker |
212 | }; | 229 | }; |
@@ -14,6 +14,7 @@ | @@ -14,6 +14,7 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | tb-timeinterval { | 16 | tb-timeinterval { |
17 | + min-width: 355px; | ||
17 | md-input-container { | 18 | md-input-container { |
18 | margin-bottom: 0px; | 19 | margin-bottom: 0px; |
19 | .md-errors-spacer { | 20 | .md-errors-spacer { |
@@ -25,10 +26,13 @@ tb-timeinterval { | @@ -25,10 +26,13 @@ tb-timeinterval { | ||
25 | width: 150px; | 26 | width: 150px; |
26 | } | 27 | } |
27 | } | 28 | } |
28 | -} | ||
29 | - | ||
30 | -tb-timeinterval { | ||
31 | .md-input { | 29 | .md-input { |
32 | width: 70px !important; | 30 | width: 70px !important; |
33 | } | 31 | } |
32 | + .advanced-switch { | ||
33 | + margin-top: 0; | ||
34 | + } | ||
35 | + .advanced-label { | ||
36 | + margin: 5px 0; | ||
37 | + } | ||
34 | } | 38 | } |
@@ -15,33 +15,41 @@ | @@ -15,33 +15,41 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<section layout="row" layout-align="start start"> | ||
19 | - <md-input-container> | ||
20 | - <label translate>timeinterval.days</label> | ||
21 | - <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}"> | ||
22 | - </md-input-container> | ||
23 | - <md-input-container> | ||
24 | - <label translate>timeinterval.hours</label> | ||
25 | - <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}"> | ||
26 | - </md-input-container> | ||
27 | - <md-input-container> | ||
28 | - <label translate>timeinterval.minutes</label> | ||
29 | - <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}"> | ||
30 | - </md-input-container> | ||
31 | - <md-input-container> | ||
32 | - <label translate>timeinterval.seconds</label> | ||
33 | - <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}"> | ||
34 | - </md-input-container> | ||
35 | - <md-menu md-position-mode="target-right target"> | ||
36 | - <md-button class="md-icon-button" aria-label="Open intervals" ng-click="$mdOpenMenu($event)"> | ||
37 | - <md-icon md-menu-origin aria-label="arrow_drop_down" class="material-icons">arrow_drop_down</md-icon> | ||
38 | - </md-button> | ||
39 | - <md-menu-content width="4"> | ||
40 | - <md-menu-item ng-repeat="interval in predefIntervals" > | ||
41 | - <md-button ng-click="setIntervalMs(interval.value)"> | ||
42 | - <span>{{interval.name}}</span> | ||
43 | - </md-button> | ||
44 | - </md-menu-item> | ||
45 | - </md-menu-content> | ||
46 | - </md-menu> | 18 | +<section layout="row"> |
19 | + <section layout="column" flex ng-show="advanced"> | ||
20 | + <label class="tb-small" translate>{{ predefinedName }}</label> | ||
21 | + <section layout="row" layout-align="start start" flex> | ||
22 | + <md-input-container> | ||
23 | + <label translate>timeinterval.days</label> | ||
24 | + <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}"> | ||
25 | + </md-input-container> | ||
26 | + <md-input-container> | ||
27 | + <label translate>timeinterval.hours</label> | ||
28 | + <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}"> | ||
29 | + </md-input-container> | ||
30 | + <md-input-container> | ||
31 | + <label translate>timeinterval.minutes</label> | ||
32 | + <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}"> | ||
33 | + </md-input-container> | ||
34 | + <md-input-container> | ||
35 | + <label translate>timeinterval.seconds</label> | ||
36 | + <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}"> | ||
37 | + </md-input-container> | ||
38 | + </section> | ||
39 | + </section> | ||
40 | + <section layout="row" flex ng-show="!advanced"> | ||
41 | + <md-input-container flex> | ||
42 | + <label translate>{{ predefinedName }}</label> | ||
43 | + <md-select ng-model="intervalMs" style="min-width: 150px;" aria-label="predefined-interval"> | ||
44 | + <md-option ng-repeat="interval in intervals" ng-value="interval.value"> | ||
45 | + {{interval.name}} | ||
46 | + </md-option> | ||
47 | + </md-select> | ||
48 | + </md-input-container> | ||
49 | + </section> | ||
50 | + <section layout="column" layout-align="center center"> | ||
51 | + <label class="tb-small advanced-label" translate>timeinterval.advanced</label> | ||
52 | + <md-switch class="advanced-switch" ng-model="advanced" aria-label="predefined-switcher"> | ||
53 | + </md-switch> | ||
54 | + </section> | ||
47 | </section> | 55 | </section> |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | /*@ngInject*/ | 16 | /*@ngInject*/ |
17 | -export default function TimewindowPanelController(mdPanelRef, $scope, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) { | 17 | +export default function TimewindowPanelController(mdPanelRef, $scope, timeService, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) { |
18 | 18 | ||
19 | var vm = this; | 19 | var vm = this; |
20 | 20 | ||
@@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim | @@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim | ||
24 | vm.aggregation = aggregation; | 24 | vm.aggregation = aggregation; |
25 | vm.onTimewindowUpdate = onTimewindowUpdate; | 25 | vm.onTimewindowUpdate = onTimewindowUpdate; |
26 | vm.aggregationTypes = types.aggregation; | 26 | vm.aggregationTypes = types.aggregation; |
27 | + vm.showLimit = showLimit; | ||
28 | + vm.showRealtimeAggInterval = showRealtimeAggInterval; | ||
29 | + vm.showHistoryAggInterval = showHistoryAggInterval; | ||
30 | + vm.minRealtimeAggInterval = minRealtimeAggInterval; | ||
31 | + vm.maxRealtimeAggInterval = maxRealtimeAggInterval; | ||
32 | + vm.minHistoryAggInterval = minHistoryAggInterval; | ||
33 | + vm.maxHistoryAggInterval = maxHistoryAggInterval; | ||
27 | 34 | ||
28 | if (vm.historyOnly) { | 35 | if (vm.historyOnly) { |
29 | vm.timewindow.selectedTab = 1; | 36 | vm.timewindow.selectedTab = 1; |
@@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim | @@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim | ||
48 | vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow); | 55 | vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow); |
49 | }); | 56 | }); |
50 | }; | 57 | }; |
58 | + | ||
59 | + function showLimit() { | ||
60 | + return vm.timewindow.aggregation.type === vm.aggregationTypes.none.value; | ||
61 | + } | ||
62 | + | ||
63 | + function showRealtimeAggInterval() { | ||
64 | + return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value && | ||
65 | + vm.timewindow.selectedTab === 0; | ||
66 | + } | ||
67 | + | ||
68 | + function showHistoryAggInterval() { | ||
69 | + return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value && | ||
70 | + vm.timewindow.selectedTab === 1; | ||
71 | + } | ||
72 | + | ||
73 | + function minRealtimeAggInterval () { | ||
74 | + return timeService.minIntervalLimit(vm.timewindow.realtime.timewindowMs); | ||
75 | + } | ||
76 | + | ||
77 | + function maxRealtimeAggInterval () { | ||
78 | + return timeService.maxIntervalLimit(vm.timewindow.realtime.timewindowMs); | ||
79 | + } | ||
80 | + | ||
81 | + function minHistoryAggInterval () { | ||
82 | + return timeService.minIntervalLimit(currentHistoryTimewindow()); | ||
83 | + } | ||
84 | + | ||
85 | + function maxHistoryAggInterval () { | ||
86 | + return timeService.maxIntervalLimit(currentHistoryTimewindow()); | ||
87 | + } | ||
88 | + | ||
89 | + function currentHistoryTimewindow() { | ||
90 | + if (vm.timewindow.history.historyType === 0) { | ||
91 | + return vm.timewindow.history.timewindowMs; | ||
92 | + } else { | ||
93 | + return vm.timewindow.history.fixedTimewindow.endTimeMs - | ||
94 | + vm.timewindow.history.fixedTimewindow.startTimeMs; | ||
95 | + } | ||
96 | + } | ||
97 | + | ||
51 | } | 98 | } |
99 | + |
@@ -17,61 +17,70 @@ | @@ -17,61 +17,70 @@ | ||
17 | --> | 17 | --> |
18 | <form name="theForm" ng-submit="vm.update()"> | 18 | <form name="theForm" ng-submit="vm.update()"> |
19 | <fieldset ng-disabled="loading"> | 19 | <fieldset ng-disabled="loading"> |
20 | - <md-content layout="column"> | ||
21 | - <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom> | ||
22 | - <md-tab label="{{ 'timewindow.realtime' | translate }}"> | ||
23 | - <md-content class="md-padding" layout="column"> | ||
24 | - <span translate>timewindow.last</span> | ||
25 | - <tb-timeinterval | ||
26 | - ng-required="vm.timewindow.selectedTab === 0" | ||
27 | - ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval> | ||
28 | - </md-content> | ||
29 | - </md-tab> | ||
30 | - <md-tab label="{{ 'timewindow.history' | translate }}"> | ||
31 | - <md-content class="md-padding" layout="column"> | ||
32 | - <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary"> | ||
33 | - <md-radio-button ng-value=0 class="md-primary md-align-top-left md-radio-interactive"> | ||
34 | - <section layout="column"> | ||
35 | - <span translate>timewindow.last</span> | ||
36 | - <tb-timeinterval | ||
37 | - ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0" | ||
38 | - ng-show="vm.timewindow.history.historyType === 0" | ||
39 | - ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval> | ||
40 | - </section> | ||
41 | - </md-radio-button> | ||
42 | - <md-radio-button ng-value=1 class="md-primary md-align-top-left md-radio-interactive"> | ||
43 | - <section layout="column"> | ||
44 | - <span translate>timewindow.time-period</span> | ||
45 | - <tb-datetime-period | ||
46 | - ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1" | ||
47 | - ng-show="vm.timewindow.history.historyType === 1" | ||
48 | - ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period> | ||
49 | - </section> | ||
50 | - </md-radio-button> | ||
51 | - </md-radio-group> | ||
52 | - </md-content> | ||
53 | - </md-tab> | ||
54 | - </md-tabs> | ||
55 | - <md-content ng-if="vm.aggregation" class="md-padding" layout="column"> | ||
56 | - <md-input-container> | ||
57 | - <label translate>aggregation.function</label> | ||
58 | - <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;"> | ||
59 | - <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value"> | ||
60 | - {{type.name | translate}} | ||
61 | - </md-option> | ||
62 | - </md-select> | ||
63 | - </md-input-container> | ||
64 | - <md-slider-container> | ||
65 | - <span translate>aggregation.limit</span> | ||
66 | - <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider"> | ||
67 | - </md-slider> | 20 | + <md-content style="height: 100%" flex layout="column"> |
21 | + <section layout="column"> | ||
22 | + <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom> | ||
23 | + <md-tab label="{{ 'timewindow.realtime' | translate }}"> | ||
24 | + <md-content class="md-padding" layout="column"> | ||
25 | + <tb-timeinterval predefined-name="'timewindow.last'" | ||
26 | + ng-required="vm.timewindow.selectedTab === 0" | ||
27 | + ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval> | ||
28 | + </md-content> | ||
29 | + </md-tab> | ||
30 | + <md-tab label="{{ 'timewindow.history' | translate }}"> | ||
31 | + <md-content class="md-padding" layout="column" style="padding-top: 8px;"> | ||
32 | + <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary"> | ||
33 | + <md-radio-button ng-value=0 aria-label="{{ 'timewindow.last' | translate }}" class="md-primary md-align-top-left md-radio-interactive"> | ||
34 | + <section layout="column"> | ||
35 | + <tb-timeinterval predefined-name="'timewindow.last'" | ||
36 | + ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0" | ||
37 | + ng-show="vm.timewindow.history.historyType === 0" | ||
38 | + ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval> | ||
39 | + </section> | ||
40 | + </md-radio-button> | ||
41 | + <md-radio-button ng-value=1 aria-label="{{ 'timewindow.time-period' | translate }}" class="md-primary md-align-top-left md-radio-interactive"> | ||
42 | + <section layout="column"> | ||
43 | + <span translate>timewindow.time-period</span> | ||
44 | + <tb-datetime-period | ||
45 | + ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1" | ||
46 | + ng-show="vm.timewindow.history.historyType === 1" | ||
47 | + ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period> | ||
48 | + </section> | ||
49 | + </md-radio-button> | ||
50 | + </md-radio-group> | ||
51 | + </md-content> | ||
52 | + </md-tab> | ||
53 | + </md-tabs> | ||
54 | + <md-content ng-if="vm.aggregation" class="md-padding" layout="column"> | ||
68 | <md-input-container> | 55 | <md-input-container> |
69 | - <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider"> | 56 | + <label translate>aggregation.function</label> |
57 | + <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;"> | ||
58 | + <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value"> | ||
59 | + {{type.name | translate}} | ||
60 | + </md-option> | ||
61 | + </md-select> | ||
70 | </md-input-container> | 62 | </md-input-container> |
71 | - </md-slider-container> | ||
72 | - </md-content> | 63 | + <md-slider-container ng-show="vm.showLimit()"> |
64 | + <span translate>aggregation.limit</span> | ||
65 | + <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider"> | ||
66 | + </md-slider> | ||
67 | + <md-input-container> | ||
68 | + <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider"> | ||
69 | + </md-input-container> | ||
70 | + </md-slider-container> | ||
71 | + <tb-timeinterval ng-show="vm.showRealtimeAggInterval()" min="vm.minRealtimeAggInterval()" max="vm.maxRealtimeAggInterval()" | ||
72 | + predefined-name="'aggregation.group-interval'" | ||
73 | + ng-model="vm.timewindow.realtime.interval"> | ||
74 | + </tb-timeinterval> | ||
75 | + <tb-timeinterval ng-show="vm.showHistoryAggInterval()" min="vm.minHistoryAggInterval()" max="vm.maxHistoryAggInterval()" | ||
76 | + predefined-name="'aggregation.group-interval'" | ||
77 | + ng-model="vm.timewindow.history.interval"> | ||
78 | + </tb-timeinterval> | ||
79 | + </md-content> | ||
80 | + </section> | ||
81 | + <span flex></span> | ||
73 | <section layout="row" layout-alignment="start center"> | 82 | <section layout="row" layout-alignment="start center"> |
74 | - <span flex></span> | 83 | + <span flex></span> |
75 | <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary"> | 84 | <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary"> |
76 | {{ 'action.update' | translate }} | 85 | {{ 'action.update' | translate }} |
77 | </md-button> | 86 | </md-button> |
@@ -79,6 +88,6 @@ | @@ -79,6 +88,6 @@ | ||
79 | {{ 'action.cancel' | translate }} | 88 | {{ 'action.cancel' | translate }} |
80 | </md-button> | 89 | </md-button> |
81 | </section> | 90 | </section> |
82 | - </section> | 91 | + </md-content> |
83 | </fieldset> | 92 | </fieldset> |
84 | </form> | 93 | </form> |
@@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT | @@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT | ||
37 | 37 | ||
38 | /* eslint-disable angular/angularelement */ | 38 | /* eslint-disable angular/angularelement */ |
39 | /*@ngInject*/ | 39 | /*@ngInject*/ |
40 | -function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, types) { | 40 | +function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, timeService) { |
41 | 41 | ||
42 | var linker = function (scope, element, attrs, ngModelCtrl) { | 42 | var linker = function (scope, element, attrs, ngModelCtrl) { |
43 | 43 | ||
44 | /* tbTimewindow (ng-model) | 44 | /* tbTimewindow (ng-model) |
45 | * { | 45 | * { |
46 | * realtime: { | 46 | * realtime: { |
47 | + * interval: 0, | ||
47 | * timewindowMs: 0 | 48 | * timewindowMs: 0 |
48 | * }, | 49 | * }, |
49 | * history: { | 50 | * history: { |
51 | + * interval: 0, | ||
50 | * timewindowMs: 0, | 52 | * timewindowMs: 0, |
51 | * fixedTimewindow: { | 53 | * fixedTimewindow: { |
52 | * startTimeMs: 0, | 54 | * startTimeMs: 0, |
@@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | @@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | ||
54 | * } | 56 | * } |
55 | * }, | 57 | * }, |
56 | * aggregation: { | 58 | * aggregation: { |
57 | - * limit: 200, | ||
58 | - * type: types.aggregation.avg.value | 59 | + * type: types.aggregation.avg.value, |
60 | + * limit: 200 | ||
59 | * } | 61 | * } |
60 | * } | 62 | * } |
61 | */ | 63 | */ |
@@ -81,16 +83,6 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | @@ -81,16 +83,6 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | ||
81 | } | 83 | } |
82 | element.html(template); | 84 | element.html(template); |
83 | 85 | ||
84 | - scope.isHovered = false; | ||
85 | - | ||
86 | - scope.onHoverIn = function () { | ||
87 | - scope.isHovered = true; | ||
88 | - } | ||
89 | - | ||
90 | - scope.onHoverOut = function () { | ||
91 | - scope.isHovered = false; | ||
92 | - } | ||
93 | - | ||
94 | scope.openEditMode = function (event) { | 86 | scope.openEditMode = function (event) { |
95 | var position; | 87 | var position; |
96 | var isGtSm = $mdMedia('gt-sm'); | 88 | var isGtSm = $mdMedia('gt-sm'); |
@@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | @@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | ||
143 | var model = scope.model; | 135 | var model = scope.model; |
144 | if (model.selectedTab === 0) { | 136 | if (model.selectedTab === 0) { |
145 | value.realtime = { | 137 | value.realtime = { |
138 | + interval: model.realtime.interval, | ||
146 | timewindowMs: model.realtime.timewindowMs | 139 | timewindowMs: model.realtime.timewindowMs |
147 | }; | 140 | }; |
148 | } else { | 141 | } else { |
149 | if (model.history.historyType === 0) { | 142 | if (model.history.historyType === 0) { |
150 | value.history = { | 143 | value.history = { |
144 | + interval: model.history.interval, | ||
151 | timewindowMs: model.history.timewindowMs | 145 | timewindowMs: model.history.timewindowMs |
152 | }; | 146 | }; |
153 | } else { | 147 | } else { |
154 | value.history = { | 148 | value.history = { |
149 | + interval: model.history.interval, | ||
155 | fixedTimewindow: { | 150 | fixedTimewindow: { |
156 | startTimeMs: model.history.fixedTimewindow.startTimeMs, | 151 | startTimeMs: model.history.fixedTimewindow.startTimeMs, |
157 | endTimeMs: model.history.fixedTimewindow.endTimeMs | 152 | endTimeMs: model.history.fixedTimewindow.endTimeMs |
@@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | @@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | ||
160 | } | 155 | } |
161 | } | 156 | } |
162 | value.aggregation = { | 157 | value.aggregation = { |
163 | - limit: model.aggregation.limit, | ||
164 | - type: model.aggregation.type | 158 | + type: model.aggregation.type, |
159 | + limit: model.aggregation.limit | ||
165 | }; | 160 | }; |
166 | ngModelCtrl.$setViewValue(value); | 161 | ngModelCtrl.$setViewValue(value); |
167 | scope.updateDisplayValue(); | 162 | scope.updateDisplayValue(); |
@@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | @@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | ||
190 | } | 185 | } |
191 | 186 | ||
192 | ngModelCtrl.$render = function () { | 187 | ngModelCtrl.$render = function () { |
193 | - var currentTime = (new Date).getTime(); | ||
194 | - scope.model = { | ||
195 | - displayValue: "", | ||
196 | - selectedTab: 0, | ||
197 | - realtime: { | ||
198 | - timewindowMs: 60000 // 1 min by default | ||
199 | - }, | ||
200 | - history: { | ||
201 | - historyType: 0, | ||
202 | - timewindowMs: 60000, // 1 min by default | ||
203 | - fixedTimewindow: { | ||
204 | - startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default | ||
205 | - endTimeMs: currentTime | ||
206 | - } | ||
207 | - }, | ||
208 | - aggregation: { | ||
209 | - limit: 200, | ||
210 | - type: types.aggregation.avg.value | ||
211 | - } | ||
212 | - }; | 188 | + scope.model = timeService.defaultTimewindow(); |
213 | if (ngModelCtrl.$viewValue) { | 189 | if (ngModelCtrl.$viewValue) { |
214 | var value = ngModelCtrl.$viewValue; | 190 | var value = ngModelCtrl.$viewValue; |
215 | var model = scope.model; | 191 | var model = scope.model; |
216 | if (angular.isDefined(value.realtime)) { | 192 | if (angular.isDefined(value.realtime)) { |
217 | model.selectedTab = 0; | 193 | model.selectedTab = 0; |
194 | + model.realtime.interval = value.realtime.interval; | ||
218 | model.realtime.timewindowMs = value.realtime.timewindowMs; | 195 | model.realtime.timewindowMs = value.realtime.timewindowMs; |
219 | } else { | 196 | } else { |
220 | model.selectedTab = 1; | 197 | model.selectedTab = 1; |
198 | + model.history.interval = value.history.interval; | ||
221 | if (angular.isDefined(value.history.timewindowMs)) { | 199 | if (angular.isDefined(value.history.timewindowMs)) { |
222 | model.history.historyType = 0; | 200 | model.history.historyType = 0; |
223 | model.history.timewindowMs = value.history.timewindowMs; | 201 | model.history.timewindowMs = value.history.timewindowMs; |
@@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | @@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM | ||
228 | } | 206 | } |
229 | } | 207 | } |
230 | if (angular.isDefined(value.aggregation)) { | 208 | if (angular.isDefined(value.aggregation)) { |
231 | - model.aggregation.limit = value.aggregation.limit || 200; | ||
232 | if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) { | 209 | if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) { |
233 | model.aggregation.type = value.aggregation.type; | 210 | model.aggregation.type = value.aggregation.type; |
234 | } | 211 | } |
212 | + model.aggregation.limit = value.aggregation.limit || timeService.avgAggregationLimit(); | ||
235 | } | 213 | } |
236 | } | 214 | } |
237 | scope.updateDisplayValue(); | 215 | scope.updateDisplayValue(); |
@@ -21,14 +21,39 @@ | @@ -21,14 +21,39 @@ | ||
21 | } | 21 | } |
22 | 22 | ||
23 | .tb-timewindow-panel { | 23 | .tb-timewindow-panel { |
24 | - min-height: 375px; | 24 | + max-height: 440px; |
25 | + min-width: 417px; | ||
25 | background: white; | 26 | background: white; |
26 | border-radius: 4px; | 27 | border-radius: 4px; |
27 | box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), | 28 | box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), |
28 | 0 13px 19px 2px rgba(0, 0, 0, 0.14), | 29 | 0 13px 19px 2px rgba(0, 0, 0, 0.14), |
29 | 0 5px 24px 4px rgba(0, 0, 0, 0.12); | 30 | 0 5px 24px 4px rgba(0, 0, 0, 0.12); |
30 | overflow: hidden; | 31 | overflow: hidden; |
32 | + form, fieldset { | ||
33 | + height: 100%; | ||
34 | + } | ||
31 | md-content { | 35 | md-content { |
32 | background-color: #fff; | 36 | background-color: #fff; |
37 | + overflow: hidden; | ||
38 | + } | ||
39 | + .md-padding { | ||
40 | + padding: 0 16px; | ||
41 | + } | ||
42 | + .md-radio-interactive { | ||
43 | + md-select, md-switch { | ||
44 | + pointer-events: all; | ||
45 | + } | ||
46 | + } | ||
47 | + md-radio-button { | ||
48 | + .md-label { | ||
49 | + width: 100%; | ||
50 | + } | ||
51 | + tb-timeinterval { | ||
52 | + width: 355px; | ||
53 | + .advanced-switch { | ||
54 | + min-height: 30px; | ||
55 | + max-width: 44px; | ||
56 | + } | ||
57 | + } | ||
33 | } | 58 | } |
34 | } | 59 | } |
@@ -15,9 +15,9 @@ | @@ -15,9 +15,9 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<section ng-mouseover="onHoverIn()" ng-mouseleave="onHoverOut()" layout='row' layout-align="start center" style="min-height: 32px;"> | 18 | +<section layout='row' layout-align="start center" style="min-height: 32px;"> |
19 | <span ng-click="openEditMode($event)">{{model.displayValue}}</span> | 19 | <span ng-click="openEditMode($event)">{{model.displayValue}}</span> |
20 | - <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-show="isHovered" ng-click="openEditMode($event)"> | 20 | + <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)"> |
21 | <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon> | 21 | <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon> |
22 | </md-button> | 22 | </md-button> |
23 | </section> | 23 | </section> |
@@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; | @@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; | ||
19 | /* eslint-disable angular/angularelement */ | 19 | /* eslint-disable angular/angularelement */ |
20 | 20 | ||
21 | /*@ngInject*/ | 21 | /*@ngInject*/ |
22 | -export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, | 22 | +export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, timeService, |
23 | datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) { | 23 | datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) { |
24 | 24 | ||
25 | var vm = this; | 25 | var vm = this; |
@@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | @@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | ||
41 | var targetDeviceAliasId = null; | 41 | var targetDeviceAliasId = null; |
42 | var targetDeviceId = null; | 42 | var targetDeviceId = null; |
43 | var originalTimewindow = null; | 43 | var originalTimewindow = null; |
44 | - var subscriptionTimewindow = { | ||
45 | - fixedWindow: null, | ||
46 | - realtimeWindowMs: null, | ||
47 | - aggregation: null | ||
48 | - }; | 44 | + var subscriptionTimewindow = null; |
49 | var dataUpdateCaf = null; | 45 | var dataUpdateCaf = null; |
50 | 46 | ||
51 | /* | 47 | /* |
@@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | @@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | ||
488 | if (!originalTimewindow) { | 484 | if (!originalTimewindow) { |
489 | originalTimewindow = angular.copy(widget.config.timewindow); | 485 | originalTimewindow = angular.copy(widget.config.timewindow); |
490 | } | 486 | } |
491 | - widget.config.timewindow = { | ||
492 | - history: { | ||
493 | - fixedTimewindow: { | ||
494 | - startTimeMs: startTimeMs, | ||
495 | - endTimeMs: endTimeMs | ||
496 | - } | ||
497 | - }, | ||
498 | - aggregation: angular.copy(widget.config.timewindow.aggregation) | ||
499 | - }; | 487 | + widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs); |
500 | } | 488 | } |
501 | 489 | ||
502 | function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) { | 490 | function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) { |
@@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | @@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | ||
511 | } | 499 | } |
512 | } | 500 | } |
513 | if (update) { | 501 | if (update) { |
514 | - if (subscriptionTimewindow.realtimeWindowMs) { | 502 | + if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) { |
515 | updateTimewindow(); | 503 | updateTimewindow(); |
516 | } | 504 | } |
517 | widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data; | 505 | widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data; |
@@ -555,62 +543,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | @@ -555,62 +543,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | ||
555 | } | 543 | } |
556 | } | 544 | } |
557 | 545 | ||
546 | + function updateRealtimeSubscription(_subscriptionTimewindow) { | ||
547 | + if (_subscriptionTimewindow) { | ||
548 | + subscriptionTimewindow = _subscriptionTimewindow; | ||
549 | + } else { | ||
550 | + subscriptionTimewindow = timeService.createSubscriptionTimewindow(widget.config.timewindow, widgetContext.timeWindow.stDiff); | ||
551 | + } | ||
552 | + updateTimewindow(); | ||
553 | + return subscriptionTimewindow; | ||
554 | + } | ||
555 | + | ||
558 | function subscribe() { | 556 | function subscribe() { |
559 | if (widget.type !== types.widgetType.rpc.value) { | 557 | if (widget.type !== types.widgetType.rpc.value) { |
560 | - var index = 0; | ||
561 | - subscriptionTimewindow.fixedWindow = null; | ||
562 | - subscriptionTimewindow.realtimeWindowMs = null; | ||
563 | - subscriptionTimewindow.aggregation = { | ||
564 | - limit: 200, | ||
565 | - type: types.aggregation.avg.value | ||
566 | - }; | ||
567 | if (widget.type === types.widgetType.timeseries.value && | 558 | if (widget.type === types.widgetType.timeseries.value && |
568 | angular.isDefined(widget.config.timewindow)) { | 559 | angular.isDefined(widget.config.timewindow)) { |
569 | - var timeWindow = 0; | ||
570 | - if (angular.isDefined(widget.config.timewindow.aggregation)) { | ||
571 | - subscriptionTimewindow.aggregation = { | ||
572 | - limit: widget.config.timewindow.aggregation.limit || 200, | ||
573 | - type: widget.config.timewindow.aggregation.type || types.aggregation.avg.value | ||
574 | - }; | ||
575 | - } | ||
576 | - | ||
577 | - if (angular.isDefined(widget.config.timewindow.realtime)) { | ||
578 | - subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs; | ||
579 | - subscriptionTimewindow.startTs = (new Date).getTime() + widgetContext.timeWindow.stDiff - subscriptionTimewindow.realtimeWindowMs; | ||
580 | - timeWindow = subscriptionTimewindow.realtimeWindowMs; | ||
581 | - } else if (angular.isDefined(widget.config.timewindow.history)) { | ||
582 | - if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) { | ||
583 | - var currentTime = (new Date).getTime(); | ||
584 | - subscriptionTimewindow.fixedWindow = { | ||
585 | - startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs, | ||
586 | - endTimeMs: currentTime | ||
587 | - } | ||
588 | - timeWindow = widget.config.timewindow.history.timewindowMs; | ||
589 | - } else { | ||
590 | - subscriptionTimewindow.fixedWindow = { | ||
591 | - startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs, | ||
592 | - endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs | ||
593 | - } | ||
594 | - timeWindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; | ||
595 | - } | ||
596 | - subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; | ||
597 | - } | ||
598 | - var aggregation = subscriptionTimewindow.aggregation; | ||
599 | - var noAggregation = aggregation.type === types.aggregation.none.value; | ||
600 | - var interval = Math.floor(timeWindow / aggregation.limit); | ||
601 | - if (!noAggregation) { | ||
602 | - aggregation.interval = Math.max(interval, 1000); | ||
603 | - aggregation.limit = Math.ceil(interval/aggregation.interval * aggregation.limit); | ||
604 | - aggregation.timeWindow = aggregation.interval * aggregation.limit; | ||
605 | - } else { | ||
606 | - aggregation.timeWindow = interval * aggregation.limit; | ||
607 | - aggregation.interval = 1000; | ||
608 | - } | ||
609 | - updateTimewindow(); | 560 | + updateRealtimeSubscription(); |
610 | if (subscriptionTimewindow.fixedWindow) { | 561 | if (subscriptionTimewindow.fixedWindow) { |
611 | onDataUpdated(); | 562 | onDataUpdated(); |
612 | } | 563 | } |
613 | } | 564 | } |
565 | + var index = 0; | ||
614 | for (var i in widget.config.datasources) { | 566 | for (var i in widget.config.datasources) { |
615 | var datasource = widget.config.datasources[i]; | 567 | var datasource = widget.config.datasources[i]; |
616 | var deviceId = null; | 568 | var deviceId = null; |
@@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | @@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q | ||
630 | dataUpdated: function (data, datasourceIndex, dataKeyIndex) { | 582 | dataUpdated: function (data, datasourceIndex, dataKeyIndex) { |
631 | dataUpdated(data, datasourceIndex, dataKeyIndex); | 583 | dataUpdated(data, datasourceIndex, dataKeyIndex); |
632 | }, | 584 | }, |
585 | + updateRealtimeSubscription: function() { | ||
586 | + this.subscriptionTimewindow = updateRealtimeSubscription(); | ||
587 | + return this.subscriptionTimewindow; | ||
588 | + }, | ||
589 | + setRealtimeSubscription: function(subscriptionTimewindow) { | ||
590 | + updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); | ||
591 | + }, | ||
633 | datasourceIndex: index | 592 | datasourceIndex: index |
634 | }; | 593 | }; |
635 | 594 |
@@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller'; | @@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller'; | ||
29 | 29 | ||
30 | /*@ngInject*/ | 30 | /*@ngInject*/ |
31 | export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, | 31 | export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, |
32 | - $document, $translate, utils, types, dashboardService, deviceService, widgetService) { | 32 | + $document, $translate, $filter, utils, types, dashboardService, deviceService, widgetService) { |
33 | 33 | ||
34 | var linker = function (scope, element, attrs) { | 34 | var linker = function (scope, element, attrs) { |
35 | 35 | ||
@@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS | @@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS | ||
303 | var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid; | 303 | var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid; |
304 | widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( | 304 | widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( |
305 | function success(widgetTypes) { | 305 | function success(widgetTypes) { |
306 | + | ||
307 | + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']); | ||
308 | + | ||
306 | for (var i = 0; i < widgetTypes.length; i++) { | 309 | for (var i = 0; i < widgetTypes.length; i++) { |
307 | var widgetType = widgetTypes[i]; | 310 | var widgetType = widgetTypes[i]; |
308 | var widgetInfo = widgetService.toWidgetInfo(widgetType); | 311 | var widgetInfo = widgetService.toWidgetInfo(widgetType); |
@@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', []) | @@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', []) | ||
67 | "aggregation": "Aggregation", | 67 | "aggregation": "Aggregation", |
68 | "function": "Data aggregation function", | 68 | "function": "Data aggregation function", |
69 | "limit": "Max values", | 69 | "limit": "Max values", |
70 | + "group-interval": "Grouping interval", | ||
70 | "min": "Min", | 71 | "min": "Min", |
71 | "max": "Max", | 72 | "max": "Max", |
72 | "avg": "Average", | 73 | "avg": "Average", |
@@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', []) | @@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', []) | ||
558 | "days": "Days", | 559 | "days": "Days", |
559 | "hours": "Hours", | 560 | "hours": "Hours", |
560 | "minutes": "Minutes", | 561 | "minutes": "Minutes", |
561 | - "seconds": "Seconds" | 562 | + "seconds": "Seconds", |
563 | + "advanced": "Advanced" | ||
562 | }, | 564 | }, |
563 | "timewindow": { | 565 | "timewindow": { |
564 | "days": "{ days, select, 1 { day } other {# days } }", | 566 | "days": "{ days, select, 1 { day } other {# days } }", |
@@ -167,6 +167,7 @@ export default class TbFlot { | @@ -167,6 +167,7 @@ export default class TbFlot { | ||
167 | var settings = ctx.settings; | 167 | var settings = ctx.settings; |
168 | ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1; | 168 | ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1; |
169 | ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false); | 169 | ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false); |
170 | + ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false; | ||
170 | 171 | ||
171 | var font = { | 172 | var font = { |
172 | color: settings.fontColor || "#545454", | 173 | color: settings.fontColor || "#545454", |
@@ -232,6 +233,21 @@ export default class TbFlot { | @@ -232,6 +233,21 @@ export default class TbFlot { | ||
232 | options.yaxis.tickFormatter = function() { | 233 | options.yaxis.tickFormatter = function() { |
233 | return ''; | 234 | return ''; |
234 | }; | 235 | }; |
236 | + } else if (settings.units && settings.units.length > 0) { | ||
237 | + options.yaxis.tickFormatter = function(value, axis) { | ||
238 | + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1, | ||
239 | + formatted = "" + Math.round(value * factor) / factor; | ||
240 | + if (axis.tickDecimals != null) { | ||
241 | + var decimal = formatted.indexOf("."), | ||
242 | + precision = decimal === -1 ? 0 : formatted.length - decimal - 1; | ||
243 | + | ||
244 | + if (precision < axis.tickDecimals) { | ||
245 | + formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); | ||
246 | + } | ||
247 | + } | ||
248 | + formatted += ' ' + tbFlot.ctx.settings.units; | ||
249 | + return formatted; | ||
250 | + }; | ||
235 | } | 251 | } |
236 | options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color; | 252 | options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color; |
237 | options.yaxis.label = settings.yaxis.title || null; | 253 | options.yaxis.label = settings.yaxis.title || null; |
@@ -323,6 +339,8 @@ export default class TbFlot { | @@ -323,6 +339,8 @@ export default class TbFlot { | ||
323 | 339 | ||
324 | this.options = options; | 340 | this.options = options; |
325 | 341 | ||
342 | + this.checkMouseEvents(); | ||
343 | + | ||
326 | if (this.chartType === 'pie' && this.ctx.animatedPie) { | 344 | if (this.chartType === 'pie' && this.ctx.animatedPie) { |
327 | this.ctx.pieDataAnimationDuration = 250; | 345 | this.ctx.pieDataAnimationDuration = 250; |
328 | this.ctx.pieData = angular.copy(this.ctx.data); | 346 | this.ctx.pieData = angular.copy(this.ctx.data); |
@@ -337,7 +355,6 @@ export default class TbFlot { | @@ -337,7 +355,6 @@ export default class TbFlot { | ||
337 | } else { | 355 | } else { |
338 | this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); | 356 | this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); |
339 | } | 357 | } |
340 | - this.checkMouseEvents(); | ||
341 | } | 358 | } |
342 | 359 | ||
343 | update() { | 360 | update() { |
@@ -577,6 +594,11 @@ export default class TbFlot { | @@ -577,6 +594,11 @@ export default class TbFlot { | ||
577 | "type": "boolean", | 594 | "type": "boolean", |
578 | "default": false | 595 | "default": false |
579 | }, | 596 | }, |
597 | + "tooltipCumulative": { | ||
598 | + "title": "Show cumulative values in stacking mode", | ||
599 | + "type": "boolean", | ||
600 | + "default": false | ||
601 | + }, | ||
580 | "grid": { | 602 | "grid": { |
581 | "title": "Grid settings", | 603 | "title": "Grid settings", |
582 | "type": "object", | 604 | "type": "object", |
@@ -710,6 +732,7 @@ export default class TbFlot { | @@ -710,6 +732,7 @@ export default class TbFlot { | ||
710 | "decimals", | 732 | "decimals", |
711 | "units", | 733 | "units", |
712 | "tooltipIndividual", | 734 | "tooltipIndividual", |
735 | + "tooltipCumulative", | ||
713 | { | 736 | { |
714 | "key": "grid", | 737 | "key": "grid", |
715 | "items": [ | 738 | "items": [ |
@@ -834,10 +857,28 @@ export default class TbFlot { | @@ -834,10 +857,28 @@ export default class TbFlot { | ||
834 | } | 857 | } |
835 | 858 | ||
836 | checkMouseEvents() { | 859 | checkMouseEvents() { |
837 | - if (this.ctx.isMobile || this.ctx.isEdit) { | ||
838 | - this.disableMouseEvents(); | ||
839 | - } else if (!this.ctx.isEdit) { | ||
840 | - this.enableMouseEvents(); | 860 | + var enabled = !this.ctx.isMobile && !this.ctx.isEdit; |
861 | + if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) { | ||
862 | + this.mouseEventsEnabled = enabled; | ||
863 | + if (enabled) { | ||
864 | + this.enableMouseEvents(); | ||
865 | + } else { | ||
866 | + this.disableMouseEvents(); | ||
867 | + } | ||
868 | + if (this.ctx.plot) { | ||
869 | + this.ctx.plot.destroy(); | ||
870 | + if (this.chartType === 'pie' && this.ctx.animatedPie) { | ||
871 | + this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options); | ||
872 | + } else { | ||
873 | + this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); | ||
874 | + } | ||
875 | + } | ||
876 | + } | ||
877 | + } | ||
878 | + | ||
879 | + destroy() { | ||
880 | + if (this.ctx.plot) { | ||
881 | + this.ctx.plot.destroy(); | ||
841 | } | 882 | } |
842 | } | 883 | } |
843 | 884 | ||
@@ -1030,7 +1071,7 @@ export default class TbFlot { | @@ -1030,7 +1071,7 @@ export default class TbFlot { | ||
1030 | minTime = pointTime; | 1071 | minTime = pointTime; |
1031 | } | 1072 | } |
1032 | if (series.stack) { | 1073 | if (series.stack) { |
1033 | - if (this.ctx.tooltipIndividual) { | 1074 | + if (this.ctx.tooltipIndividual || !this.ctx.tooltipCumulative) { |
1034 | value = series.data[hoverIndex][1]; | 1075 | value = series.data[hoverIndex][1]; |
1035 | } else { | 1076 | } else { |
1036 | last_value += series.data[hoverIndex][1]; | 1077 | last_value += series.data[hoverIndex][1]; |
@@ -201,6 +201,14 @@ md-sidenav { | @@ -201,6 +201,14 @@ md-sidenav { | ||
201 | color: rgba(0,0,0,0.54); | 201 | color: rgba(0,0,0,0.54); |
202 | } | 202 | } |
203 | 203 | ||
204 | +label { | ||
205 | + &.tb-small { | ||
206 | + pointer-events: none; | ||
207 | + color: rgba(0,0,0,0.54); | ||
208 | + font-size: 12px; | ||
209 | + } | ||
210 | +} | ||
211 | + | ||
204 | /*********************** | 212 | /*********************** |
205 | * Prompt | 213 | * Prompt |
206 | ***********************/ | 214 | ***********************/ |