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 | 23 | private final String key; |
24 | 24 | private final long startTs; |
25 | 25 | private final long endTs; |
26 | + private final long interval; | |
26 | 27 | private final int limit; |
27 | 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 | 31 | this.key = key; |
31 | 32 | this.startTs = startTs; |
32 | 33 | this.endTs = endTs; |
34 | + this.interval = interval; | |
33 | 35 | this.limit = limit; |
34 | 36 | this.aggregation = aggregation; |
35 | 37 | } |
36 | 38 | |
37 | 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 | 112 | if (query.getAggregation() == Aggregation.NONE) { |
113 | 113 | return findAllAsyncWithLimit(entityType, entityId, query); |
114 | 114 | } else { |
115 | - long step = Math.max((query.getEndTs() - query.getStartTs()) / query.getLimit(), minAggregationStepMs); | |
115 | + long step = Math.max(query.getInterval(), minAggregationStepMs); | |
116 | 116 | long stepTs = query.getStartTs(); |
117 | 117 | List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); |
118 | 118 | while (stepTs < query.getEndTs()) { |
119 | 119 | long startTs = stepTs; |
120 | 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 | 122 | futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); |
123 | 123 | stepTs = endTs; |
124 | 124 | } | ... | ... |
... | ... | @@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map', |
272 | 272 | |
273 | 273 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
274 | 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 | 276 | 'Pie - Flot' ); |
277 | 277 | |
278 | 278 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
279 | 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 | 281 | 'Timeseries Bars - Flot' ); |
282 | 282 | |
283 | 283 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
284 | 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 | 286 | 'Timeseries - Flot' ); |
287 | 287 | |
288 | 288 | /** System plugins and rules **/ | ... | ... |
... | ... | @@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { |
115 | 115 | entries.add(save(deviceId, 55000, 600)); |
116 | 116 | |
117 | 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 | 119 | assertEquals(3, list.size()); |
120 | 120 | assertEquals(55000, list.get(0).getTs()); |
121 | 121 | assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue()); |
... | ... | @@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { |
127 | 127 | assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue()); |
128 | 128 | |
129 | 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 | 131 | assertEquals(3, list.size()); |
132 | 132 | assertEquals(10000, list.get(0).getTs()); |
133 | 133 | assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue()); |
... | ... | @@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { |
139 | 139 | assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue()); |
140 | 140 | |
141 | 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 | 144 | assertEquals(3, list.size()); |
145 | 145 | assertEquals(10000, list.get(0).getTs()); |
... | ... | @@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { |
152 | 152 | assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue()); |
153 | 153 | |
154 | 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 | 157 | assertEquals(3, list.size()); |
158 | 158 | assertEquals(10000, list.get(0).getTs()); |
... | ... | @@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { |
165 | 165 | assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue()); |
166 | 166 | |
167 | 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 | 170 | assertEquals(3, list.size()); |
171 | 171 | assertEquals(10000, list.get(0).getTs()); |
... | ... | @@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { |
178 | 178 | assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue()); |
179 | 179 | |
180 | 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 | 183 | assertEquals(3, list.size()); |
184 | 184 | assertEquals(10000, list.get(0).getTs()); | ... | ... |
... | ... | @@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { |
89 | 89 | String keysStr = request.getParameter("keys"); |
90 | 90 | Optional<Long> startTs = request.getLongParamValue("startTs"); |
91 | 91 | Optional<Long> endTs = request.getLongParamValue("endTs"); |
92 | + Optional<Long> interval = request.getLongParamValue("interval"); | |
92 | 93 | Optional<Integer> limit = request.getIntParamValue("limit"); |
93 | 94 | Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); |
94 | 95 | |
95 | 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 | 98 | ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { |
98 | 99 | @Override |
99 | 100 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { | ... | ... |
... | ... | @@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { |
193 | 193 | log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); |
194 | 194 | startTs = cmd.getStartTs(); |
195 | 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 | 197 | ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); |
198 | 198 | } else { |
199 | 199 | List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); |
... | ... | @@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { |
277 | 277 | } |
278 | 278 | DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); |
279 | 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 | 281 | ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { |
282 | 282 | @Override |
283 | 283 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { | ... | ... |
... | ... | @@ -25,11 +25,12 @@ export default class DataAggregator { |
25 | 25 | this.$timeout = $timeout; |
26 | 26 | this.$filter = $filter; |
27 | 27 | this.dataReceived = false; |
28 | + this.resetPending = false; | |
28 | 29 | this.noAggregation = aggregationType === types.aggregation.none.value; |
29 | 30 | this.limit = limit; |
30 | 31 | this.timeWindow = timeWindow; |
31 | 32 | this.interval = interval; |
32 | - this.aggregationTimeout = this.interval; | |
33 | + this.aggregationTimeout = Math.max(this.interval, 1000); | |
33 | 34 | switch (aggregationType) { |
34 | 35 | case types.aggregation.min.value: |
35 | 36 | this.aggFunction = min; |
... | ... | @@ -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 | 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 | 89 | if (update) { |
63 | 90 | this.aggregationMap = {}; |
64 | 91 | updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, |
... | ... | @@ -66,19 +93,24 @@ export default class DataAggregator { |
66 | 93 | } else { |
67 | 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 | 100 | } else { |
71 | 101 | updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, |
72 | 102 | this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); |
73 | 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 | 111 | var now = currentTime(); |
81 | - this.elapsed += now - startedTime; | |
112 | + this.elapsed += now - this.intervalScheduledTime; | |
113 | + this.intervalScheduledTime = now; | |
82 | 114 | if (this.intervalTimeoutHandle) { |
83 | 115 | this.$timeout.cancel(this.intervalTimeoutHandle); |
84 | 116 | this.intervalTimeoutHandle = null; |
... | ... | @@ -101,16 +133,11 @@ export default class DataAggregator { |
101 | 133 | var self = this; |
102 | 134 | if (!history) { |
103 | 135 | this.intervalTimeoutHandle = this.$timeout(function() { |
104 | - self.onInterval(now); | |
136 | + self.onInterval(); | |
105 | 137 | }, this.aggregationTimeout, false); |
106 | 138 | } |
107 | 139 | } |
108 | 140 | |
109 | - reset() { | |
110 | - this.destroy(); | |
111 | - this.dataReceived = false; | |
112 | - } | |
113 | - | |
114 | 141 | destroy() { |
115 | 142 | if (this.intervalTimeoutHandle) { |
116 | 143 | this.$timeout.cancel(this.intervalTimeoutHandle); | ... | ... |
... | ... | @@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic |
254 | 254 | keys: tsKeys, |
255 | 255 | startTs: subsTw.fixedWindow.startTimeMs, |
256 | 256 | endTs: subsTw.fixedWindow.endTimeMs, |
257 | + interval: subsTw.aggregation.interval, | |
257 | 258 | limit: subsTw.aggregation.limit, |
258 | 259 | agg: subsTw.aggregation.type |
259 | 260 | }; |
... | ... | @@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic |
266 | 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 | 273 | telemetryWebsocketService.subscribe(subscriber); |
... | ... | @@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic |
287 | 286 | }; |
288 | 287 | |
289 | 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 | 291 | subscriber.onData = function(data) { |
309 | 292 | dataAggregator.onData(data); |
310 | 293 | } |
311 | 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 | 307 | } else { |
316 | - subscriber.onReconnected = function() { | |
317 | - onReconnected(); | |
318 | - } | |
308 | + subscriber.onReconnected = function() {} | |
319 | 309 | subscriber.onData = function(data) { |
320 | 310 | if (data.data) { |
321 | 311 | onData(data.data, types.dataKeyType.timeseries); |
... | ... | @@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic |
344 | 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 | 340 | telemetryWebsocketService.subscribe(subscriber); |
... | ... | @@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic |
384 | 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 | 402 | function unsubscribe() { |
... | ... | @@ -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 | 510 | function isNumeric(val) { |
520 | 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 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ |
129 | 129 | resources: [], |
130 | 130 | templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>', |
131 | 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 | 133 | settingsSchema: '{}\n', |
134 | 134 | dataKeySettingsSchema: '{}\n', |
135 | 135 | defaultConfig: '{\n' + |
... | ... | @@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ |
147 | 147 | resources: [], |
148 | 148 | templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', |
149 | 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 | 151 | settingsSchema: '{}\n', |
152 | 152 | dataKeySettingsSchema: '{}\n', |
153 | 153 | defaultConfig: '{\n' + | ... | ... |
... | ... | @@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service'; |
51 | 51 | import thingsboardRaf from './common/raf.provider'; |
52 | 52 | import thingsboardUtils from './common/utils.service'; |
53 | 53 | import thingsboardTypes from './common/types.constant'; |
54 | +import thingsboardApiTime from './api/time.service'; | |
54 | 55 | import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; |
55 | 56 | import thingsboardHelp from './help/help.directive'; |
56 | 57 | import thingsboardToast from './services/toast'; |
... | ... | @@ -101,6 +102,7 @@ angular.module('thingsboard', [ |
101 | 102 | thingsboardRaf, |
102 | 103 | thingsboardUtils, |
103 | 104 | thingsboardTypes, |
105 | + thingsboardApiTime, | |
104 | 106 | thingsboardKeyboardShortcut, |
105 | 107 | thingsboardHelp, |
106 | 108 | thingsboardToast, | ... | ... |
... | ... | @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', []) |
26 | 26 | .name; |
27 | 27 | |
28 | 28 | /*@ngInject*/ |
29 | -function Timeinterval($compile, $templateCache, $translate) { | |
29 | +function Timeinterval($compile, $templateCache, timeService) { | |
30 | 30 | |
31 | 31 | var linker = function (scope, element, attrs, ngModelCtrl) { |
32 | 32 | |
... | ... | @@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) { |
39 | 39 | scope.mins = 1; |
40 | 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 | 65 | scope.setIntervalMs = function (intervalMs) { |
66 | + if (!scope.advanced) { | |
67 | + scope.intervalMs = intervalMs; | |
68 | + } | |
98 | 69 | var intervalSeconds = Math.floor(intervalMs / 1000); |
99 | 70 | scope.days = Math.floor(intervalSeconds / 86400); |
100 | 71 | scope.hours = Math.floor((intervalSeconds % 86400) / 3600); |
... | ... | @@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) { |
105 | 76 | ngModelCtrl.$render = function () { |
106 | 77 | if (ngModelCtrl.$viewValue) { |
107 | 78 | var intervalMs = ngModelCtrl.$viewValue; |
79 | + if (!scope.rendered) { | |
80 | + scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs); | |
81 | + } | |
108 | 82 | scope.setIntervalMs(intervalMs); |
109 | 83 | } |
110 | 84 | scope.rendered = true; |
... | ... | @@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) { |
115 | 89 | return; |
116 | 90 | } |
117 | 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 | 97 | scope.hours * 3600 + |
120 | 98 | scope.mins * 60 + |
121 | 99 | scope.secs) * 1000; |
100 | + } | |
122 | 101 | if (!isNaN(intervalMs) && intervalMs > 0) { |
123 | 102 | value = intervalMs; |
124 | 103 | ngModelCtrl.$setValidity('tb-timeinterval', true); |
... | ... | @@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) { |
126 | 105 | ngModelCtrl.$setValidity('tb-timeinterval', !scope.required); |
127 | 106 | } |
128 | 107 | ngModelCtrl.$setViewValue(value); |
108 | + scope.boundInterval(); | |
129 | 109 | } |
130 | 110 | |
131 | 111 | scope.$watch('required', function (newRequired, prevRequired) { |
... | ... | @@ -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 | 149 | scope.$watch('secs', function (newSecs) { |
138 | 150 | if (angular.isUndefined(newSecs)) { |
139 | 151 | return; |
... | ... | @@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) { |
198 | 210 | scope.updateView(); |
199 | 211 | }); |
200 | 212 | |
213 | + scope.boundInterval(); | |
214 | + | |
201 | 215 | $compile(element.contents())(scope); |
202 | 216 | |
203 | 217 | } |
... | ... | @@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) { |
206 | 220 | restrict: "E", |
207 | 221 | require: "^ngModel", |
208 | 222 | scope: { |
209 | - required: '=ngRequired' | |
223 | + required: '=ngRequired', | |
224 | + min: '=?', | |
225 | + max: '=?', | |
226 | + predefinedName: '=?' | |
210 | 227 | }, |
211 | 228 | link: linker |
212 | 229 | }; | ... | ... |
... | ... | @@ -14,6 +14,7 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | tb-timeinterval { |
17 | + min-width: 355px; | |
17 | 18 | md-input-container { |
18 | 19 | margin-bottom: 0px; |
19 | 20 | .md-errors-spacer { |
... | ... | @@ -25,10 +26,13 @@ tb-timeinterval { |
25 | 26 | width: 150px; |
26 | 27 | } |
27 | 28 | } |
28 | -} | |
29 | - | |
30 | -tb-timeinterval { | |
31 | 29 | .md-input { |
32 | 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 | 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 | 55 | </section> | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 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 | 19 | var vm = this; |
20 | 20 | |
... | ... | @@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim |
24 | 24 | vm.aggregation = aggregation; |
25 | 25 | vm.onTimewindowUpdate = onTimewindowUpdate; |
26 | 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 | 35 | if (vm.historyOnly) { |
29 | 36 | vm.timewindow.selectedTab = 1; |
... | ... | @@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim |
48 | 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 | 17 | --> |
18 | 18 | <form name="theForm" ng-submit="vm.update()"> |
19 | 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 | 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 | 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 | 82 | <section layout="row" layout-alignment="start center"> |
74 | - <span flex></span> | |
83 | + <span flex></span> | |
75 | 84 | <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary"> |
76 | 85 | {{ 'action.update' | translate }} |
77 | 86 | </md-button> |
... | ... | @@ -79,6 +88,6 @@ |
79 | 88 | {{ 'action.cancel' | translate }} |
80 | 89 | </md-button> |
81 | 90 | </section> |
82 | - </section> | |
91 | + </md-content> | |
83 | 92 | </fieldset> |
84 | 93 | </form> |
\ No newline at end of file | ... | ... |
... | ... | @@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT |
37 | 37 | |
38 | 38 | /* eslint-disable angular/angularelement */ |
39 | 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 | 42 | var linker = function (scope, element, attrs, ngModelCtrl) { |
43 | 43 | |
44 | 44 | /* tbTimewindow (ng-model) |
45 | 45 | * { |
46 | 46 | * realtime: { |
47 | + * interval: 0, | |
47 | 48 | * timewindowMs: 0 |
48 | 49 | * }, |
49 | 50 | * history: { |
51 | + * interval: 0, | |
50 | 52 | * timewindowMs: 0, |
51 | 53 | * fixedTimewindow: { |
52 | 54 | * startTimeMs: 0, |
... | ... | @@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM |
54 | 56 | * } |
55 | 57 | * }, |
56 | 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 | 83 | } |
82 | 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 | 86 | scope.openEditMode = function (event) { |
95 | 87 | var position; |
96 | 88 | var isGtSm = $mdMedia('gt-sm'); |
... | ... | @@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM |
143 | 135 | var model = scope.model; |
144 | 136 | if (model.selectedTab === 0) { |
145 | 137 | value.realtime = { |
138 | + interval: model.realtime.interval, | |
146 | 139 | timewindowMs: model.realtime.timewindowMs |
147 | 140 | }; |
148 | 141 | } else { |
149 | 142 | if (model.history.historyType === 0) { |
150 | 143 | value.history = { |
144 | + interval: model.history.interval, | |
151 | 145 | timewindowMs: model.history.timewindowMs |
152 | 146 | }; |
153 | 147 | } else { |
154 | 148 | value.history = { |
149 | + interval: model.history.interval, | |
155 | 150 | fixedTimewindow: { |
156 | 151 | startTimeMs: model.history.fixedTimewindow.startTimeMs, |
157 | 152 | endTimeMs: model.history.fixedTimewindow.endTimeMs |
... | ... | @@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM |
160 | 155 | } |
161 | 156 | } |
162 | 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 | 161 | ngModelCtrl.$setViewValue(value); |
167 | 162 | scope.updateDisplayValue(); |
... | ... | @@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM |
190 | 185 | } |
191 | 186 | |
192 | 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 | 189 | if (ngModelCtrl.$viewValue) { |
214 | 190 | var value = ngModelCtrl.$viewValue; |
215 | 191 | var model = scope.model; |
216 | 192 | if (angular.isDefined(value.realtime)) { |
217 | 193 | model.selectedTab = 0; |
194 | + model.realtime.interval = value.realtime.interval; | |
218 | 195 | model.realtime.timewindowMs = value.realtime.timewindowMs; |
219 | 196 | } else { |
220 | 197 | model.selectedTab = 1; |
198 | + model.history.interval = value.history.interval; | |
221 | 199 | if (angular.isDefined(value.history.timewindowMs)) { |
222 | 200 | model.history.historyType = 0; |
223 | 201 | model.history.timewindowMs = value.history.timewindowMs; |
... | ... | @@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM |
228 | 206 | } |
229 | 207 | } |
230 | 208 | if (angular.isDefined(value.aggregation)) { |
231 | - model.aggregation.limit = value.aggregation.limit || 200; | |
232 | 209 | if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) { |
233 | 210 | model.aggregation.type = value.aggregation.type; |
234 | 211 | } |
212 | + model.aggregation.limit = value.aggregation.limit || timeService.avgAggregationLimit(); | |
235 | 213 | } |
236 | 214 | } |
237 | 215 | scope.updateDisplayValue(); | ... | ... |
... | ... | @@ -21,14 +21,39 @@ |
21 | 21 | } |
22 | 22 | |
23 | 23 | .tb-timewindow-panel { |
24 | - min-height: 375px; | |
24 | + max-height: 440px; | |
25 | + min-width: 417px; | |
25 | 26 | background: white; |
26 | 27 | border-radius: 4px; |
27 | 28 | box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), |
28 | 29 | 0 13px 19px 2px rgba(0, 0, 0, 0.14), |
29 | 30 | 0 5px 24px 4px rgba(0, 0, 0, 0.12); |
30 | 31 | overflow: hidden; |
32 | + form, fieldset { | |
33 | + height: 100%; | |
34 | + } | |
31 | 35 | md-content { |
32 | 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 | 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 | 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 | 21 | <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon> |
22 | 22 | </md-button> |
23 | 23 | </section> |
\ No newline at end of file | ... | ... |
... | ... | @@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; |
19 | 19 | /* eslint-disable angular/angularelement */ |
20 | 20 | |
21 | 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 | 23 | datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) { |
24 | 24 | |
25 | 25 | var vm = this; |
... | ... | @@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
41 | 41 | var targetDeviceAliasId = null; |
42 | 42 | var targetDeviceId = null; |
43 | 43 | var originalTimewindow = null; |
44 | - var subscriptionTimewindow = { | |
45 | - fixedWindow: null, | |
46 | - realtimeWindowMs: null, | |
47 | - aggregation: null | |
48 | - }; | |
44 | + var subscriptionTimewindow = null; | |
49 | 45 | var dataUpdateCaf = null; |
50 | 46 | |
51 | 47 | /* |
... | ... | @@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
488 | 484 | if (!originalTimewindow) { |
489 | 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 | 490 | function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) { |
... | ... | @@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
511 | 499 | } |
512 | 500 | } |
513 | 501 | if (update) { |
514 | - if (subscriptionTimewindow.realtimeWindowMs) { | |
502 | + if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) { | |
515 | 503 | updateTimewindow(); |
516 | 504 | } |
517 | 505 | widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data; |
... | ... | @@ -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 | 556 | function subscribe() { |
559 | 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 | 558 | if (widget.type === types.widgetType.timeseries.value && |
568 | 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 | 561 | if (subscriptionTimewindow.fixedWindow) { |
611 | 562 | onDataUpdated(); |
612 | 563 | } |
613 | 564 | } |
565 | + var index = 0; | |
614 | 566 | for (var i in widget.config.datasources) { |
615 | 567 | var datasource = widget.config.datasources[i]; |
616 | 568 | var deviceId = null; |
... | ... | @@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q |
630 | 582 | dataUpdated: function (data, datasourceIndex, dataKeyIndex) { |
631 | 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 | 592 | datasourceIndex: index |
634 | 593 | }; |
635 | 594 | ... | ... |
... | ... | @@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller'; |
29 | 29 | |
30 | 30 | /*@ngInject*/ |
31 | 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 | 34 | var linker = function (scope, element, attrs) { |
35 | 35 | |
... | ... | @@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS |
303 | 303 | var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid; |
304 | 304 | widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( |
305 | 305 | function success(widgetTypes) { |
306 | + | |
307 | + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']); | |
308 | + | |
306 | 309 | for (var i = 0; i < widgetTypes.length; i++) { |
307 | 310 | var widgetType = widgetTypes[i]; |
308 | 311 | var widgetInfo = widgetService.toWidgetInfo(widgetType); | ... | ... |
... | ... | @@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', []) |
67 | 67 | "aggregation": "Aggregation", |
68 | 68 | "function": "Data aggregation function", |
69 | 69 | "limit": "Max values", |
70 | + "group-interval": "Grouping interval", | |
70 | 71 | "min": "Min", |
71 | 72 | "max": "Max", |
72 | 73 | "avg": "Average", |
... | ... | @@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', []) |
558 | 559 | "days": "Days", |
559 | 560 | "hours": "Hours", |
560 | 561 | "minutes": "Minutes", |
561 | - "seconds": "Seconds" | |
562 | + "seconds": "Seconds", | |
563 | + "advanced": "Advanced" | |
562 | 564 | }, |
563 | 565 | "timewindow": { |
564 | 566 | "days": "{ days, select, 1 { day } other {# days } }", | ... | ... |
... | ... | @@ -167,6 +167,7 @@ export default class TbFlot { |
167 | 167 | var settings = ctx.settings; |
168 | 168 | ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1; |
169 | 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 | 172 | var font = { |
172 | 173 | color: settings.fontColor || "#545454", |
... | ... | @@ -232,6 +233,21 @@ export default class TbFlot { |
232 | 233 | options.yaxis.tickFormatter = function() { |
233 | 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 | 252 | options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color; |
237 | 253 | options.yaxis.label = settings.yaxis.title || null; |
... | ... | @@ -323,6 +339,8 @@ export default class TbFlot { |
323 | 339 | |
324 | 340 | this.options = options; |
325 | 341 | |
342 | + this.checkMouseEvents(); | |
343 | + | |
326 | 344 | if (this.chartType === 'pie' && this.ctx.animatedPie) { |
327 | 345 | this.ctx.pieDataAnimationDuration = 250; |
328 | 346 | this.ctx.pieData = angular.copy(this.ctx.data); |
... | ... | @@ -337,7 +355,6 @@ export default class TbFlot { |
337 | 355 | } else { |
338 | 356 | this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); |
339 | 357 | } |
340 | - this.checkMouseEvents(); | |
341 | 358 | } |
342 | 359 | |
343 | 360 | update() { |
... | ... | @@ -577,6 +594,11 @@ export default class TbFlot { |
577 | 594 | "type": "boolean", |
578 | 595 | "default": false |
579 | 596 | }, |
597 | + "tooltipCumulative": { | |
598 | + "title": "Show cumulative values in stacking mode", | |
599 | + "type": "boolean", | |
600 | + "default": false | |
601 | + }, | |
580 | 602 | "grid": { |
581 | 603 | "title": "Grid settings", |
582 | 604 | "type": "object", |
... | ... | @@ -710,6 +732,7 @@ export default class TbFlot { |
710 | 732 | "decimals", |
711 | 733 | "units", |
712 | 734 | "tooltipIndividual", |
735 | + "tooltipCumulative", | |
713 | 736 | { |
714 | 737 | "key": "grid", |
715 | 738 | "items": [ |
... | ... | @@ -834,10 +857,28 @@ export default class TbFlot { |
834 | 857 | } |
835 | 858 | |
836 | 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 | 1071 | minTime = pointTime; |
1031 | 1072 | } |
1032 | 1073 | if (series.stack) { |
1033 | - if (this.ctx.tooltipIndividual) { | |
1074 | + if (this.ctx.tooltipIndividual || !this.ctx.tooltipCumulative) { | |
1034 | 1075 | value = series.data[hoverIndex][1]; |
1035 | 1076 | } else { |
1036 | 1077 | last_value += series.data[hoverIndex][1]; | ... | ... |
... | ... | @@ -201,6 +201,14 @@ md-sidenav { |
201 | 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 | 213 | * Prompt |
206 | 214 | ***********************/ | ... | ... |