feat(testdata): worked on testdata app

This commit is contained in:
Torkel Ödegaard
2016-09-27 14:39:51 +02:00
parent 81cb4a740b
commit 34f15d92d0
30 changed files with 512 additions and 169 deletions

View File

@ -97,12 +97,7 @@ func (slice DataSourceList) Swap(i, j int) {
} }
type MetricQueryResultDto struct { type MetricQueryResultDto struct {
Data []MetricQueryResultDataDto `json:"data"` Data []interface{} `json:"data"`
}
type MetricQueryResultDataDto struct {
Target string `json:"target"`
DataPoints [][2]float64 `json:"datapoints"`
} }
type UserStars struct { type UserStars struct {

View File

@ -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{Divider: true})
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
} }

View File

@ -2,39 +2,49 @@ package api
import ( import (
"encoding/json" "encoding/json"
"math/rand"
"net/http" "net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func GetTestMetrics(c *middleware.Context) Response { func GetTestMetrics(c *middleware.Context) Response {
from := c.QueryInt64("from")
to := c.QueryInt64("to") timeRange := tsdb.NewTimeRange(c.Query("from"), c.Query("to"))
maxDataPoints := c.QueryInt64("maxDataPoints")
stepInSeconds := (to - from) / maxDataPoints 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 := dtos.MetricQueryResultDto{}
result.Data = make([]dtos.MetricQueryResultDataDto, 1)
for seriesIndex := range result.Data { for _, v := range resp.Results {
points := make([][2]float64, maxDataPoints) if v.Error != nil {
walker := rand.Float64() * 100 return ApiError(500, "tsdb.HandleRequest() response error", v.Error)
time := from
for i := range points {
points[i][0] = walker
points[i][1] = float64(time)
walker += rand.Float64() - 0.5
time += stepInSeconds
} }
result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex) for _, series := range v.Series {
result.Data[seriesIndex].DataPoints = points result.Data = append(result.Data, series)
}
} }
return Json(200, &result) return Json(200, &result)

View File

@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1) appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
fp.IncludedInAppId = app.Id fp.IncludedInAppId = app.Id
fp.BaseUrl = app.BaseUrl 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() { func (fp *FrontendPluginBase) handleModuleDefaults() {

View File

@ -34,8 +34,8 @@ type AlertQuery struct {
} }
func (c *QueryCondition) Eval(context *alerting.EvalContext) { func (c *QueryCondition) Eval(context *alerting.EvalContext) {
timerange := tsdb.NewTimerange(c.Query.From, c.Query.To) timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
seriesList, err := c.executeQuery(context, timerange) seriesList, err := c.executeQuery(context, timeRange)
if err != nil { if err != nil {
context.Error = err context.Error = err
return return
@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
context.Firing = len(context.EvalMatches) > 0 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{ getDsInfo := &m.GetDataSourceByIdQuery{
Id: c.Query.DatasourceId, Id: c.Query.DatasourceId,
OrgId: context.Rule.OrgId, 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") 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) result := make(tsdb.TimeSeriesSlice, 0)
resp, err := c.HandleRequest(req) resp, err := c.HandleRequest(req)

View File

@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/tsdb/graphite" _ "github.com/grafana/grafana/pkg/tsdb/graphite"
_ "github.com/grafana/grafana/pkg/tsdb/prometheus" _ "github.com/grafana/grafana/pkg/tsdb/prometheus"
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
) )
var engine *alerting.Engine var engine *alerting.Engine

View File

@ -3,21 +3,22 @@ package tsdb
import "github.com/grafana/grafana/pkg/components/simplejson" import "github.com/grafana/grafana/pkg/components/simplejson"
type Query struct { type Query struct {
RefId string RefId string
Query string Query string
Model *simplejson.Json Model *simplejson.Json
Depends []string Depends []string
DataSource *DataSourceInfo DataSource *DataSourceInfo
Results []*TimeSeries Results []*TimeSeries
Exclude bool Exclude bool
MaxDataPoints int64
IntervalMs int64
} }
type QuerySlice []*Query type QuerySlice []*Query
type Request struct { type Request struct {
TimeRange TimeRange TimeRange TimeRange
MaxDataPoints int Queries QuerySlice
Queries QuerySlice
} }
type Response struct { type Response struct {

54
pkg/tsdb/testdata/testdata.go vendored Normal file
View File

@ -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, &timestamp})
walker += rand.Float64() - 0.5
time += stepInSeconds
}
series.Points = points
queryRes.Series = append(queryRes.Series, series)
}
result.QueryResults["A"] = queryRes
return result
}

View File

@ -2,11 +2,12 @@ package tsdb
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
) )
func NewTimerange(from, to string) TimeRange { func NewTimeRange(from, to string) TimeRange {
return TimeRange{ return TimeRange{
From: from, From: from,
To: to, To: to,
@ -21,6 +22,10 @@ type TimeRange struct {
} }
func (tr TimeRange) FromTime() (time.Time, error) { 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) fromRaw := strings.Replace(tr.From, "now-", "", 1)
diff, err := time.ParseDuration("-" + fromRaw) diff, err := time.ParseDuration("-" + fromRaw)
@ -45,5 +50,9 @@ func (tr TimeRange) ToTime() (time.Time, error) {
return tr.Now.Add(diff), nil 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) return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
} }

View File

@ -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() { Convey("Cannot parse asdf", func() {
var err error var err error
tr := TimeRange{ tr := TimeRange{

View File

@ -31,6 +31,8 @@ export default class TimeSeries {
allIsZero: boolean; allIsZero: boolean;
decimals: number; decimals: number;
scaledDecimals: number; scaledDecimals: number;
hasMsResolution: boolean;
isOutsideRange: boolean;
lines: any; lines: any;
bars: any; bars: any;
@ -54,6 +56,7 @@ export default class TimeSeries {
this.stats = {}; this.stats = {};
this.legend = true; this.legend = true;
this.unit = opts.unit; this.unit = opts.unit;
this.hasMsResolution = this.isMsResolutionNeeded();
} }
applySeriesOverrides(overrides) { applySeriesOverrides(overrides) {

View File

@ -174,7 +174,10 @@ function($, _, moment) {
lowLimitMs = kbn.interval_to_ms(lowLimitInterval); lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
} }
else { else {
return userInterval; return {
intervalMs: kbn.interval_to_ms(userInterval),
interval: userInterval,
};
} }
} }
@ -183,7 +186,10 @@ function($, _, moment) {
intervalMs = lowLimitMs; intervalMs = lowLimitMs;
} }
return kbn.secondsToHms(intervalMs / 1000); return {
intervalMs: intervalMs,
interval: kbn.secondsToHms(intervalMs / 1000),
};
}; };
kbn.describe_interval = function (string) { kbn.describe_interval = function (string) {

View File

@ -25,6 +25,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range: any; range: any;
rangeRaw: any; rangeRaw: any;
interval: any; interval: any;
intervalMs: any;
resolution: any; resolution: any;
timeInfo: any; timeInfo: any;
skipDataOnInit: boolean; skipDataOnInit: boolean;
@ -123,11 +124,22 @@ class MetricsPanelCtrl extends PanelCtrl {
this.resolution = Math.ceil($(window).width() * (this.panel.span / 12)); this.resolution = Math.ceil($(window).width() * (this.panel.span / 12));
} }
var panelInterval = this.panel.interval; this.calculateInterval();
var datasourceInterval = (this.datasource || {}).interval;
this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval);
}; };
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() { applyPanelTimeOverrides() {
this.timeInfo = ''; this.timeInfo = '';
@ -183,6 +195,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range: this.range, range: this.range,
rangeRaw: this.rangeRaw, rangeRaw: this.rangeRaw,
interval: this.interval, interval: this.interval,
intervalMs: this.intervalMs,
targets: this.panel.targets, targets: this.panel.targets,
format: this.panel.renderer === 'png' ? 'png' : 'json', format: this.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: this.resolution, maxDataPoints: this.resolution,

View File

@ -54,8 +54,8 @@ export class IntervalVariable implements Variable {
this.options.unshift({ text: 'auto', value: '$__auto_interval' }); 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)); var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
this.templateSrv.setGrafanaVariable('$__auto_interval', interval); this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
} }
updateOptions() { updateOptions() {

View File

@ -0,0 +1,5 @@
{
"title": "TestData - Graph Panel Last 1h",
"tags": ["testdata"],
"revision": 1
}

View File

@ -0,0 +1,45 @@
///<reference path="../../../../headers/common.d.ts" />
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};

View File

@ -0,0 +1,22 @@
///<reference path="../../../../headers/common.d.ts" />
import {TestDataDatasource} from './datasource';
import {TestDataQueryCtrl} from './query_ctrl';
class TestDataAnnotationsQueryCtrl {
annotation: any;
constructor() {
}
static template = '<h2>test data</h2>';
}
export {
TestDataDatasource,
TestDataDatasource as Datasource,
TestDataQueryCtrl as QueryCtrl,
TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -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": ""
}
}
}

View File

@ -0,0 +1,24 @@
///<reference path="../../../../headers/common.d.ts" />
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'},
};
}
}

View File

@ -0,0 +1,36 @@
///<reference path="../../../headers/common.d.ts" />
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();
});
}
}

View File

@ -0,0 +1,22 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword">Scenario</label>
<div class="gf-form-select-wrapper width-20">
<select class="gf-form-input width-20" ng-model="ctrl.target.scenario" ng-options="k as v.text for (k, v) in ctrl.scenarioDefs" ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">With Options</label>
<input type="text" class="gf-form-input" placeholder="optional" ng-model="target.param1" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">Alias</label>
<input type="text" class="gf-form-input" placeholder="optional" ng-model="target.alias" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</query-editor-row>

View File

@ -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"
}
}

View File

@ -2,6 +2,7 @@
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import {colors} from 'app/core/core'; import {colors} from 'app/core/core';
@ -28,8 +29,10 @@ export class DataProcessor {
switch (this.panel.xaxis.mode) { switch (this.panel.xaxis.mode) {
case 'series': case 'series':
case 'time': { case 'time': {
return options.dataList.map(this.timeSeriesHandler.bind(this)); return options.dataList.map((item, index) => {
return this.timeSeriesHandler(item, index, options);
});
} }
case 'field': { case 'field': {
return this.customHandler(firstItem); 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 colorIndex = index % colors.length;
var color = this.panel.aliasColors[alias] || colors[colorIndex]; var color = this.panel.aliasColors[alias] || colors[colorIndex];
var series = new TimeSeries({datapoints: datapoints, alias: alias, color: color, unit: seriesData.unit}); var series = new TimeSeries({datapoints: datapoints, alias: alias, color: color, unit: seriesData.unit});
// if (datapoints && datapoints.length > 0) { if (datapoints && datapoints.length > 0) {
// var last = moment.utc(datapoints[datapoints.length - 1][1]); var last = datapoints[datapoints.length - 1][1];
// var from = moment.utc(this.range.from); var from = options.range.from;
// if (last - from < -10000) { if (last - from < -10000) {
// this.datapointsOutside = true; series.isOutsideRange = true;
// } }
// }
// this.datapointsCount += datapoints.length;
// this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded();
// }
return series; return series;
} }
timeSeriesHandler(seriesData, index) {
var datapoints = seriesData.datapoints;
var alias = seriesData.target;
return this.seriesHandler(seriesData, index, datapoints, alias);
}
customHandler(dataItem) { customHandler(dataItem) {
console.log('custom', dataItem); console.log('custom', dataItem);
let nameField = this.panel.xaxis.name; let nameField = this.panel.xaxis.name;
@ -126,21 +122,21 @@ export class DataProcessor {
return []; return [];
} }
tableHandler(seriesData, index) { // tableHandler(seriesData, index) {
var xColumnIndex = Number(this.panel.xaxis.columnIndex); // var xColumnIndex = Number(this.panel.xaxis.columnIndex);
var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); // var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex);
var datapoints = _.map(seriesData.rows, (row) => { // var datapoints = _.map(seriesData.rows, (row) => {
var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); // var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row);
return [ // return [
value, // Y value // value, // Y value
row[xColumnIndex] // X value // row[xColumnIndex] // X value
]; // ];
}); // });
//
var alias = seriesData.columns[valueColumnIndex].text; // var alias = seriesData.columns[valueColumnIndex].text;
//
return this.seriesHandler(seriesData, index, datapoints, alias); // return this.seriesHandler(seriesData, index, datapoints, alias);
} // }
// esRawDocHandler(seriesData, index) { // esRawDocHandler(seriesData, index) {
// let xField = this.panel.xaxis.esField; // let xField = this.panel.xaxis.esField;
@ -160,7 +156,7 @@ export class DataProcessor {
// var alias = valueField; // var alias = valueField;
// return this.seriesHandler(seriesData, index, datapoints, alias); // return this.seriesHandler(seriesData, index, datapoints, alias);
// } // }
//
validateXAxisSeriesValue() { validateXAxisSeriesValue() {
switch (this.panel.xaxis.mode) { switch (this.panel.xaxis.mode) {
case 'series': { case 'series': {

View File

@ -121,20 +121,20 @@ function ($, _) {
var seriesList = getSeriesFn(); var seriesList = getSeriesFn();
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; 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) { if (dashboard.sharedCrosshair) {
ctrl.publishAppEvent('setCrosshair', { pos: pos, scope: scope }); ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope});
} }
if (seriesList.length === 0) { if (seriesList.length === 0) {
return; 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) { if (panel.tooltip.shared) {
plot.unhighlight(); plot.unhighlight();

View File

@ -25,7 +25,6 @@ class GraphCtrl extends MetricsPanelCtrl {
annotationsPromise: any; annotationsPromise: any;
datapointsCount: number; datapointsCount: number;
datapointsOutside: boolean; datapointsOutside: boolean;
datapointsWarning: boolean;
colors: any = []; colors: any = [];
subTabIndex: number; subTabIndex: number;
processor: DataProcessor; processor: DataProcessor;
@ -172,13 +171,20 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
onDataReceived(dataList) { onDataReceived(dataList) {
this.datapointsWarning = false;
this.datapointsCount = 0;
this.datapointsOutside = false;
this.dataList = dataList; this.dataList = dataList;
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range}); 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.annotationsPromise.then(annotations => {
this.loading = false; this.loading = false;

View File

@ -3,6 +3,7 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common'; import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import angular from 'angular'; import angular from 'angular';
import moment from 'moment';
import {GraphCtrl} from '../module'; import {GraphCtrl} from '../module';
import helpers from '../../../../../test/specs/helpers'; import helpers from '../../../../../test/specs/helpers';
@ -19,64 +20,53 @@ describe('GraphCtrl', function() {
ctx.ctrl.updateTimeRange(); ctx.ctrl.updateTimeRange();
}); });
describe.skip('msResolution with second resolution timestamps', function() { describe('when time series are outside range', function() {
beforeEach(function() { beforeEach(function() {
var data = [ var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]}, {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890], [90, 1234456709]]}
]; ];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.range = {from: moment().valueOf(), to: moment().valueOf()};
ctx.ctrl.onDataReceived(data); ctx.ctrl.onDataReceived(data);
}); });
it('should not show millisecond resolution tooltip', function() { it('should set datapointsOutside', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false); 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() { beforeEach(function() {
var range = {
from: moment().subtract(1, 'days').valueOf(),
to: moment().valueOf()
};
var data = [ var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, {target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890001], [90, 1234456709000]]}
]; ];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.range = range;
ctx.ctrl.onDataReceived(data); ctx.ctrl.onDataReceived(data);
}); });
it('should show millisecond resolution tooltip', function() { it('should set datapointsOutside', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true); 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() { beforeEach(function() {
var data = [ var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890000], [90, 1234456709000]]} {target: 'test.cpu2', datapoints: [[45, 1234567890]]},
]; ];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data); ctx.ctrl.onDataReceived(data);
}); });
it('should not show millisecond resolution tooltip', function() { it('should set datapointsCount to sum of datapoints', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false); expect(ctx.ctrl.datapointsCount).to.be(3);
});
});
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);
}); });
}); });

View File

@ -2,11 +2,14 @@ var template = `
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}"> <div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
<div class="graph-canvas-wrapper"> <div class="graph-canvas-wrapper">
<div ng-if="datapointsWarning" class="datapoints-warning"> <div class="datapoints-warning" ng-show="ctrl.datapointsCount===0">
<span class="small" ng-show="!datapointsCount"> <span class="small" >
No datapoints <tip>No datapoints returned from metric query</tip> No datapoints <tip>No datapoints returned from metric query</tip>
</span> </span>
<span class="small" ng-show="datapointsOutside"> </div>
<div class="datapoints-warning" ng-show="ctrl.datapointsOutside">
<span class="small">
Datapoints outside time range Datapoints outside time range
<tip>Can be caused by timezone mismatch between browser and graphite server</tip> <tip>Can be caused by timezone mismatch between browser and graphite server</tip>
</span> </span>

View File

@ -7,7 +7,7 @@
"author": { "author": {
"name": "Grafana Project", "name": "Grafana Project",
"url": "http://grafana.org" "url": "http://grafana.org"
}, },
"logos": { "logos": {
"small": "img/icn-dashlist-panel.svg", "small": "img/icn-dashlist-panel.svg",
"large": "img/icn-dashlist-panel.svg" "large": "img/icn-dashlist-panel.svg"

View File

@ -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() { describe('can detect if series contains ms precision', function() {
var fakedata; var fakedata;

View File

@ -132,62 +132,64 @@ define([
describe('calculateInterval', function() { describe('calculateInterval', function() {
it('1h 100 resultion', function() { it('1h 100 resultion', function() {
var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 100, null); var res = kbn.calculateInterval(range, 100, null);
expect(str).to.be('30s'); expect(res.interval).to.be('30s');
}); });
it('10m 1600 resolution', function() { it('10m 1600 resolution', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1600, null); var res = kbn.calculateInterval(range, 1600, null);
expect(str).to.be('500ms'); expect(res.interval).to.be('500ms');
expect(res.intervalMs).to.be(500);
}); });
it('fixed user interval', function() { it('fixed user interval', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1600, '10s'); var res = kbn.calculateInterval(range, 1600, '10s');
expect(str).to.be('10s'); expect(res.interval).to.be('10s');
expect(res.intervalMs).to.be(10000);
}); });
it('short time range and user low limit', function() { it('short time range and user low limit', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1600, '>10s'); var res = kbn.calculateInterval(range, 1600, '>10s');
expect(str).to.be('10s'); expect(res.interval).to.be('10s');
}); });
it('large time range and user low limit', function() { it('large time range and user low limit', function() {
var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')};
var str = kbn.calculateInterval(range, 1000, '>10s'); var res = kbn.calculateInterval(range, 1000, '>10s');
expect(str).to.be('20m'); expect(res.interval).to.be('20m');
}); });
it('10s 900 resolution and user low limit in ms', function() { it('10s 900 resolution and user low limit in ms', function() {
var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 900, '>15ms'); var res = kbn.calculateInterval(range, 900, '>15ms');
expect(str).to.be('15ms'); expect(res.interval).to.be('15ms');
}); });
}); });
describe('hex', function() { describe('hex', function() {
it('positive integer', function() { it('positive integer', function() {
var str = kbn.valueFormats.hex(100, 0); var str = kbn.valueFormats.hex(100, 0);
expect(str).to.be('64'); expect(str).to.be('64');
}); });
it('negative integer', function() { it('negative integer', function() {
var str = kbn.valueFormats.hex(-100, 0); var str = kbn.valueFormats.hex(-100, 0);
expect(str).to.be('-64'); expect(str).to.be('-64');
}); });
it('null', function() { it('null', function() {
var str = kbn.valueFormats.hex(null, 0); var str = kbn.valueFormats.hex(null, 0);
expect(str).to.be(''); expect(str).to.be('');
}); });
it('positive float', function() { it('positive float', function() {
var str = kbn.valueFormats.hex(50.52, 1); var str = kbn.valueFormats.hex(50.52, 1);
expect(str).to.be('32.8'); expect(str).to.be('32.8');
}); });
it('negative float', function() { it('negative float', function() {
var str = kbn.valueFormats.hex(-50.333, 2); var str = kbn.valueFormats.hex(-50.333, 2);
expect(str).to.be('-32.547AE147AE14'); expect(str).to.be('-32.547AE147AE14');
}); });
}); });
describe('hex 0x', function() { describe('hex 0x', function() {