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