mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 07:32:13 +08:00
feat(testdata): worked on testdata app
This commit is contained in:
@ -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 {
|
||||
|
@ -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"})
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
result := dtos.MetricQueryResultDto{}
|
||||
result.Data = make([]dtos.MetricQueryResultDataDto, 1)
|
||||
timeRange := tsdb.NewTimeRange(c.Query("from"), c.Query("to"))
|
||||
|
||||
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
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
|
||||
result.Data[seriesIndex].DataPoints = points
|
||||
resp, err := tsdb.HandleRequest(req)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
}
|
||||
|
||||
result := dtos.MetricQueryResultDto{}
|
||||
|
||||
for _, v := range resp.Results {
|
||||
if v.Error != nil {
|
||||
return ApiError(500, "tsdb.HandleRequest() response error", v.Error)
|
||||
}
|
||||
|
||||
for _, series := range v.Series {
|
||||
result.Data = append(result.Data, series)
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, &result)
|
||||
|
@ -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
|
||||
|
||||
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() {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -10,13 +10,14 @@ type Query struct {
|
||||
DataSource *DataSourceInfo
|
||||
Results []*TimeSeries
|
||||
Exclude bool
|
||||
MaxDataPoints int64
|
||||
IntervalMs int64
|
||||
}
|
||||
|
||||
type QuerySlice []*Query
|
||||
|
||||
type Request struct {
|
||||
TimeRange TimeRange
|
||||
MaxDataPoints int
|
||||
Queries QuerySlice
|
||||
}
|
||||
|
||||
|
54
pkg/tsdb/testdata/testdata.go
vendored
Normal file
54
pkg/tsdb/testdata/testdata.go
vendored
Normal 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, ×tamp})
|
||||
|
||||
walker += rand.Float64() - 0.5
|
||||
time += stepInSeconds
|
||||
}
|
||||
|
||||
series.Points = points
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
}
|
||||
|
||||
result.QueryResults["A"] = queryRes
|
||||
return result
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
5
public/app/plugins/app/testdata/dashboards/graph_last_1h.json
vendored
Normal file
5
public/app/plugins/app/testdata/dashboards/graph_last_1h.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "TestData - Graph Panel Last 1h",
|
||||
"tags": ["testdata"],
|
||||
"revision": 1
|
||||
}
|
45
public/app/plugins/app/testdata/datasource/datasource.ts
vendored
Normal file
45
public/app/plugins/app/testdata/datasource/datasource.ts
vendored
Normal 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};
|
22
public/app/plugins/app/testdata/datasource/module.ts
vendored
Normal file
22
public/app/plugins/app/testdata/datasource/module.ts
vendored
Normal 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,
|
||||
};
|
||||
|
19
public/app/plugins/app/testdata/datasource/plugin.json
vendored
Normal file
19
public/app/plugins/app/testdata/datasource/plugin.json
vendored
Normal 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": ""
|
||||
}
|
||||
}
|
||||
}
|
24
public/app/plugins/app/testdata/datasource/query_ctrl.ts
vendored
Normal file
24
public/app/plugins/app/testdata/datasource/query_ctrl.ts
vendored
Normal 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'},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
36
public/app/plugins/app/testdata/module.ts
vendored
Normal file
36
public/app/plugins/app/testdata/module.ts
vendored
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
22
public/app/plugins/app/testdata/partials/query.editor.html
vendored
Normal file
22
public/app/plugins/app/testdata/partials/query.editor.html
vendored
Normal 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>
|
||||
|
27
public/app/plugins/app/testdata/plugin.json
vendored
Normal file
27
public/app/plugins/app/testdata/plugin.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
||||
@ -29,7 +30,9 @@ export class DataProcessor {
|
||||
switch (this.panel.xaxis.mode) {
|
||||
case 'series':
|
||||
case 'time': {
|
||||
return options.dataList.map(this.timeSeriesHandler.bind(this));
|
||||
return options.dataList.map((item, index) => {
|
||||
return this.timeSeriesHandler(item, index, options);
|
||||
});
|
||||
}
|
||||
case 'field': {
|
||||
return this.customHandler(firstItem);
|
||||
@ -74,31 +77,24 @@ 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();
|
||||
// }
|
||||
|
||||
return series;
|
||||
if (datapoints && datapoints.length > 0) {
|
||||
var last = datapoints[datapoints.length - 1][1];
|
||||
var from = options.range.from;
|
||||
if (last - from < -10000) {
|
||||
series.isOutsideRange = true;
|
||||
}
|
||||
}
|
||||
|
||||
timeSeriesHandler(seriesData, index) {
|
||||
var datapoints = seriesData.datapoints;
|
||||
var alias = seriesData.target;
|
||||
|
||||
return this.seriesHandler(seriesData, index, datapoints, alias);
|
||||
return series;
|
||||
}
|
||||
|
||||
customHandler(dataItem) {
|
||||
@ -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': {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,11 +2,14 @@ var template = `
|
||||
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
|
||||
<div class="graph-canvas-wrapper">
|
||||
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">
|
||||
<div class="datapoints-warning" ng-show="ctrl.datapointsCount===0">
|
||||
<span class="small" >
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
</span>
|
||||
<span class="small" ng-show="datapointsOutside">
|
||||
</div>
|
||||
|
||||
<div class="datapoints-warning" ng-show="ctrl.datapointsOutside">
|
||||
<span class="small">
|
||||
Datapoints outside time range
|
||||
<tip>Can be caused by timezone mismatch between browser and graphite server</tip>
|
||||
</span>
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
||||
|
@ -132,38 +132,40 @@ 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');
|
||||
});
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user