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