diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go
index 8bfc9f9138d..143ee5b98d5 100644
--- a/pkg/api/dtos/models.go
+++ b/pkg/api/dtos/models.go
@@ -97,12 +97,7 @@ func (slice DataSourceList) Swap(i, j int) {
}
type MetricQueryResultDto struct {
- Data []MetricQueryResultDataDto `json:"data"`
-}
-
-type MetricQueryResultDataDto struct {
- Target string `json:"target"`
- DataPoints [][2]float64 `json:"datapoints"`
+ Data []interface{} `json:"data"`
}
type UserStars struct {
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 063e91ef5da..385810b942e 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -165,7 +165,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
}
- if c.OrgRole == m.ROLE_ADMIN {
+ if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
}
diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index 154f863af53..c36f2108581 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -2,39 +2,49 @@ package api
import (
"encoding/json"
- "math/rand"
"net/http"
- "strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
+ "github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util"
)
func GetTestMetrics(c *middleware.Context) Response {
- from := c.QueryInt64("from")
- to := c.QueryInt64("to")
- maxDataPoints := c.QueryInt64("maxDataPoints")
- stepInSeconds := (to - from) / maxDataPoints
+
+ timeRange := tsdb.NewTimeRange(c.Query("from"), c.Query("to"))
+
+ req := &tsdb.Request{
+ TimeRange: timeRange,
+ Queries: []*tsdb.Query{
+ {
+ RefId: "A",
+ MaxDataPoints: c.QueryInt64("maxDataPoints"),
+ IntervalMs: c.QueryInt64("intervalMs"),
+ DataSource: &tsdb.DataSourceInfo{
+ Name: "Grafana TestDataDB",
+ PluginId: "grafana-testdata-datasource",
+ },
+ },
+ },
+ }
+
+ resp, err := tsdb.HandleRequest(req)
+ if err != nil {
+ return ApiError(500, "Metric request error", err)
+ }
result := dtos.MetricQueryResultDto{}
- result.Data = make([]dtos.MetricQueryResultDataDto, 1)
- for seriesIndex := range result.Data {
- points := make([][2]float64, maxDataPoints)
- walker := rand.Float64() * 100
- time := from
-
- for i := range points {
- points[i][0] = walker
- points[i][1] = float64(time)
- walker += rand.Float64() - 0.5
- time += stepInSeconds
+ for _, v := range resp.Results {
+ if v.Error != nil {
+ return ApiError(500, "tsdb.HandleRequest() response error", v.Error)
}
- result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
- result.Data[seriesIndex].DataPoints = points
+ for _, series := range v.Series {
+ result.Data = append(result.Data, series)
+ }
}
return Json(200, &result)
diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go
index 974559001d1..8db480f947d 100644
--- a/pkg/plugins/frontend_plugin.go
+++ b/pkg/plugins/frontend_plugin.go
@@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
fp.IncludedInAppId = app.Id
fp.BaseUrl = app.BaseUrl
- fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
+
+ if isExternalPlugin(app.PluginDir) {
+ fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
+ } else {
+ fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
+ }
}
func (fp *FrontendPluginBase) handleModuleDefaults() {
diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go
index 15db31838b0..e808d77a182 100644
--- a/pkg/services/alerting/conditions/query.go
+++ b/pkg/services/alerting/conditions/query.go
@@ -34,8 +34,8 @@ type AlertQuery struct {
}
func (c *QueryCondition) Eval(context *alerting.EvalContext) {
- timerange := tsdb.NewTimerange(c.Query.From, c.Query.To)
- seriesList, err := c.executeQuery(context, timerange)
+ timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
+ seriesList, err := c.executeQuery(context, timeRange)
if err != nil {
context.Error = err
return
@@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
context.Firing = len(context.EvalMatches) > 0
}
-func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
+func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
getDsInfo := &m.GetDataSourceByIdQuery{
Id: c.Query.DatasourceId,
OrgId: context.Rule.OrgId,
@@ -79,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange t
return nil, fmt.Errorf("Could not find datasource")
}
- req := c.getRequestForAlertRule(getDsInfo.Result, timerange)
+ req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
result := make(tsdb.TimeSeriesSlice, 0)
resp, err := c.HandleRequest(req)
diff --git a/pkg/services/alerting/init/init.go b/pkg/services/alerting/init/init.go
index b9cba2fd353..94f97a41905 100644
--- a/pkg/services/alerting/init/init.go
+++ b/pkg/services/alerting/init/init.go
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
+ _ "github.com/grafana/grafana/pkg/tsdb/testdata"
)
var engine *alerting.Engine
diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go
index 262be5cbd24..b8c55f1e60d 100644
--- a/pkg/tsdb/models.go
+++ b/pkg/tsdb/models.go
@@ -3,21 +3,22 @@ package tsdb
import "github.com/grafana/grafana/pkg/components/simplejson"
type Query struct {
- RefId string
- Query string
- Model *simplejson.Json
- Depends []string
- DataSource *DataSourceInfo
- Results []*TimeSeries
- Exclude bool
+ RefId string
+ Query string
+ Model *simplejson.Json
+ Depends []string
+ DataSource *DataSourceInfo
+ Results []*TimeSeries
+ Exclude bool
+ MaxDataPoints int64
+ IntervalMs int64
}
type QuerySlice []*Query
type Request struct {
- TimeRange TimeRange
- MaxDataPoints int
- Queries QuerySlice
+ TimeRange TimeRange
+ Queries QuerySlice
}
type Response struct {
diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go
new file mode 100644
index 00000000000..b1eca4cb46a
--- /dev/null
+++ b/pkg/tsdb/testdata/testdata.go
@@ -0,0 +1,54 @@
+package testdata
+
+import (
+ "math/rand"
+
+ "github.com/grafana/grafana/pkg/tsdb"
+)
+
+type TestDataExecutor struct {
+ *tsdb.DataSourceInfo
+}
+
+func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+ return &TestDataExecutor{dsInfo}
+}
+
+func init() {
+ tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
+}
+
+func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
+ result := &tsdb.BatchResult{}
+ result.QueryResults = make(map[string]*tsdb.QueryResult)
+
+ from, _ := context.TimeRange.FromTime()
+ to, _ := context.TimeRange.ToTime()
+
+ queryRes := &tsdb.QueryResult{}
+
+ for _, query := range queries {
+ // scenario := query.Model.Get("scenario").MustString("random_walk")
+ series := &tsdb.TimeSeries{Name: "test-series-0"}
+
+ stepInSeconds := (to.Unix() - from.Unix()) / query.MaxDataPoints
+ points := make([][2]*float64, 0)
+ walker := rand.Float64() * 100
+ time := from.Unix()
+
+ for i := int64(0); i < query.MaxDataPoints; i++ {
+ timestamp := float64(time)
+ val := float64(walker)
+ points = append(points, [2]*float64{&val, ×tamp})
+
+ walker += rand.Float64() - 0.5
+ time += stepInSeconds
+ }
+
+ series.Points = points
+ queryRes.Series = append(queryRes.Series, series)
+ }
+
+ result.QueryResults["A"] = queryRes
+ return result
+}
diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go
index 8e1a1c66e3d..dee3e683516 100644
--- a/pkg/tsdb/time_range.go
+++ b/pkg/tsdb/time_range.go
@@ -2,11 +2,12 @@ package tsdb
import (
"fmt"
+ "strconv"
"strings"
"time"
)
-func NewTimerange(from, to string) TimeRange {
+func NewTimeRange(from, to string) TimeRange {
return TimeRange{
From: from,
To: to,
@@ -21,6 +22,10 @@ type TimeRange struct {
}
func (tr TimeRange) FromTime() (time.Time, error) {
+ if val, err := strconv.ParseInt(tr.From, 10, 64); err == nil {
+ return time.Unix(val, 0), nil
+ }
+
fromRaw := strings.Replace(tr.From, "now-", "", 1)
diff, err := time.ParseDuration("-" + fromRaw)
@@ -45,5 +50,9 @@ func (tr TimeRange) ToTime() (time.Time, error) {
return tr.Now.Add(diff), nil
}
+ if val, err := strconv.ParseInt(tr.To, 10, 64); err == nil {
+ return time.Unix(val, 0), nil
+ }
+
return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
}
diff --git a/pkg/tsdb/time_range_test.go b/pkg/tsdb/time_range_test.go
index 56ea9d24490..f4acb5e6d80 100644
--- a/pkg/tsdb/time_range_test.go
+++ b/pkg/tsdb/time_range_test.go
@@ -60,6 +60,23 @@ func TestTimeRange(t *testing.T) {
})
})
+ Convey("can parse unix epocs", func() {
+ var err error
+ tr := TimeRange{
+ From: "1474973725473",
+ To: "1474975757930",
+ Now: now,
+ }
+
+ res, err := tr.FromTime()
+ So(err, ShouldBeNil)
+ So(res.Unix(), ShouldEqual, 1474973725473)
+
+ res, err = tr.ToTime()
+ So(err, ShouldBeNil)
+ So(res.Unix(), ShouldEqual, 1474975757930)
+ })
+
Convey("Cannot parse asdf", func() {
var err error
tr := TimeRange{
diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts
index dfae26fb48b..d672e0dd0dc 100644
--- a/public/app/core/time_series2.ts
+++ b/public/app/core/time_series2.ts
@@ -31,6 +31,8 @@ export default class TimeSeries {
allIsZero: boolean;
decimals: number;
scaledDecimals: number;
+ hasMsResolution: boolean;
+ isOutsideRange: boolean;
lines: any;
bars: any;
@@ -54,6 +56,7 @@ export default class TimeSeries {
this.stats = {};
this.legend = true;
this.unit = opts.unit;
+ this.hasMsResolution = this.isMsResolutionNeeded();
}
applySeriesOverrides(overrides) {
diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js
index cf80d671d71..a807a249235 100644
--- a/public/app/core/utils/kbn.js
+++ b/public/app/core/utils/kbn.js
@@ -174,7 +174,10 @@ function($, _, moment) {
lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
}
else {
- return userInterval;
+ return {
+ intervalMs: kbn.interval_to_ms(userInterval),
+ interval: userInterval,
+ };
}
}
@@ -183,7 +186,10 @@ function($, _, moment) {
intervalMs = lowLimitMs;
}
- return kbn.secondsToHms(intervalMs / 1000);
+ return {
+ intervalMs: intervalMs,
+ interval: kbn.secondsToHms(intervalMs / 1000),
+ };
};
kbn.describe_interval = function (string) {
diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts
index 62cece44acf..f6f2d730cd3 100644
--- a/public/app/features/panel/metrics_panel_ctrl.ts
+++ b/public/app/features/panel/metrics_panel_ctrl.ts
@@ -25,6 +25,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range: any;
rangeRaw: any;
interval: any;
+ intervalMs: any;
resolution: any;
timeInfo: any;
skipDataOnInit: boolean;
@@ -123,11 +124,22 @@ class MetricsPanelCtrl extends PanelCtrl {
this.resolution = Math.ceil($(window).width() * (this.panel.span / 12));
}
- var panelInterval = this.panel.interval;
- var datasourceInterval = (this.datasource || {}).interval;
- this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval);
+ this.calculateInterval();
};
+ calculateInterval() {
+ var intervalOverride = this.panel.interval;
+
+ // if no panel interval check datasource
+ if (!intervalOverride && this.datasource && this.datasource.interval) {
+ intervalOverride = this.datasource.interval;
+ }
+
+ var res = kbn.calculateInterval(this.range, this.resolution, intervalOverride);
+ this.interval = res.interval;
+ this.intervalMs = res.intervalMs;
+ }
+
applyPanelTimeOverrides() {
this.timeInfo = '';
@@ -183,6 +195,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range: this.range,
rangeRaw: this.rangeRaw,
interval: this.interval,
+ intervalMs: this.intervalMs,
targets: this.panel.targets,
format: this.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: this.resolution,
diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts
index 30b056e1c60..a1cfbf324c0 100644
--- a/public/app/features/templating/interval_variable.ts
+++ b/public/app/features/templating/interval_variable.ts
@@ -54,8 +54,8 @@ export class IntervalVariable implements Variable {
this.options.unshift({ text: 'auto', value: '$__auto_interval' });
}
- var interval = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
- this.templateSrv.setGrafanaVariable('$__auto_interval', interval);
+ var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
+ this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
}
updateOptions() {
diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
new file mode 100644
index 00000000000..7533a44760b
--- /dev/null
+++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
@@ -0,0 +1,5 @@
+{
+ "title": "TestData - Graph Panel Last 1h",
+ "tags": ["testdata"],
+ "revision": 1
+}
diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/app/testdata/datasource/datasource.ts
new file mode 100644
index 00000000000..75b43bd34c9
--- /dev/null
+++ b/public/app/plugins/app/testdata/datasource/datasource.ts
@@ -0,0 +1,45 @@
+///
+
+import _ from 'lodash';
+
+class TestDataDatasource {
+
+ /** @ngInject */
+ constructor(private backendSrv, private $q) {}
+
+ query(options) {
+ var queries = _.filter(options.targets, item => {
+ return item.hide !== true;
+ });
+
+ if (queries.length === 0) {
+ return this.$q.when({data: []});
+ }
+
+ return this.backendSrv.get('/api/metrics/test', {
+ from: options.range.from.valueOf(),
+ to: options.range.to.valueOf(),
+ scenario: options.targets[0].scenario,
+ interval: options.intervalMs,
+ maxDataPoints: options.maxDataPoints,
+ }).then(res => {
+ res.data = res.data.map(item => {
+ return {target: item.name, datapoints: item.points};
+ });
+
+ return res;
+ });
+ }
+
+ annotationQuery(options) {
+ return this.backendSrv.get('/api/annotations', {
+ from: options.range.from.valueOf(),
+ to: options.range.to.valueOf(),
+ limit: options.limit,
+ type: options.type,
+ });
+ }
+
+}
+
+export {TestDataDatasource};
diff --git a/public/app/plugins/app/testdata/datasource/module.ts b/public/app/plugins/app/testdata/datasource/module.ts
new file mode 100644
index 00000000000..309b7443836
--- /dev/null
+++ b/public/app/plugins/app/testdata/datasource/module.ts
@@ -0,0 +1,22 @@
+///
+
+import {TestDataDatasource} from './datasource';
+import {TestDataQueryCtrl} from './query_ctrl';
+
+class TestDataAnnotationsQueryCtrl {
+ annotation: any;
+
+ constructor() {
+ }
+
+ static template = '
test data
';
+}
+
+
+export {
+ TestDataDatasource,
+ TestDataDatasource as Datasource,
+ TestDataQueryCtrl as QueryCtrl,
+ TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+};
+
diff --git a/public/app/plugins/app/testdata/datasource/plugin.json b/public/app/plugins/app/testdata/datasource/plugin.json
new file mode 100644
index 00000000000..0ad87a15081
--- /dev/null
+++ b/public/app/plugins/app/testdata/datasource/plugin.json
@@ -0,0 +1,19 @@
+{
+ "type": "datasource",
+ "name": "Grafana TestDataDB",
+ "id": "grafana-testdata-datasource",
+
+ "metrics": true,
+ "annotations": true,
+
+ "info": {
+ "author": {
+ "name": "Grafana Project",
+ "url": "http://grafana.org"
+ },
+ "logos": {
+ "small": "",
+ "large": ""
+ }
+ }
+}
diff --git a/public/app/plugins/app/testdata/datasource/query_ctrl.ts b/public/app/plugins/app/testdata/datasource/query_ctrl.ts
new file mode 100644
index 00000000000..44a62fd1a11
--- /dev/null
+++ b/public/app/plugins/app/testdata/datasource/query_ctrl.ts
@@ -0,0 +1,24 @@
+///
+
+import {TestDataDatasource} from './datasource';
+import {QueryCtrl} from 'app/plugins/sdk';
+
+export class TestDataQueryCtrl extends QueryCtrl {
+ static templateUrl = 'partials/query.editor.html';
+
+ scenarioDefs: any;
+
+ /** @ngInject **/
+ constructor($scope, $injector) {
+ super($scope, $injector);
+
+ this.target.scenario = this.target.scenario || 'random_walk';
+
+ this.scenarioDefs = {
+ 'random_walk': {text: 'Random Walk'},
+ 'no_datapoints': {text: 'No Datapoints'},
+ 'data_outside_range': {text: 'Data Outside Range'},
+ };
+ }
+}
+
diff --git a/public/app/plugins/app/testdata/module.ts b/public/app/plugins/app/testdata/module.ts
new file mode 100644
index 00000000000..dee1679637a
--- /dev/null
+++ b/public/app/plugins/app/testdata/module.ts
@@ -0,0 +1,36 @@
+///
+
+export class ConfigCtrl {
+ static template = '';
+
+ appEditCtrl: any;
+
+ constructor(private backendSrv) {
+ this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
+ }
+
+ initDatasource() {
+ return this.backendSrv.get('/api/datasources').then(res => {
+ var found = false;
+ for (let ds of res) {
+ if (ds.type === "grafana-testdata-datasource") {
+ found = true;
+ }
+ }
+
+ if (!found) {
+ var dsInstance = {
+ name: 'Grafana TestData',
+ type: 'grafana-testdata-datasource',
+ access: 'direct',
+ jsonData: {}
+ };
+
+ return this.backendSrv.post('/api/datasources', dsInstance);
+ }
+
+ return Promise.resolve();
+ });
+ }
+}
+
diff --git a/public/app/plugins/app/testdata/partials/query.editor.html b/public/app/plugins/app/testdata/partials/query.editor.html
new file mode 100644
index 00000000000..d9068dfda49
--- /dev/null
+++ b/public/app/plugins/app/testdata/partials/query.editor.html
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json
new file mode 100644
index 00000000000..47ab291409b
--- /dev/null
+++ b/public/app/plugins/app/testdata/plugin.json
@@ -0,0 +1,27 @@
+{
+ "type": "app",
+ "name": "Grafana TestData",
+ "id": "testdata",
+
+ "info": {
+ "description": "Grafana test data app",
+ "author": {
+ "name": "Grafana Project",
+ "url": "http://grafana.org"
+ },
+ "version": "1.0.5",
+ "updated": "2016-09-26"
+ },
+
+ "includes": [
+ {
+ "type": "dashboard",
+ "name": "TestData - Graph Last 1h",
+ "path": "dashboards/graph_last_1h.json"
+ }
+ ],
+
+ "dependencies": {
+ "grafanaVersion": "4.x.x"
+ }
+}
diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts
index b22d9f391d0..404b5a90aee 100644
--- a/public/app/plugins/panel/graph/data_processor.ts
+++ b/public/app/plugins/panel/graph/data_processor.ts
@@ -2,6 +2,7 @@
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
+import moment from 'moment';
import TimeSeries from 'app/core/time_series2';
import {colors} from 'app/core/core';
@@ -28,8 +29,10 @@ export class DataProcessor {
switch (this.panel.xaxis.mode) {
case 'series':
- case 'time': {
- return options.dataList.map(this.timeSeriesHandler.bind(this));
+ case 'time': {
+ return options.dataList.map((item, index) => {
+ return this.timeSeriesHandler(item, index, options);
+ });
}
case 'field': {
return this.customHandler(firstItem);
@@ -74,33 +77,26 @@ export class DataProcessor {
}
}
- seriesHandler(seriesData, index, datapoints, alias) {
+ timeSeriesHandler(seriesData, index, options) {
+ var datapoints = seriesData.datapoints;
+ var alias = seriesData.target;
+
var colorIndex = index % colors.length;
var color = this.panel.aliasColors[alias] || colors[colorIndex];
var series = new TimeSeries({datapoints: datapoints, alias: alias, color: color, unit: seriesData.unit});
- // if (datapoints && datapoints.length > 0) {
- // var last = moment.utc(datapoints[datapoints.length - 1][1]);
- // var from = moment.utc(this.range.from);
- // if (last - from < -10000) {
- // this.datapointsOutside = true;
- // }
- //
- // this.datapointsCount += datapoints.length;
- // this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded();
- // }
+ if (datapoints && datapoints.length > 0) {
+ var last = datapoints[datapoints.length - 1][1];
+ var from = options.range.from;
+ if (last - from < -10000) {
+ series.isOutsideRange = true;
+ }
+ }
return series;
}
- timeSeriesHandler(seriesData, index) {
- var datapoints = seriesData.datapoints;
- var alias = seriesData.target;
-
- return this.seriesHandler(seriesData, index, datapoints, alias);
- }
-
customHandler(dataItem) {
console.log('custom', dataItem);
let nameField = this.panel.xaxis.name;
@@ -126,21 +122,21 @@ export class DataProcessor {
return [];
}
- tableHandler(seriesData, index) {
- var xColumnIndex = Number(this.panel.xaxis.columnIndex);
- var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex);
- var datapoints = _.map(seriesData.rows, (row) => {
- var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row);
- return [
- value, // Y value
- row[xColumnIndex] // X value
- ];
- });
-
- var alias = seriesData.columns[valueColumnIndex].text;
-
- return this.seriesHandler(seriesData, index, datapoints, alias);
- }
+ // tableHandler(seriesData, index) {
+ // var xColumnIndex = Number(this.panel.xaxis.columnIndex);
+ // var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex);
+ // var datapoints = _.map(seriesData.rows, (row) => {
+ // var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row);
+ // return [
+ // value, // Y value
+ // row[xColumnIndex] // X value
+ // ];
+ // });
+ //
+ // var alias = seriesData.columns[valueColumnIndex].text;
+ //
+ // return this.seriesHandler(seriesData, index, datapoints, alias);
+ // }
// esRawDocHandler(seriesData, index) {
// let xField = this.panel.xaxis.esField;
@@ -160,7 +156,7 @@ export class DataProcessor {
// var alias = valueField;
// return this.seriesHandler(seriesData, index, datapoints, alias);
// }
- //
+
validateXAxisSeriesValue() {
switch (this.panel.xaxis.mode) {
case 'series': {
diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js
index 70eef7c5fe3..cd3bddf41ef 100644
--- a/public/app/plugins/panel/graph/graph_tooltip.js
+++ b/public/app/plugins/panel/graph/graph_tooltip.js
@@ -121,20 +121,20 @@ function ($, _) {
var seriesList = getSeriesFn();
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
- if (panel.tooltip.msResolution) {
- tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
- } else {
- tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
- }
-
if (dashboard.sharedCrosshair) {
- ctrl.publishAppEvent('setCrosshair', { pos: pos, scope: scope });
+ ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope});
}
if (seriesList.length === 0) {
return;
}
+ if (seriesList[0].hasMsResolution) {
+ tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
+ } else {
+ tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
+ }
+
if (panel.tooltip.shared) {
plot.unhighlight();
diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts
index 9edcb7fe1ff..f6d77636845 100644
--- a/public/app/plugins/panel/graph/module.ts
+++ b/public/app/plugins/panel/graph/module.ts
@@ -25,7 +25,6 @@ class GraphCtrl extends MetricsPanelCtrl {
annotationsPromise: any;
datapointsCount: number;
datapointsOutside: boolean;
- datapointsWarning: boolean;
colors: any = [];
subTabIndex: number;
processor: DataProcessor;
@@ -172,13 +171,20 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onDataReceived(dataList) {
- this.datapointsWarning = false;
- this.datapointsCount = 0;
- this.datapointsOutside = false;
this.dataList = dataList;
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
- this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside;
+
+ this.datapointsCount = this.seriesList.reduce((prev, series) => {
+ return prev + series.datapoints.length;
+ }, 0);
+
+ this.datapointsOutside = false;
+ for (let series of this.seriesList) {
+ if (series.isOutsideRange) {
+ this.datapointsOutside = true;
+ }
+ }
this.annotationsPromise.then(annotations => {
this.loading = false;
diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
index c7d981f78d4..cac69807eab 100644
--- a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
+++ b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
@@ -3,6 +3,7 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import angular from 'angular';
+import moment from 'moment';
import {GraphCtrl} from '../module';
import helpers from '../../../../../test/specs/helpers';
@@ -19,64 +20,53 @@ describe('GraphCtrl', function() {
ctx.ctrl.updateTimeRange();
});
- describe.skip('msResolution with second resolution timestamps', function() {
+ describe('when time series are outside range', function() {
+
beforeEach(function() {
var data = [
- { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
- { target: 'test.cpu2', datapoints: [[55, 1236547890], [90, 1234456709]]}
+ {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
];
- ctx.ctrl.panel.tooltip.msResolution = false;
+
+ ctx.ctrl.range = {from: moment().valueOf(), to: moment().valueOf()};
ctx.ctrl.onDataReceived(data);
});
- it('should not show millisecond resolution tooltip', function() {
- expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false);
+ it('should set datapointsOutside', function() {
+ expect(ctx.ctrl.datapointsOutside).to.be(true);
});
});
- describe.skip('msResolution with millisecond resolution timestamps', function() {
+ describe('when time series are inside range', function() {
beforeEach(function() {
+ var range = {
+ from: moment().subtract(1, 'days').valueOf(),
+ to: moment().valueOf()
+ };
+
var data = [
- { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
- { target: 'test.cpu2', datapoints: [[55, 1236547890001], [90, 1234456709000]]}
+ {target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]]},
];
- ctx.ctrl.panel.tooltip.msResolution = false;
+
+ ctx.ctrl.range = range;
ctx.ctrl.onDataReceived(data);
});
- it('should show millisecond resolution tooltip', function() {
- expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true);
+ it('should set datapointsOutside', function() {
+ expect(ctx.ctrl.datapointsOutside).to.be(false);
});
});
- describe.skip('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
+ describe('datapointsCount given 2 series', function() {
beforeEach(function() {
var data = [
- { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
- { target: 'test.cpu2', datapoints: [[55, 1236547890000], [90, 1234456709000]]}
+ {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
+ {target: 'test.cpu2', datapoints: [[45, 1234567890]]},
];
- ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);
});
- it('should not show millisecond resolution tooltip', function() {
- expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false);
- });
- });
-
- describe.skip('msResolution with millisecond resolution timestamps in one of the series', function() {
- beforeEach(function() {
- var data = [
- { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
- { target: 'test.cpu2', datapoints: [[55, 1236547890010], [90, 1234456709000]]},
- { target: 'test.cpu3', datapoints: [[65, 1236547890000], [120, 1234456709000]]}
- ];
- ctx.ctrl.panel.tooltip.msResolution = false;
- ctx.ctrl.onDataReceived(data);
- });
-
- it('should show millisecond resolution tooltip', function() {
- expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true);
+ it('should set datapointsCount to sum of datapoints', function() {
+ expect(ctx.ctrl.datapointsCount).to.be(3);
});
});
diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts
index fc989e659c7..ec6cd8d0907 100644
--- a/public/app/plugins/panel/graph/template.ts
+++ b/public/app/plugins/panel/graph/template.ts
@@ -2,11 +2,14 @@ var template = `
-
-
+
+
No datapoints No datapoints returned from metric query
-
+
+
+
+
Datapoints outside time range
Can be caused by timezone mismatch between browser and graphite server
diff --git a/public/app/plugins/panel/pluginlist/plugin.json b/public/app/plugins/panel/pluginlist/plugin.json
index be6ae9a5985..72f5ea06d25 100644
--- a/public/app/plugins/panel/pluginlist/plugin.json
+++ b/public/app/plugins/panel/pluginlist/plugin.json
@@ -7,7 +7,7 @@
"author": {
"name": "Grafana Project",
"url": "http://grafana.org"
-},
+ },
"logos": {
"small": "img/icn-dashlist-panel.svg",
"large": "img/icn-dashlist-panel.svg"
diff --git a/public/test/core/time_series_specs.js b/public/test/core/time_series_specs.js
index 034e872e2f1..2b325cf6d46 100644
--- a/public/test/core/time_series_specs.js
+++ b/public/test/core/time_series_specs.js
@@ -56,6 +56,38 @@ define([
});
});
+ describe('When checking if ms resolution is needed', function() {
+ describe('msResolution with second resolution timestamps', function() {
+ beforeEach(function() {
+ series = new TimeSeries({datapoints: [[45, 1234567890], [60, 1234567899]]});
+ });
+
+ it('should set hasMsResolution to false', function() {
+ expect(series.hasMsResolution).to.be(false);
+ });
+ });
+
+ describe('msResolution with millisecond resolution timestamps', function() {
+ beforeEach(function() {
+ series = new TimeSeries({datapoints: [[55, 1236547890001], [90, 1234456709000]]});
+ });
+
+ it('should show millisecond resolution tooltip', function() {
+ expect(series.hasMsResolution).to.be(true);
+ });
+ });
+
+ describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
+ beforeEach(function() {
+ series = new TimeSeries({datapoints: [[45, 1234567890000], [60, 1234567899000]]});
+ });
+
+ it('should not show millisecond resolution tooltip', function() {
+ expect(series.hasMsResolution).to.be(false);
+ });
+ });
+ });
+
describe('can detect if series contains ms precision', function() {
var fakedata;
diff --git a/public/test/core/utils/kbn_specs.js b/public/test/core/utils/kbn_specs.js
index 959b176b06c..95bef57ef1a 100644
--- a/public/test/core/utils/kbn_specs.js
+++ b/public/test/core/utils/kbn_specs.js
@@ -132,62 +132,64 @@ define([
describe('calculateInterval', function() {
it('1h 100 resultion', function() {
var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
- var str = kbn.calculateInterval(range, 100, null);
- expect(str).to.be('30s');
+ var res = kbn.calculateInterval(range, 100, null);
+ expect(res.interval).to.be('30s');
});
it('10m 1600 resolution', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
- var str = kbn.calculateInterval(range, 1600, null);
- expect(str).to.be('500ms');
+ var res = kbn.calculateInterval(range, 1600, null);
+ expect(res.interval).to.be('500ms');
+ expect(res.intervalMs).to.be(500);
});
it('fixed user interval', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
- var str = kbn.calculateInterval(range, 1600, '10s');
- expect(str).to.be('10s');
+ var res = kbn.calculateInterval(range, 1600, '10s');
+ expect(res.interval).to.be('10s');
+ expect(res.intervalMs).to.be(10000);
});
it('short time range and user low limit', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
- var str = kbn.calculateInterval(range, 1600, '>10s');
- expect(str).to.be('10s');
+ var res = kbn.calculateInterval(range, 1600, '>10s');
+ expect(res.interval).to.be('10s');
});
it('large time range and user low limit', function() {
- var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') };
- var str = kbn.calculateInterval(range, 1000, '>10s');
- expect(str).to.be('20m');
+ var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')};
+ var res = kbn.calculateInterval(range, 1000, '>10s');
+ expect(res.interval).to.be('20m');
});
-
+
it('10s 900 resolution and user low limit in ms', function() {
var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
- var str = kbn.calculateInterval(range, 900, '>15ms');
- expect(str).to.be('15ms');
+ var res = kbn.calculateInterval(range, 900, '>15ms');
+ expect(res.interval).to.be('15ms');
});
});
describe('hex', function() {
- it('positive integer', function() {
- var str = kbn.valueFormats.hex(100, 0);
- expect(str).to.be('64');
- });
- it('negative integer', function() {
- var str = kbn.valueFormats.hex(-100, 0);
- expect(str).to.be('-64');
- });
- it('null', function() {
- var str = kbn.valueFormats.hex(null, 0);
- expect(str).to.be('');
- });
- it('positive float', function() {
- var str = kbn.valueFormats.hex(50.52, 1);
- expect(str).to.be('32.8');
- });
- it('negative float', function() {
- var str = kbn.valueFormats.hex(-50.333, 2);
- expect(str).to.be('-32.547AE147AE14');
- });
+ it('positive integer', function() {
+ var str = kbn.valueFormats.hex(100, 0);
+ expect(str).to.be('64');
+ });
+ it('negative integer', function() {
+ var str = kbn.valueFormats.hex(-100, 0);
+ expect(str).to.be('-64');
+ });
+ it('null', function() {
+ var str = kbn.valueFormats.hex(null, 0);
+ expect(str).to.be('');
+ });
+ it('positive float', function() {
+ var str = kbn.valueFormats.hex(50.52, 1);
+ expect(str).to.be('32.8');
+ });
+ it('negative float', function() {
+ var str = kbn.valueFormats.hex(-50.333, 2);
+ expect(str).to.be('-32.547AE147AE14');
+ });
});
describe('hex 0x', function() {