mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into go_routines
This commit is contained in:
commit
bc634f20d5
@ -244,7 +244,8 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/search/", Search)
|
||||
|
||||
// metrics
|
||||
r.Get("/metrics/test", wrap(GetTestMetrics))
|
||||
r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
|
||||
r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
|
||||
|
||||
// metrics
|
||||
r.Get("/metrics", wrap(GetInternalMetrics))
|
||||
|
@ -96,13 +96,10 @@ func (slice DataSourceList) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
type MetricQueryResultDto struct {
|
||||
Data []MetricQueryResultDataDto `json:"data"`
|
||||
}
|
||||
|
||||
type MetricQueryResultDataDto struct {
|
||||
Target string `json:"target"`
|
||||
DataPoints [][2]float64 `json:"datapoints"`
|
||||
type MetricRequest struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Queries []*simplejson.Json `json:"queries"`
|
||||
}
|
||||
|
||||
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,54 @@ 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/tsdb/testdata"
|
||||
"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
|
||||
// POST /api/tsdb/query
|
||||
func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
|
||||
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
|
||||
|
||||
result := dtos.MetricQueryResultDto{}
|
||||
result.Data = make([]dtos.MetricQueryResultDataDto, 1)
|
||||
request := &tsdb.Request{TimeRange: timeRange}
|
||||
|
||||
for seriesIndex := range result.Data {
|
||||
points := make([][2]float64, maxDataPoints)
|
||||
walker := rand.Float64() * 100
|
||||
time := from
|
||||
for _, query := range reqDto.Queries {
|
||||
request.Queries = append(request.Queries, &tsdb.Query{
|
||||
RefId: query.Get("refId").MustString("A"),
|
||||
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
|
||||
IntervalMs: query.Get("intervalMs").MustInt64(1000),
|
||||
Model: query,
|
||||
DataSource: &tsdb.DataSourceInfo{
|
||||
Name: "Grafana TestDataDB",
|
||||
PluginId: "grafana-testdata-datasource",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i := range points {
|
||||
points[i][0] = walker
|
||||
points[i][1] = float64(time)
|
||||
walker += rand.Float64() - 0.5
|
||||
time += stepInSeconds
|
||||
}
|
||||
resp, err := tsdb.HandleRequest(request)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
}
|
||||
|
||||
result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
|
||||
result.Data[seriesIndex].DataPoints = points
|
||||
return Json(200, &resp)
|
||||
}
|
||||
|
||||
// GET /api/tsdb/testdata/scenarios
|
||||
func GetTestDataScenarios(c *middleware.Context) Response {
|
||||
result := make([]interface{}, 0)
|
||||
|
||||
for _, scenario := range testdata.ScenarioRegistry {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": scenario.Id,
|
||||
"name": scenario.Name,
|
||||
"description": scenario.Description,
|
||||
"stringInput": scenario.StringInput,
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, &result)
|
||||
|
@ -6,6 +6,7 @@ type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
Mixed bool `json:"mixed"`
|
||||
App string `json:"app"`
|
||||
|
@ -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() {
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -13,13 +14,13 @@ var (
|
||||
)
|
||||
|
||||
type AlertEvaluator interface {
|
||||
Eval(reducedValue *float64) bool
|
||||
Eval(reducedValue null.Float) bool
|
||||
}
|
||||
|
||||
type NoDataEvaluator struct{}
|
||||
|
||||
func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
|
||||
return reducedValue == nil
|
||||
func (e *NoDataEvaluator) Eval(reducedValue null.Float) bool {
|
||||
return reducedValue.Valid == false
|
||||
}
|
||||
|
||||
type ThresholdEvaluator struct {
|
||||
@ -43,16 +44,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
|
||||
return defaultEval, nil
|
||||
}
|
||||
|
||||
func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
|
||||
if reducedValue == nil {
|
||||
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
return false
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case "gt":
|
||||
return *reducedValue > e.Threshold
|
||||
return reducedValue.Float64 > e.Threshold
|
||||
case "lt":
|
||||
return *reducedValue < e.Threshold
|
||||
return reducedValue.Float64 < e.Threshold
|
||||
}
|
||||
|
||||
return false
|
||||
@ -86,16 +87,18 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
|
||||
return rangedEval, nil
|
||||
}
|
||||
|
||||
func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
|
||||
if reducedValue == nil {
|
||||
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
return false
|
||||
}
|
||||
|
||||
floatValue := reducedValue.Float64
|
||||
|
||||
switch e.Type {
|
||||
case "within_range":
|
||||
return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
|
||||
return (e.Lower < floatValue && e.Upper > floatValue) || (e.Upper < floatValue && e.Lower > floatValue)
|
||||
case "outside_range":
|
||||
return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
|
||||
return (e.Upper < floatValue && e.Lower < floatValue) || (e.Upper > floatValue && e.Lower > floatValue)
|
||||
}
|
||||
|
||||
return false
|
||||
|
@ -3,6 +3,8 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
@ -14,7 +16,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
|
||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return evaluator.Eval(&reducedValue)
|
||||
return evaluator.Eval(null.FloatFrom(reducedValue))
|
||||
}
|
||||
|
||||
func TestEvalutors(t *testing.T) {
|
||||
@ -51,6 +53,6 @@ func TestEvalutors(t *testing.T) {
|
||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(evaluator.Eval(nil), ShouldBeTrue)
|
||||
So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
@ -46,21 +46,21 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
|
||||
reducedValue := c.Reducer.Reduce(series)
|
||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||
|
||||
if reducedValue == nil {
|
||||
if reducedValue.Valid == false {
|
||||
emptySerieCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if context.IsTestRun {
|
||||
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
|
||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue),
|
||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64),
|
||||
})
|
||||
}
|
||||
|
||||
if evalMatch {
|
||||
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
|
||||
Metric: series.Name,
|
||||
Value: *reducedValue,
|
||||
Value: reducedValue.Float64,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
@ -105,9 +105,9 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange t
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timerange tsdb.TimeRange) *tsdb.Request {
|
||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request {
|
||||
req := &tsdb.Request{
|
||||
TimeRange: timerange,
|
||||
TimeRange: timeRange,
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
RefId: "A",
|
||||
|
@ -3,6 +3,8 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
null "gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -41,9 +43,8 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("should fire when avg is above 100", func() {
|
||||
one := float64(120)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
|
||||
points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
|
||||
ctx.exec()
|
||||
|
||||
So(ctx.result.Error, ShouldBeNil)
|
||||
@ -51,9 +52,8 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should not fire when avg is below 100", func() {
|
||||
one := float64(90)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
|
||||
points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
|
||||
ctx.exec()
|
||||
|
||||
So(ctx.result.Error, ShouldBeNil)
|
||||
@ -61,11 +61,9 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should fire if only first serie matches", func() {
|
||||
one := float64(120)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{{&two, &two}}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
|
||||
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
@ -76,8 +74,8 @@ func TestQueryCondition(t *testing.T) {
|
||||
Convey("Empty series", func() {
|
||||
Convey("Should set NoDataFound both series are empty", func() {
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
@ -86,10 +84,9 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should set NoDataFound both series contains null", func() {
|
||||
one := float64(120)
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{{nil, &one}}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{{nil, &one}}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
|
||||
tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
@ -98,11 +95,9 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should not set NoDataFound if one serie is empty", func() {
|
||||
one := float64(120)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{{&one, &two}}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
|
@ -4,19 +4,20 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
type QueryReducer interface {
|
||||
Reduce(timeSeries *tsdb.TimeSeries) *float64
|
||||
Reduce(timeSeries *tsdb.TimeSeries) null.Float
|
||||
}
|
||||
|
||||
type SimpleReducer struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
if len(series.Points) == 0 {
|
||||
return nil
|
||||
return null.FloatFromPtr(nil)
|
||||
}
|
||||
|
||||
value := float64(0)
|
||||
@ -25,36 +26,36 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||
switch s.Type {
|
||||
case "avg":
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
value += *point[0]
|
||||
if point[0].Valid {
|
||||
value += point[0].Float64
|
||||
allNull = false
|
||||
}
|
||||
}
|
||||
value = value / float64(len(series.Points))
|
||||
case "sum":
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
value += *point[0]
|
||||
if point[0].Valid {
|
||||
value += point[0].Float64
|
||||
allNull = false
|
||||
}
|
||||
}
|
||||
case "min":
|
||||
value = math.MaxFloat64
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
if point[0].Valid {
|
||||
allNull = false
|
||||
if value > *point[0] {
|
||||
value = *point[0]
|
||||
if value > point[0].Float64 {
|
||||
value = point[0].Float64
|
||||
}
|
||||
}
|
||||
}
|
||||
case "max":
|
||||
value = -math.MaxFloat64
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
if point[0].Valid {
|
||||
allNull = false
|
||||
if value < *point[0] {
|
||||
value = *point[0]
|
||||
if value < point[0].Float64 {
|
||||
value = point[0].Float64
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,10 +65,10 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||
}
|
||||
|
||||
if allNull {
|
||||
return nil
|
||||
return null.FloatFromPtr(nil)
|
||||
}
|
||||
|
||||
return &value
|
||||
return null.FloatFrom(value)
|
||||
}
|
||||
|
||||
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||
|
@ -10,44 +10,41 @@ import (
|
||||
func TestSimpleReducer(t *testing.T) {
|
||||
Convey("Test simple reducer by calculating", t, func() {
|
||||
Convey("avg", func() {
|
||||
result := *testReducer("avg", 1, 2, 3)
|
||||
result := testReducer("avg", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(2))
|
||||
})
|
||||
|
||||
Convey("sum", func() {
|
||||
result := *testReducer("sum", 1, 2, 3)
|
||||
result := testReducer("sum", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(6))
|
||||
})
|
||||
|
||||
Convey("min", func() {
|
||||
result := *testReducer("min", 3, 2, 1)
|
||||
result := testReducer("min", 3, 2, 1)
|
||||
So(result, ShouldEqual, float64(1))
|
||||
})
|
||||
|
||||
Convey("max", func() {
|
||||
result := *testReducer("max", 1, 2, 3)
|
||||
result := testReducer("max", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
|
||||
Convey("count", func() {
|
||||
result := *testReducer("count", 1, 2, 3000)
|
||||
result := testReducer("count", 1, 2, 3000)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testReducer(typ string, datapoints ...float64) *float64 {
|
||||
func testReducer(typ string, datapoints ...float64) float64 {
|
||||
reducer := NewSimpleReducer(typ)
|
||||
var timeserie [][2]*float64
|
||||
dummieTimestamp := float64(521452145)
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
for idx := range datapoints {
|
||||
timeserie = append(timeserie, [2]*float64{&datapoints[idx], &dummieTimestamp})
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(datapoints[idx], 1234134))
|
||||
}
|
||||
|
||||
tsdb := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
Points: timeserie,
|
||||
}
|
||||
return reducer.Reduce(tsdb)
|
||||
return reducer.Reduce(series).Float64
|
||||
}
|
||||
|
@ -9,6 +9,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
|
||||
|
@ -79,7 +79,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
}
|
||||
|
||||
result.QueryResults = make(map[string]*tsdb.QueryResult)
|
||||
queryRes := &tsdb.QueryResult{}
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
for _, series := range data {
|
||||
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
||||
Name: series.Target,
|
||||
|
@ -1,6 +1,8 @@
|
||||
package graphite
|
||||
|
||||
import "github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
type TargetResponseDTO struct {
|
||||
Target string `json:"target"`
|
||||
DataPoints [][2]*float64 `json:"datapoints"`
|
||||
Target string `json:"target"`
|
||||
DataPoints tsdb.TimeSeriesPoints `json:"datapoints"`
|
||||
}
|
||||
|
@ -1,28 +1,31 @@
|
||||
package tsdb
|
||||
|
||||
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
RefId string
|
||||
Query string
|
||||
Model *simplejson.Json
|
||||
Depends []string
|
||||
DataSource *DataSourceInfo
|
||||
Results []*TimeSeries
|
||||
Exclude bool
|
||||
RefId 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 {
|
||||
BatchTimings []*BatchTiming
|
||||
Results map[string]*QueryResult
|
||||
BatchTimings []*BatchTiming `json:"timings"`
|
||||
Results map[string]*QueryResult `json:"results"`
|
||||
}
|
||||
|
||||
type DataSourceInfo struct {
|
||||
@ -49,19 +52,41 @@ type BatchResult struct {
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Error error
|
||||
RefId string
|
||||
Series TimeSeriesSlice
|
||||
Error error `json:"error"`
|
||||
RefId string `json:"refId"`
|
||||
Series TimeSeriesSlice `json:"series"`
|
||||
}
|
||||
|
||||
type TimeSeries struct {
|
||||
Name string `json:"name"`
|
||||
Points [][2]*float64 `json:"points"`
|
||||
Name string `json:"name"`
|
||||
Points TimeSeriesPoints `json:"points"`
|
||||
}
|
||||
|
||||
type TimePoint [2]null.Float
|
||||
type TimeSeriesPoints []TimePoint
|
||||
type TimeSeriesSlice []*TimeSeries
|
||||
|
||||
func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
|
||||
func NewQueryResult() *QueryResult {
|
||||
return &QueryResult{
|
||||
Series: make(TimeSeriesSlice, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func NewTimePoint(value float64, timestamp float64) TimePoint {
|
||||
return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)}
|
||||
}
|
||||
|
||||
func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints {
|
||||
points := make(TimeSeriesPoints, 0)
|
||||
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
points = append(points, NewTimePoint(values[i], values[i+1]))
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
func NewTimeSeries(name string, points TimeSeriesPoints) *TimeSeries {
|
||||
return &TimeSeries{
|
||||
Name: name,
|
||||
Points: points,
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/prometheus/client_golang/api/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
pmodel "github.com/prometheus/common/model"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type PrometheusExecutor struct {
|
||||
@ -111,12 +111,12 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start, err := queryContext.TimeRange.FromTime()
|
||||
start, err := queryContext.TimeRange.ParseFrom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
end, err := queryContext.TimeRange.ToTime()
|
||||
end, err := queryContext.TimeRange.ParseTo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,7 +132,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
|
||||
|
||||
func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) {
|
||||
queryResults := make(map[string]*tsdb.QueryResult)
|
||||
queryRes := &tsdb.QueryResult{}
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
data, ok := value.(pmodel.Matrix)
|
||||
if !ok {
|
||||
@ -140,17 +140,15 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
var points [][2]*float64
|
||||
for _, k := range v.Values {
|
||||
timestamp := float64(k.Timestamp)
|
||||
val := float64(k.Value)
|
||||
points = append(points, [2]*float64{&val, ×tamp})
|
||||
series := tsdb.TimeSeries{
|
||||
Name: formatLegend(v.Metric, query),
|
||||
}
|
||||
|
||||
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
||||
Name: formatLegend(v.Metric, query),
|
||||
Points: points,
|
||||
})
|
||||
for _, k := range v.Values {
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(float64(k.Value), float64(k.Timestamp.Unix()*1000)))
|
||||
}
|
||||
|
||||
queryRes.Series = append(queryRes.Series, &series)
|
||||
}
|
||||
|
||||
queryResults["A"] = queryRes
|
||||
|
@ -3,7 +3,7 @@ package tsdb
|
||||
import "sync"
|
||||
|
||||
type QueryContext struct {
|
||||
TimeRange TimeRange
|
||||
TimeRange *TimeRange
|
||||
Queries QuerySlice
|
||||
Results map[string]*QueryResult
|
||||
ResultsChan chan *BatchResult
|
||||
@ -11,7 +11,7 @@ type QueryContext struct {
|
||||
BatchWaits sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewQueryContext(queries QuerySlice, timeRange TimeRange) *QueryContext {
|
||||
func NewQueryContext(queries QuerySlice, timeRange *TimeRange) *QueryContext {
|
||||
return &QueryContext{
|
||||
TimeRange: timeRange,
|
||||
Queries: queries,
|
||||
|
130
pkg/tsdb/testdata/scenarios.go
vendored
Normal file
130
pkg/tsdb/testdata/scenarios.go
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult
|
||||
|
||||
type Scenario struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StringInput string `json:"stringOption"`
|
||||
Description string `json:"description"`
|
||||
Handler ScenarioHandler `json:"-"`
|
||||
}
|
||||
|
||||
var ScenarioRegistry map[string]*Scenario
|
||||
|
||||
func init() {
|
||||
ScenarioRegistry = make(map[string]*Scenario)
|
||||
logger := log.New("tsdb.testdata")
|
||||
|
||||
logger.Debug("Initializing TestData Scenario")
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "random_walk",
|
||||
Name: "Random Walk",
|
||||
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
timeWalkerMs := context.TimeRange.GetFromAsMsEpoch()
|
||||
to := context.TimeRange.GetToAsMsEpoch()
|
||||
|
||||
series := newSeriesForQuery(query)
|
||||
|
||||
points := make(tsdb.TimeSeriesPoints, 0)
|
||||
walker := rand.Float64() * 100
|
||||
|
||||
for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
|
||||
points = append(points, tsdb.NewTimePoint(walker, float64(timeWalkerMs)))
|
||||
|
||||
walker += rand.Float64() - 0.5
|
||||
timeWalkerMs += query.IntervalMs
|
||||
}
|
||||
|
||||
series.Points = points
|
||||
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
return queryRes
|
||||
},
|
||||
})
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "no_data_points",
|
||||
Name: "No Data Points",
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
return tsdb.NewQueryResult()
|
||||
},
|
||||
})
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "datapoints_outside_range",
|
||||
Name: "Datapoints Outside Range",
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
series := newSeriesForQuery(query)
|
||||
outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(10, float64(outsideTime)))
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
|
||||
return queryRes
|
||||
},
|
||||
})
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "csv_metric_values",
|
||||
Name: "CSV Metric Values",
|
||||
StringInput: "1,20,90,30,5,0",
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
stringInput := query.Model.Get("stringInput").MustString()
|
||||
values := []float64{}
|
||||
for _, strVal := range strings.Split(stringInput, ",") {
|
||||
if val, err := strconv.ParseFloat(strVal, 64); err == nil {
|
||||
values = append(values, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return queryRes
|
||||
}
|
||||
|
||||
series := newSeriesForQuery(query)
|
||||
startTime := context.TimeRange.GetFromAsMsEpoch()
|
||||
endTime := context.TimeRange.GetToAsMsEpoch()
|
||||
step := (endTime - startTime) / int64(len(values)-1)
|
||||
|
||||
for _, val := range values {
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(val, float64(startTime)))
|
||||
startTime += step
|
||||
}
|
||||
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
|
||||
return queryRes
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func registerScenario(scenario *Scenario) {
|
||||
ScenarioRegistry[scenario.Id] = scenario
|
||||
}
|
||||
|
||||
func newSeriesForQuery(query *tsdb.Query) *tsdb.TimeSeries {
|
||||
alias := query.Model.Get("alias").MustString("")
|
||||
if alias == "" {
|
||||
alias = query.RefId + "-series"
|
||||
}
|
||||
|
||||
return &tsdb.TimeSeries{Name: alias}
|
||||
}
|
39
pkg/tsdb/testdata/testdata.go
vendored
Normal file
39
pkg/tsdb/testdata/testdata.go
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
type TestDataExecutor struct {
|
||||
*tsdb.DataSourceInfo
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
|
||||
return &TestDataExecutor{
|
||||
DataSourceInfo: dsInfo,
|
||||
log: log.New("tsdb.testdata"),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
for _, query := range queries {
|
||||
scenarioId := query.Model.Get("scenarioId").MustString("random_walk")
|
||||
if scenario, exist := ScenarioRegistry[scenarioId]; exist {
|
||||
result.QueryResults[query.RefId] = scenario.Handler(query, context)
|
||||
result.QueryResults[query.RefId].RefId = query.RefId
|
||||
} else {
|
||||
e.log.Error("Scenario not found", "scenarioId", scenarioId)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -2,12 +2,13 @@ package tsdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewTimerange(from, to string) TimeRange {
|
||||
return TimeRange{
|
||||
func NewTimeRange(from, to string) *TimeRange {
|
||||
return &TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
Now: time.Now(),
|
||||
@ -20,9 +21,45 @@ type TimeRange struct {
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
func (tr TimeRange) FromTime() (time.Time, error) {
|
||||
fromRaw := strings.Replace(tr.From, "now-", "", 1)
|
||||
func (tr *TimeRange) GetFromAsMsEpoch() int64 {
|
||||
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) GetToAsMsEpoch() int64 {
|
||||
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) MustGetFrom() time.Time {
|
||||
if res, err := tr.ParseFrom(); err != nil {
|
||||
return time.Unix(0, 0)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *TimeRange) MustGetTo() time.Time {
|
||||
if res, err := tr.ParseTo(); err != nil {
|
||||
return time.Unix(0, 0)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func tryParseUnixMsEpoch(val string) (time.Time, bool) {
|
||||
if val, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
seconds := val / 1000
|
||||
nano := (val - seconds*1000) * 1000000
|
||||
return time.Unix(seconds, nano), true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseFrom() (time.Time, error) {
|
||||
if res, ok := tryParseUnixMsEpoch(tr.From); ok {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
fromRaw := strings.Replace(tr.From, "now-", "", 1)
|
||||
diff, err := time.ParseDuration("-" + fromRaw)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
@ -31,7 +68,7 @@ func (tr TimeRange) FromTime() (time.Time, error) {
|
||||
return tr.Now.Add(diff), nil
|
||||
}
|
||||
|
||||
func (tr TimeRange) ToTime() (time.Time, error) {
|
||||
func (tr *TimeRange) ParseTo() (time.Time, error) {
|
||||
if tr.To == "now" {
|
||||
return tr.Now, nil
|
||||
} else if strings.HasPrefix(tr.To, "now-") {
|
||||
@ -45,5 +82,9 @@ func (tr TimeRange) ToTime() (time.Time, error) {
|
||||
return tr.Now.Add(diff), nil
|
||||
}
|
||||
|
||||
if res, ok := tryParseUnixMsEpoch(tr.To); ok {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
|
||||
}
|
||||
|
@ -23,13 +23,13 @@ func TestTimeRange(t *testing.T) {
|
||||
fiveMinAgo, _ := time.ParseDuration("-5m")
|
||||
expected := now.Add(fiveMinAgo)
|
||||
|
||||
res, err := tr.FromTime()
|
||||
res, err := tr.ParseFrom()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, expected.Unix())
|
||||
})
|
||||
|
||||
Convey("now ", func() {
|
||||
res, err := tr.ToTime()
|
||||
res, err := tr.ParseTo()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, now.Unix())
|
||||
})
|
||||
@ -46,7 +46,7 @@ func TestTimeRange(t *testing.T) {
|
||||
fiveHourAgo, _ := time.ParseDuration("-5h")
|
||||
expected := now.Add(fiveHourAgo)
|
||||
|
||||
res, err := tr.FromTime()
|
||||
res, err := tr.ParseFrom()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, expected.Unix())
|
||||
})
|
||||
@ -54,12 +54,29 @@ func TestTimeRange(t *testing.T) {
|
||||
Convey("now-10m ", func() {
|
||||
fiveMinAgo, _ := time.ParseDuration("-10m")
|
||||
expected := now.Add(fiveMinAgo)
|
||||
res, err := tr.ToTime()
|
||||
res, err := tr.ParseTo()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, expected.Unix())
|
||||
})
|
||||
})
|
||||
|
||||
Convey("can parse unix epocs", func() {
|
||||
var err error
|
||||
tr := TimeRange{
|
||||
From: "1474973725473",
|
||||
To: "1474975757930",
|
||||
Now: now,
|
||||
}
|
||||
|
||||
res, err := tr.ParseFrom()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474973725473)
|
||||
|
||||
res, err = tr.ParseTo()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474975757930)
|
||||
})
|
||||
|
||||
Convey("Cannot parse asdf", func() {
|
||||
var err error
|
||||
tr := TimeRange{
|
||||
@ -68,10 +85,10 @@ func TestTimeRange(t *testing.T) {
|
||||
Now: now,
|
||||
}
|
||||
|
||||
_, err = tr.FromTime()
|
||||
_, err = tr.ParseFrom()
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
_, err = tr.ToTime()
|
||||
_, err = tr.ParseTo()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
@ -14,9 +14,9 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("Given 3 queries for 2 data sources", func() {
|
||||
request := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "C", DataSource: &DataSourceInfo{Id: 2}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -31,9 +31,9 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("Given query 2 depends on query 1", func() {
|
||||
request := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
|
||||
{RefId: "C", Query: "#A / #B", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 2}},
|
||||
{RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When executing request with one query", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -74,8 +74,8 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When executing one request with two queries from same data source", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -100,9 +100,9 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When executing one request with three queries from different datasources", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When query uses data source of unknown type", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -129,10 +129,10 @@ func TestMetricQuery(t *testing.T) {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{
|
||||
RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
|
||||
RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
|
||||
},
|
||||
{
|
||||
RefId: "B", Query: "#A / 2", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
|
||||
RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import 'app/core/routes/routes';
|
||||
import './filters/filters';
|
||||
import coreModule from './core_module';
|
||||
import appEvents from './app_events';
|
||||
import colors from './utils/colors';
|
||||
|
||||
|
||||
export {
|
||||
@ -60,4 +61,5 @@ export {
|
||||
dashboardSelector,
|
||||
queryPartEditorDirective,
|
||||
WizardFlow,
|
||||
colors,
|
||||
};
|
||||
|
@ -23,10 +23,10 @@ function (_, $, coreModule) {
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
},
|
||||
link: function($scope, elem, attrs) {
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var $button = $(attrs.styleMode === 'select' ? selectTemplate : linkTemplate);
|
||||
var segment = $scope.segment;
|
||||
var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
var linkMode = true;
|
||||
@ -170,6 +170,7 @@ function (_, $, coreModule) {
|
||||
},
|
||||
link: {
|
||||
pre: function postLink($scope, elem, attrs) {
|
||||
var cachedOptions;
|
||||
|
||||
$scope.valueToSegment = function(value) {
|
||||
var option = _.find($scope.options, {value: value});
|
||||
@ -177,7 +178,9 @@ function (_, $, coreModule) {
|
||||
cssClass: attrs.cssClass,
|
||||
custom: attrs.custom,
|
||||
value: option ? option.text : value,
|
||||
selectMode: attrs.selectMode,
|
||||
};
|
||||
|
||||
return uiSegmentSrv.newSegment(segment);
|
||||
};
|
||||
|
||||
@ -188,13 +191,20 @@ function (_, $, coreModule) {
|
||||
});
|
||||
return $q.when(optionSegments);
|
||||
} else {
|
||||
return $scope.getOptions();
|
||||
return $scope.getOptions().then(function(options) {
|
||||
cachedOptions = options;
|
||||
return _.map(options, function(option) {
|
||||
return uiSegmentSrv.newSegment({value: option.text});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onSegmentChange = function() {
|
||||
if ($scope.options) {
|
||||
var option = _.find($scope.options, {text: $scope.segment.value});
|
||||
var options = $scope.options || cachedOptions;
|
||||
|
||||
if (options) {
|
||||
var option = _.find(options, {text: $scope.segment.value});
|
||||
if (option && option.value !== $scope.property) {
|
||||
$scope.property = option.value;
|
||||
} else if (attrs.custom !== 'false') {
|
||||
|
@ -28,6 +28,7 @@ function (angular, _, coreModule) {
|
||||
this.type = options.type;
|
||||
this.fake = options.fake;
|
||||
this.value = options.value;
|
||||
this.selectMode = options.selectMode;
|
||||
this.type = options.type;
|
||||
this.expandable = options.expandable;
|
||||
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
|
@ -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) {
|
||||
|
12
public/app/core/utils/colors.ts
Normal file
12
public/app/core/utils/colors.ts
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
export default [
|
||||
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
|
||||
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
|
||||
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
|
||||
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
|
||||
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
|
||||
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
|
||||
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
|
||||
];
|
||||
|
@ -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) {
|
||||
|
@ -227,8 +227,8 @@ export class AlertTabCtrl {
|
||||
|
||||
var datasourceName = foundTarget.datasource || this.panel.datasource;
|
||||
this.datasourceSrv.get(datasourceName).then(ds => {
|
||||
if (ds.meta.id !== 'graphite' && ds.meta.id !== 'prometheus') {
|
||||
this.error = 'You datsource does not support alerting queries';
|
||||
if (!ds.meta.alerting) {
|
||||
this.error = 'The datasource does not support alerting queries';
|
||||
} else if (this.templateSrv.variableExists(foundTarget.target)) {
|
||||
this.error = 'Template variables are not supported in alert queries';
|
||||
} else {
|
||||
|
@ -30,6 +30,7 @@ export class DashboardModel {
|
||||
snapshot: any;
|
||||
schemaVersion: number;
|
||||
version: number;
|
||||
revision: number;
|
||||
links: any;
|
||||
gnetId: any;
|
||||
meta: any;
|
||||
@ -42,6 +43,7 @@ export class DashboardModel {
|
||||
|
||||
this.events = new Emitter();
|
||||
this.id = data.id || null;
|
||||
this.revision = data.revision;
|
||||
this.title = data.title || 'No Title';
|
||||
this.autoUpdate = data.autoUpdate;
|
||||
this.description = data.description;
|
||||
|
@ -16,7 +16,7 @@ var template = `
|
||||
Panel data source
|
||||
</label>
|
||||
|
||||
<metric-segment segment="ctrl.dsSegment" style-mode="select"
|
||||
<metric-segment segment="ctrl.dsSegment"
|
||||
get-options="ctrl.getOptions()"
|
||||
on-change="ctrl.datasourceChanged()"></metric-segment>
|
||||
</div>
|
||||
@ -67,7 +67,7 @@ export class MetricsDsSelectorCtrl {
|
||||
this.current = {name: dsValue + ' not found', value: null};
|
||||
}
|
||||
|
||||
this.dsSegment = uiSegmentSrv.newSegment(this.current.name);
|
||||
this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true});
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
|
@ -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,
|
||||
|
@ -10,6 +10,7 @@ export class DatasourceVariable implements Variable {
|
||||
query: string;
|
||||
options: any;
|
||||
current: any;
|
||||
refresh: any;
|
||||
|
||||
defaults = {
|
||||
type: 'datasource',
|
||||
@ -20,11 +21,13 @@ export class DatasourceVariable implements Variable {
|
||||
regex: '',
|
||||
options: [],
|
||||
query: '',
|
||||
refresh: 1,
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private model, private datasourceSrv, private variableSrv) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
this.refresh = 1;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
|
@ -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() {
|
||||
|
@ -62,6 +62,7 @@ describe('VariableSrv init', function() {
|
||||
options: [{text: "test", value: "test"}]
|
||||
}];
|
||||
scenario.urlParams["var-apps"] = "new";
|
||||
scenario.metricSources = [];
|
||||
});
|
||||
|
||||
it('should update current value', () => {
|
||||
@ -110,6 +111,30 @@ describe('VariableSrv init', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('when datasource variable is initialized', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [{
|
||||
type: 'datasource',
|
||||
query: 'graphite',
|
||||
name: 'test',
|
||||
current: {value: 'backend4_pee', text: 'backend4_pee'},
|
||||
regex: '/pee$/'
|
||||
}
|
||||
];
|
||||
scenario.metricSources = [
|
||||
{name: 'backend1', meta: {id: 'influx'}},
|
||||
{name: 'backend2_pee', meta: {id: 'graphite'}},
|
||||
{name: 'backend3', meta: {id: 'graphite'}},
|
||||
{name: 'backend4_pee', meta: {id: 'graphite'}},
|
||||
];
|
||||
});
|
||||
|
||||
it('should update current value', function() {
|
||||
var variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.options.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('when template variable is present in url multiple times', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [{
|
||||
|
287
public/app/plugins/app/testdata/dashboards/alerts.json
vendored
Normal file
287
public/app/plugins/app/testdata/dashboards/alerts.json
vendored
Normal file
@ -0,0 +1,287 @@
|
||||
{
|
||||
"revision": 2,
|
||||
"title": "TestData - Alerts",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": 255.625,
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
60
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always OK",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 60,
|
||||
"op": "gt",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"colorMode": "critical"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always OK",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": "125",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 13,
|
||||
"version": 4,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
483
public/app/plugins/app/testdata/dashboards/graph_last_1h.json
vendored
Normal file
483
public/app/plugins/app/testdata/dashboards/graph_last_1h.json
vendored
Normal file
@ -0,0 +1,483 @@
|
||||
{
|
||||
"revision": 4,
|
||||
"title": "TestData - Graph Panel Last 1h",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 1,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 4,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "no_data_points",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "No Data Points Warning",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 4,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "datapoints_outside_range",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Datapoints Outside Range Warning",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 4,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Random walk series",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 8,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": "2s",
|
||||
"timeShift": null,
|
||||
"title": "Millisecond res x-axis and tooltip",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"error": false,
|
||||
"span": 4,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"isNew": true,
|
||||
"id": 6,
|
||||
"mode": "markdown",
|
||||
"content": "Just verify that the tooltip time has millisecond resolution ",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
},
|
||||
{
|
||||
"title": "New row",
|
||||
"height": 336,
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"title": "2 yaxis and axis lables",
|
||||
"error": false,
|
||||
"span": 7.99561403508772,
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"isNew": true,
|
||||
"id": 5,
|
||||
"targets": [
|
||||
{
|
||||
"target": "",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0"
|
||||
},
|
||||
{
|
||||
"target": "",
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "2000,3000,4000,1000,3000,10000"
|
||||
}
|
||||
],
|
||||
"datasource": "Grafana TestData",
|
||||
"renderer": "flot",
|
||||
"yaxes": [
|
||||
{
|
||||
"label": "Perecent",
|
||||
"show": true,
|
||||
"logBase": 1,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"format": "percent"
|
||||
},
|
||||
{
|
||||
"label": "Pressure",
|
||||
"show": true,
|
||||
"logBase": 1,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"format": "short"
|
||||
}
|
||||
],
|
||||
"xaxis": {
|
||||
"show": true,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"values": []
|
||||
},
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": false,
|
||||
"percentage": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"msResolution": false
|
||||
},
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"aliasColors": {},
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "B-series",
|
||||
"yaxis": 2
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"error": false,
|
||||
"span": 4.00438596491228,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"isNew": true,
|
||||
"id": 7,
|
||||
"mode": "markdown",
|
||||
"content": "Verify that axis labels look ok",
|
||||
"links": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"refresh": false,
|
||||
"schemaVersion": 13,
|
||||
"version": 3,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
62
public/app/plugins/app/testdata/datasource/datasource.ts
vendored
Normal file
62
public/app/plugins/app/testdata/datasource/datasource.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
|
||||
class TestDataDatasource {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $q) {}
|
||||
|
||||
query(options) {
|
||||
var queries = _.filter(options.targets, item => {
|
||||
return item.hide !== true;
|
||||
}).map(item => {
|
||||
return {
|
||||
refId: item.refId,
|
||||
scenarioId: item.scenarioId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
stringInput: item.stringInput,
|
||||
jsonInput: angular.fromJson(item.jsonInput),
|
||||
};
|
||||
});
|
||||
|
||||
if (queries.length === 0) {
|
||||
return this.$q.when({data: []});
|
||||
}
|
||||
|
||||
return this.backendSrv.post('/api/tsdb/query', {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: queries,
|
||||
}).then(res => {
|
||||
var data = [];
|
||||
|
||||
if (res.results) {
|
||||
_.forEach(res.results, queryRes => {
|
||||
for (let series of queryRes.series) {
|
||||
data.push({
|
||||
target: series.name,
|
||||
datapoints: series.points
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {data: data};
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
20
public/app/plugins/app/testdata/datasource/plugin.json
vendored
Normal file
20
public/app/plugins/app/testdata/datasource/plugin.json
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Grafana TestDataDB",
|
||||
"id": "grafana-testdata-datasource",
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "http://grafana.org"
|
||||
},
|
||||
"logos": {
|
||||
"small": "",
|
||||
"large": ""
|
||||
}
|
||||
}
|
||||
}
|
35
public/app/plugins/app/testdata/datasource/query_ctrl.ts
vendored
Normal file
35
public/app/plugins/app/testdata/datasource/query_ctrl.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import {TestDataDatasource} from './datasource';
|
||||
import {QueryCtrl} from 'app/plugins/sdk';
|
||||
|
||||
export class TestDataQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
scenarioList: any;
|
||||
scenario: any;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor($scope, $injector, private backendSrv) {
|
||||
super($scope, $injector);
|
||||
|
||||
this.target.scenarioId = this.target.scenarioId || 'random_walk';
|
||||
this.scenarioList = [];
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.backendSrv.get('/api/tsdb/testdata/scenarios').then(res => {
|
||||
this.scenarioList = res;
|
||||
this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId});
|
||||
});
|
||||
}
|
||||
|
||||
scenarioChanged() {
|
||||
this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId});
|
||||
this.target.stringInput = this.scenario.stringInput;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
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">
|
||||
<select class="gf-form-input" ng-model="ctrl.target.scenarioId" ng-options="v.id as v.name for v in ctrl.scenarioList" ng-change="ctrl.scenarioChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form gf-form--grow" ng-if="ctrl.scenario.stringInput">
|
||||
<label class="gf-form-label query-keyword">String Input</label>
|
||||
<input type="text" class="gf-form-input" placeholder="{{ctrl.scenario.stringInput}}" ng-model="ctrl.target.stringInput" 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 max-width-7" placeholder="optional" ng-model="ctrl.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>
|
||||
|
32
public/app/plugins/app/testdata/plugin.json
vendored
Normal file
32
public/app/plugins/app/testdata/plugin.json
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "app",
|
||||
"name": "Grafana TestData",
|
||||
"id": "testdata",
|
||||
|
||||
"info": {
|
||||
"description": "Grafana test data app",
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "http://grafana.org"
|
||||
},
|
||||
"version": "1.0.13",
|
||||
"updated": "2016-09-26"
|
||||
},
|
||||
|
||||
"includes": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "TestData - Graph Last 1h",
|
||||
"path": "dashboards/graph_last_1h.json"
|
||||
},
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "TestData - Alerts",
|
||||
"path": "dashboards/alerts.json"
|
||||
}
|
||||
],
|
||||
|
||||
"dependencies": {
|
||||
"grafanaVersion": "4.x.x"
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ class GrafanaDatasource {
|
||||
return this.backendSrv.get('/api/metrics/test', {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
scenario: 'random_walk',
|
||||
interval: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints
|
||||
});
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
],
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"info": {
|
||||
@ -20,4 +21,4 @@
|
||||
"large": "img/graphite_logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
};
|
||||
|
||||
this.performTimeSeriesQuery = function(query, start, end) {
|
||||
if (start > end) {
|
||||
throw { message: 'Invalid time range' };
|
||||
}
|
||||
|
||||
var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step;
|
||||
return this._request('GET', url, query.requestId);
|
||||
};
|
||||
|
@ -8,6 +8,7 @@
|
||||
],
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"info": {
|
||||
|
@ -40,6 +40,32 @@
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">X-Axis</h5>
|
||||
<gf-form-switch class="gf-form" label="Show" label-class="width-5" checked="ctrl.panel.xaxis.show" on-change="ctrl.render()"></gf-form-switch>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.mode" ng-options="v as k for (k, v) in ctrl.xAxisModes" ng-change="ctrl.xAxisOptionChanged()"> </select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
|
||||
<label class="gf-form-label width-5">Name</label>
|
||||
<metric-segment-model property="ctrl.panel.xaxis.name" get-options="ctrl.getDataFieldNames(false)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<!-- Series mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
|
||||
<label class="gf-form-label width-5">Value</label>
|
||||
<metric-segment-model property="ctrl.panel.xaxis.values[0]" get-options="ctrl.getDataFieldNames(true)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<!-- Series mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'series'">
|
||||
<label class="gf-form-label width-5">Value</label>
|
||||
<metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
85
public/app/plugins/panel/graph/axes_editor.ts
Normal file
85
public/app/plugins/panel/graph/axes_editor.ts
Normal file
@ -0,0 +1,85 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export class AxesEditorCtrl {
|
||||
panel: any;
|
||||
panelCtrl: any;
|
||||
unitFormats: any;
|
||||
logScales: any;
|
||||
xAxisModes: any;
|
||||
xAxisStatOptions: any;
|
||||
xNameSegment: any;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, private $q) {
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
$scope.ctrl = this;
|
||||
|
||||
this.unitFormats = kbn.getUnitFormats();
|
||||
|
||||
this.logScales = {
|
||||
'linear': 1,
|
||||
'log (base 2)': 2,
|
||||
'log (base 10)': 10,
|
||||
'log (base 32)': 32,
|
||||
'log (base 1024)': 1024
|
||||
};
|
||||
|
||||
this.xAxisModes = {
|
||||
'Time': 'time',
|
||||
'Series': 'series',
|
||||
// 'Data field': 'field',
|
||||
};
|
||||
|
||||
this.xAxisStatOptions = [
|
||||
{text: 'Avg', value: 'avg'},
|
||||
{text: 'Min', value: 'min'},
|
||||
{text: 'Max', value: 'min'},
|
||||
{text: 'Total', value: 'total'},
|
||||
{text: 'Count', value: 'count'},
|
||||
];
|
||||
|
||||
if (this.panel.xaxis.mode === 'custom') {
|
||||
if (!this.panel.xaxis.name) {
|
||||
this.panel.xaxis.name = 'specify field';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUnitFormat(axis, subItem) {
|
||||
axis.format = subItem.value;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
xAxisOptionChanged() {
|
||||
this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode();
|
||||
this.panelCtrl.onDataReceived(this.panelCtrl.dataList);
|
||||
}
|
||||
|
||||
getDataFieldNames(onlyNumbers) {
|
||||
var props = this.panelCtrl.processor.getDataFieldNames(this.panelCtrl.dataList, onlyNumbers);
|
||||
var items = props.map(prop => {
|
||||
return {text: prop, value: prop};
|
||||
});
|
||||
|
||||
return this.$q.when(items);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @ngInject **/
|
||||
export function axesEditorComponent() {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
templateUrl: 'public/app/plugins/panel/graph/axes_editor.html',
|
||||
controller: AxesEditorCtrl,
|
||||
};
|
||||
}
|
192
public/app/plugins/panel/graph/data_processor.ts
Normal file
192
public/app/plugins/panel/graph/data_processor.ts
Normal file
@ -0,0 +1,192 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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';
|
||||
|
||||
export class DataProcessor {
|
||||
|
||||
constructor(private panel) {
|
||||
}
|
||||
|
||||
getSeriesList(options) {
|
||||
if (!options.dataList || options.dataList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// auto detect xaxis mode
|
||||
var firstItem;
|
||||
if (options.dataList && options.dataList.length > 0) {
|
||||
firstItem = options.dataList[0];
|
||||
let autoDetectMode = this.getAutoDetectXAxisMode(firstItem);
|
||||
if (this.panel.xaxis.mode !== autoDetectMode) {
|
||||
this.panel.xaxis.mode = autoDetectMode;
|
||||
this.setPanelDefaultsForNewXAxisMode();
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.panel.xaxis.mode) {
|
||||
case 'series':
|
||||
case 'time': {
|
||||
return options.dataList.map((item, index) => {
|
||||
return this.timeSeriesHandler(item, index, options);
|
||||
});
|
||||
}
|
||||
case 'field': {
|
||||
return this.customHandler(firstItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAutoDetectXAxisMode(firstItem) {
|
||||
switch (firstItem.type) {
|
||||
case 'docs': return 'field';
|
||||
case 'table': return 'field';
|
||||
default: {
|
||||
if (this.panel.xaxis.mode === 'series') {
|
||||
return 'series';
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPanelDefaultsForNewXAxisMode() {
|
||||
switch (this.panel.xaxis.mode) {
|
||||
case 'time': {
|
||||
this.panel.bars = false;
|
||||
this.panel.lines = true;
|
||||
this.panel.points = false;
|
||||
this.panel.legend.show = true;
|
||||
this.panel.tooltip.shared = true;
|
||||
this.panel.xaxis.values = [];
|
||||
break;
|
||||
}
|
||||
case 'series': {
|
||||
this.panel.bars = true;
|
||||
this.panel.lines = false;
|
||||
this.panel.points = false;
|
||||
this.panel.stack = false;
|
||||
this.panel.legend.show = false;
|
||||
this.panel.tooltip.shared = false;
|
||||
this.panel.xaxis.values = ['total'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = datapoints[datapoints.length - 1][1];
|
||||
var from = options.range.from;
|
||||
if (last - from < -10000) {
|
||||
series.isOutsideRange = true;
|
||||
}
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
customHandler(dataItem) {
|
||||
let nameField = this.panel.xaxis.name;
|
||||
if (!nameField) {
|
||||
throw {message: 'No field name specified to use for x-axis, check your axes settings'};
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
validateXAxisSeriesValue() {
|
||||
switch (this.panel.xaxis.mode) {
|
||||
case 'series': {
|
||||
if (this.panel.xaxis.values.length === 0) {
|
||||
this.panel.xaxis.values = ['total'];
|
||||
return;
|
||||
}
|
||||
|
||||
var validOptions = this.getXAxisValueOptions({});
|
||||
var found = _.find(validOptions, {value: this.panel.xaxis.values[0]});
|
||||
if (!found) {
|
||||
this.panel.xaxis.values = ['total'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDataFieldNames(dataList, onlyNumbers) {
|
||||
if (dataList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let fields = [];
|
||||
var firstItem = dataList[0];
|
||||
if (firstItem.type === 'docs'){
|
||||
if (firstItem.datapoints.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let fieldParts = [];
|
||||
|
||||
function getPropertiesRecursive(obj) {
|
||||
_.forEach(obj, (value, key) => {
|
||||
if (_.isObject(value)) {
|
||||
fieldParts.push(key);
|
||||
getPropertiesRecursive(value);
|
||||
} else {
|
||||
if (!onlyNumbers || _.isNumber(value)) {
|
||||
let field = fieldParts.concat(key).join('.');
|
||||
fields.push(field);
|
||||
}
|
||||
}
|
||||
});
|
||||
fieldParts.pop();
|
||||
}
|
||||
|
||||
getPropertiesRecursive(firstItem.datapoints[0]);
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
getXAxisValueOptions(options) {
|
||||
switch (this.panel.xaxis.mode) {
|
||||
case 'time': {
|
||||
return [];
|
||||
}
|
||||
case 'series': {
|
||||
return [
|
||||
{text: 'Avg', value: 'avg'},
|
||||
{text: 'Min', value: 'min'},
|
||||
{text: 'Max', value: 'min'},
|
||||
{text: 'Total', value: 'total'},
|
||||
{text: 'Count', value: 'count'},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluckDeep(obj: any, property: string) {
|
||||
let propertyParts = property.split('.');
|
||||
let value = obj;
|
||||
for (let i = 0; i < propertyParts.length; ++i) {
|
||||
if (value[propertyParts[i]]) {
|
||||
value = value[propertyParts[i]];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,533 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'moment',
|
||||
'lodash',
|
||||
'app/core/utils/kbn',
|
||||
'./graph_tooltip',
|
||||
'./threshold_manager',
|
||||
'jquery.flot',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
'jquery.flot.stack',
|
||||
'jquery.flot.stackpercent',
|
||||
'jquery.flot.fillbelow',
|
||||
'jquery.flot.crosshair',
|
||||
'./jquery.flot.events',
|
||||
],
|
||||
function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
var labelWidthCache = {};
|
||||
|
||||
module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '<div> </div>',
|
||||
link: function(scope, elem) {
|
||||
var ctrl = scope.ctrl;
|
||||
var dashboard = ctrl.dashboard;
|
||||
var panel = ctrl.panel;
|
||||
var data, annotations;
|
||||
var sortedSeries;
|
||||
var legendSideLastValue = null;
|
||||
var rootScope = scope.$root;
|
||||
var panelWidth = 0;
|
||||
var thresholdManager = new thresholdManExports.ThresholdManager(ctrl);
|
||||
|
||||
rootScope.onAppEvent('setCrosshair', function(event, info) {
|
||||
// do not need to to this if event is from this panel
|
||||
if (info.scope === scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(dashboard.sharedCrosshair) {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
|
||||
}
|
||||
}
|
||||
}, scope);
|
||||
|
||||
rootScope.onAppEvent('clearCrosshair', function() {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.clearCrosshair();
|
||||
}
|
||||
}, scope);
|
||||
|
||||
// Receive render events
|
||||
ctrl.events.on('render', function(renderData) {
|
||||
data = renderData || data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
annotations = data.annotations || annotations;
|
||||
render_panel();
|
||||
});
|
||||
|
||||
function getLegendHeight(panelHeight) {
|
||||
if (!panel.legend.show || panel.legend.rightSide) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (panel.legend.alignAsTable) {
|
||||
var legendSeries = _.filter(data, function(series) {
|
||||
return series.hideFromLegend(panel.legend) === false;
|
||||
});
|
||||
var total = 23 + (21 * legendSeries.length);
|
||||
return Math.min(total, Math.floor(panelHeight/2));
|
||||
} else {
|
||||
return 26;
|
||||
}
|
||||
}
|
||||
|
||||
function setElementHeight() {
|
||||
try {
|
||||
var height = ctrl.height - getLegendHeight(ctrl.height);
|
||||
elem.css('height', height + 'px');
|
||||
|
||||
return true;
|
||||
} catch(e) { // IE throws errors sometimes
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAbortRender() {
|
||||
if (!data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!setElementHeight()) { return true; }
|
||||
|
||||
if (panelWidth === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelWidth(text, elem) {
|
||||
var labelWidth = labelWidthCache[text];
|
||||
|
||||
if (!labelWidth) {
|
||||
labelWidth = labelWidthCache[text] = elem.width();
|
||||
}
|
||||
|
||||
return labelWidth;
|
||||
}
|
||||
|
||||
function drawHook(plot) {
|
||||
// Update legend values
|
||||
var yaxis = plot.getYAxes();
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
var axis = yaxis[series.yaxis - 1];
|
||||
var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format];
|
||||
|
||||
// decimal override
|
||||
if (_.isNumber(panel.decimals)) {
|
||||
series.updateLegendValues(formater, panel.decimals, null);
|
||||
} else {
|
||||
// auto decimals
|
||||
// legend and tooltip gets one more decimal precision
|
||||
// than graph legend ticks
|
||||
var tickDecimals = (axis.tickDecimals || -1) + 1;
|
||||
series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
|
||||
}
|
||||
|
||||
if(!rootScope.$$phase) { scope.$digest(); }
|
||||
}
|
||||
|
||||
// add left axis labels
|
||||
if (panel.yaxes[0].label) {
|
||||
var yaxisLabel = $("<div class='axisLabel left-yaxis-label'></div>")
|
||||
.text(panel.yaxes[0].label)
|
||||
.appendTo(elem);
|
||||
|
||||
yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
// add right axis labels
|
||||
if (panel.yaxes[1].label) {
|
||||
var rightLabel = $("<div class='axisLabel right-yaxis-label'></div>")
|
||||
.text(panel.yaxes[1].label)
|
||||
.appendTo(elem);
|
||||
|
||||
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
thresholdManager.draw(plot);
|
||||
}
|
||||
|
||||
function processOffsetHook(plot, gridMargin) {
|
||||
var left = panel.yaxes[0];
|
||||
var right = panel.yaxes[1];
|
||||
if (left.show && left.label) { gridMargin.left = 20; }
|
||||
if (right.show && right.label) { gridMargin.right = 20; }
|
||||
}
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
panelWidth = elem.width();
|
||||
|
||||
if (shouldAbortRender()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// give space to alert editing
|
||||
thresholdManager.prepare(elem, data);
|
||||
|
||||
var stack = panel.stack ? true : null;
|
||||
|
||||
// Populate element
|
||||
var options = {
|
||||
hooks: {
|
||||
draw: [drawHook],
|
||||
processOffset: [processOffsetHook],
|
||||
},
|
||||
legend: { show: false },
|
||||
series: {
|
||||
stackpercent: panel.stack ? panel.percentage : false,
|
||||
stack: panel.percentage ? null : stack,
|
||||
lines: {
|
||||
show: panel.lines,
|
||||
zero: false,
|
||||
fill: translateFillOption(panel.fill),
|
||||
lineWidth: panel.linewidth,
|
||||
steps: panel.steppedLine
|
||||
},
|
||||
bars: {
|
||||
show: panel.bars,
|
||||
fill: 1,
|
||||
barWidth: 1,
|
||||
zero: false,
|
||||
lineWidth: 0
|
||||
},
|
||||
points: {
|
||||
show: panel.points,
|
||||
fill: 1,
|
||||
fillColor: false,
|
||||
radius: panel.points ? panel.pointradius : 2
|
||||
},
|
||||
shadowSize: 0
|
||||
},
|
||||
yaxes: [],
|
||||
xaxis: {},
|
||||
grid: {
|
||||
minBorderMargin: 0,
|
||||
markings: [],
|
||||
backgroundColor: null,
|
||||
borderWidth: 0,
|
||||
hoverable: true,
|
||||
color: '#c8c8c8',
|
||||
margin: { left: 0, right: 0 },
|
||||
},
|
||||
selection: {
|
||||
mode: "x",
|
||||
color: '#666'
|
||||
},
|
||||
crosshair: {
|
||||
mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
|
||||
|
||||
// if hidden remove points and disable stack
|
||||
if (ctrl.hiddenSeries[series.alias]) {
|
||||
series.data = [];
|
||||
series.stack = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length && data[0].stats.timeStep) {
|
||||
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
|
||||
}
|
||||
|
||||
addTimeAxis(options);
|
||||
thresholdManager.addPlotOptions(options, panel);
|
||||
addAnnotations(options);
|
||||
configureAxisOptions(data, options);
|
||||
|
||||
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
|
||||
|
||||
function callPlot(incrementRenderCounter) {
|
||||
try {
|
||||
$.plot(elem, sortedSeries, options);
|
||||
if (ctrl.renderError) {
|
||||
delete ctrl.error;
|
||||
delete ctrl.inspector;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('flotcharts error', e);
|
||||
ctrl.error = e.message || "Render Error";
|
||||
ctrl.renderError = true;
|
||||
ctrl.inspector = {error: e};
|
||||
}
|
||||
|
||||
if (incrementRenderCounter) {
|
||||
ctrl.renderingCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelayDraw(panel)) {
|
||||
// temp fix for legends on the side, need to render twice to get dimensions right
|
||||
callPlot(false);
|
||||
setTimeout(function() { callPlot(true); }, 50);
|
||||
legendSideLastValue = panel.legend.rightSide;
|
||||
}
|
||||
else {
|
||||
callPlot(true);
|
||||
}
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
function shouldDelayDraw(panel) {
|
||||
if (panel.legend.rightSide) {
|
||||
return true;
|
||||
}
|
||||
if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function addTimeAxis(options) {
|
||||
var ticks = panelWidth / 100;
|
||||
var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
|
||||
var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.getTimezone(),
|
||||
show: panel.xaxis.show,
|
||||
mode: "time",
|
||||
min: min,
|
||||
max: max,
|
||||
label: "Datetime",
|
||||
ticks: ticks,
|
||||
timeformat: time_format(ticks, min, max),
|
||||
};
|
||||
}
|
||||
|
||||
function addAnnotations(options) {
|
||||
if(!annotations || annotations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var types = {};
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var item = annotations[i];
|
||||
|
||||
if (!types[item.source.name]) {
|
||||
types[item.source.name] = {
|
||||
color: item.source.iconColor,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
options.events = {
|
||||
levels: _.keys(types).length + 1,
|
||||
data: annotations,
|
||||
types: types,
|
||||
};
|
||||
}
|
||||
|
||||
//Override min/max to provide more flexible autoscaling
|
||||
function autoscaleSpanOverride(yaxis, data, options) {
|
||||
var expr;
|
||||
if (yaxis.min != null && data != null) {
|
||||
expr = parseThresholdExpr(yaxis.min);
|
||||
options.min = autoscaleYAxisMin(expr, data.stats);
|
||||
}
|
||||
if (yaxis.max != null && data != null) {
|
||||
expr = parseThresholdExpr(yaxis.max);
|
||||
options.max = autoscaleYAxisMax(expr, data.stats);
|
||||
}
|
||||
}
|
||||
|
||||
function parseThresholdExpr(expr) {
|
||||
var match, operator, value, precision;
|
||||
expr = String(expr);
|
||||
match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/);
|
||||
if (match) {
|
||||
operator = match[1];
|
||||
value = parseFloat(match[2]);
|
||||
//Precision based on input
|
||||
precision = match[3] ? match[3].length - 1 : 0;
|
||||
return {
|
||||
operator: operator,
|
||||
value: value,
|
||||
precision: precision
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function autoscaleYAxisMax(expr, dataStats) {
|
||||
var operator = expr.operator,
|
||||
value = expr.value,
|
||||
precision = expr.precision;
|
||||
if (operator === ">") {
|
||||
return dataStats.max < value ? value : null;
|
||||
} else if (operator === "<") {
|
||||
return dataStats.max > value ? value : null;
|
||||
} else if (operator === "~") {
|
||||
return kbn.roundValue(dataStats.avg + value, precision);
|
||||
} else if (operator === "=") {
|
||||
return kbn.roundValue(dataStats.current + value, precision);
|
||||
} else if (!operator && !isNaN(value)) {
|
||||
return kbn.roundValue(value, precision);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function autoscaleYAxisMin(expr, dataStats) {
|
||||
var operator = expr.operator,
|
||||
value = expr.value,
|
||||
precision = expr.precision;
|
||||
if (operator === ">") {
|
||||
return dataStats.min < value ? value : null;
|
||||
} else if (operator === "<") {
|
||||
return dataStats.min > value ? value : null;
|
||||
} else if (operator === "~") {
|
||||
return kbn.roundValue(dataStats.avg - value, precision);
|
||||
} else if (operator === "=") {
|
||||
return kbn.roundValue(dataStats.current - value, precision);
|
||||
} else if (!operator && !isNaN(value)) {
|
||||
return kbn.roundValue(value, precision);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function configureAxisOptions(data, options) {
|
||||
var defaults = {
|
||||
position: 'left',
|
||||
show: panel.yaxes[0].show,
|
||||
min: panel.yaxes[0].min,
|
||||
index: 1,
|
||||
logBase: panel.yaxes[0].logBase || 1,
|
||||
max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max,
|
||||
};
|
||||
|
||||
autoscaleSpanOverride(panel.yaxes[0], data[0], defaults);
|
||||
options.yaxes.push(defaults);
|
||||
|
||||
if (_.find(data, {yaxis: 2})) {
|
||||
var secondY = _.clone(defaults);
|
||||
secondY.index = 2,
|
||||
secondY.show = panel.yaxes[1].show;
|
||||
secondY.logBase = panel.yaxes[1].logBase || 1,
|
||||
secondY.position = 'right';
|
||||
secondY.min = panel.yaxes[1].min;
|
||||
secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max;
|
||||
autoscaleSpanOverride(panel.yaxes[1], data[1], secondY);
|
||||
options.yaxes.push(secondY);
|
||||
|
||||
applyLogScale(options.yaxes[1], data);
|
||||
configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format);
|
||||
}
|
||||
|
||||
applyLogScale(options.yaxes[0], data);
|
||||
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
|
||||
}
|
||||
|
||||
function applyLogScale(axis, data) {
|
||||
if (axis.logBase === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var series, i;
|
||||
var max = axis.max;
|
||||
|
||||
if (max === null) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
series = data[i];
|
||||
if (series.yaxis === axis.index) {
|
||||
if (max < series.stats.max) {
|
||||
max = series.stats.max;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (max === void 0) {
|
||||
max = Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
axis.min = axis.min !== null ? axis.min : 0;
|
||||
axis.ticks = [0, 1];
|
||||
var nextTick = 1;
|
||||
|
||||
while (true) {
|
||||
nextTick = nextTick * axis.logBase;
|
||||
axis.ticks.push(nextTick);
|
||||
if (nextTick > max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (axis.logBase === 10) {
|
||||
axis.transform = function(v) { return Math.log(v+0.1); };
|
||||
axis.inverseTransform = function (v) { return Math.pow(10,v); };
|
||||
} else {
|
||||
axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
|
||||
axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); };
|
||||
}
|
||||
}
|
||||
|
||||
function configureAxisMode(axis, format) {
|
||||
axis.tickFormatter = function(val, axis) {
|
||||
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
|
||||
};
|
||||
}
|
||||
|
||||
function time_format(ticks, min, max) {
|
||||
if (min && max && ticks) {
|
||||
var range = max - min;
|
||||
var secPerTick = (range/ticks) / 1000;
|
||||
var oneDay = 86400000;
|
||||
var oneYear = 31536000000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return "%H:%M:%S";
|
||||
}
|
||||
if (secPerTick <= 7200 || range <= oneDay) {
|
||||
return "%H:%M";
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return "%m/%d %H:%M";
|
||||
}
|
||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||
return "%m/%d";
|
||||
}
|
||||
return "%Y-%m";
|
||||
}
|
||||
|
||||
return "%H:%M";
|
||||
}
|
||||
|
||||
new GraphTooltip(elem, dashboard, scope, function() {
|
||||
return sortedSeries;
|
||||
});
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
scope.$apply(function() {
|
||||
timeSrv.setTime({
|
||||
from : moment.utc(ranges.xaxis.from),
|
||||
to : moment.utc(ranges.xaxis.to),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
521
public/app/plugins/panel/graph/graph.ts
Executable file
521
public/app/plugins/panel/graph/graph.ts
Executable file
@ -0,0 +1,521 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import 'jquery.flot';
|
||||
import 'jquery.flot.selection';
|
||||
import 'jquery.flot.time';
|
||||
import 'jquery.flot.stack';
|
||||
import 'jquery.flot.stackpercent';
|
||||
import 'jquery.flot.fillbelow';
|
||||
import 'jquery.flot.crosshair';
|
||||
import './jquery.flot.events';
|
||||
|
||||
import angular from 'angular';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import GraphTooltip from './graph_tooltip';
|
||||
import {ThresholdManager} from './threshold_manager';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
var labelWidthCache = {};
|
||||
|
||||
module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '',
|
||||
link: function(scope, elem) {
|
||||
var ctrl = scope.ctrl;
|
||||
var dashboard = ctrl.dashboard;
|
||||
var panel = ctrl.panel;
|
||||
var data, annotations;
|
||||
var sortedSeries;
|
||||
var legendSideLastValue = null;
|
||||
var rootScope = scope.$root;
|
||||
var panelWidth = 0;
|
||||
var thresholdManager = new ThresholdManager(ctrl);
|
||||
|
||||
rootScope.onAppEvent('setCrosshair', function(event, info) {
|
||||
// do not need to to this if event is from this panel
|
||||
if (info.scope === scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dashboard.sharedCrosshair) {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
|
||||
}
|
||||
}
|
||||
}, scope);
|
||||
|
||||
rootScope.onAppEvent('clearCrosshair', function() {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.clearCrosshair();
|
||||
}
|
||||
}, scope);
|
||||
|
||||
// Receive render events
|
||||
ctrl.events.on('render', function(renderData) {
|
||||
data = renderData || data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
annotations = data.annotations || annotations;
|
||||
render_panel();
|
||||
});
|
||||
|
||||
function getLegendHeight(panelHeight) {
|
||||
if (!panel.legend.show || panel.legend.rightSide) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (panel.legend.alignAsTable) {
|
||||
var legendSeries = _.filter(data, function(series) {
|
||||
return series.hideFromLegend(panel.legend) === false;
|
||||
});
|
||||
var total = 23 + (21 * legendSeries.length);
|
||||
return Math.min(total, Math.floor(panelHeight/2));
|
||||
} else {
|
||||
return 26;
|
||||
}
|
||||
}
|
||||
|
||||
function setElementHeight() {
|
||||
try {
|
||||
var height = ctrl.height - getLegendHeight(ctrl.height);
|
||||
elem.css('height', height + 'px');
|
||||
|
||||
return true;
|
||||
} catch (e) { // IE throws errors sometimes
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAbortRender() {
|
||||
if (!data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!setElementHeight()) { return true; }
|
||||
|
||||
if (panelWidth === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelWidth(text, elem) {
|
||||
var labelWidth = labelWidthCache[text];
|
||||
|
||||
if (!labelWidth) {
|
||||
labelWidth = labelWidthCache[text] = elem.width();
|
||||
}
|
||||
|
||||
return labelWidth;
|
||||
}
|
||||
|
||||
function drawHook(plot) {
|
||||
// Update legend values
|
||||
var yaxis = plot.getYAxes();
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
var axis = yaxis[series.yaxis - 1];
|
||||
var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format];
|
||||
|
||||
// decimal override
|
||||
if (_.isNumber(panel.decimals)) {
|
||||
series.updateLegendValues(formater, panel.decimals, null);
|
||||
} else {
|
||||
// auto decimals
|
||||
// legend and tooltip gets one more decimal precision
|
||||
// than graph legend ticks
|
||||
var tickDecimals = (axis.tickDecimals || -1) + 1;
|
||||
series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
|
||||
}
|
||||
|
||||
if (!rootScope.$$phase) { scope.$digest(); }
|
||||
}
|
||||
|
||||
// add left axis labels
|
||||
if (panel.yaxes[0].label) {
|
||||
var yaxisLabel = $("<div class='axisLabel left-yaxis-label'></div>")
|
||||
.text(panel.yaxes[0].label)
|
||||
.appendTo(elem);
|
||||
|
||||
yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
// add right axis labels
|
||||
if (panel.yaxes[1].label) {
|
||||
var rightLabel = $("<div class='axisLabel right-yaxis-label'></div>")
|
||||
.text(panel.yaxes[1].label)
|
||||
.appendTo(elem);
|
||||
|
||||
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
thresholdManager.draw(plot);
|
||||
}
|
||||
|
||||
function processOffsetHook(plot, gridMargin) {
|
||||
var left = panel.yaxes[0];
|
||||
var right = panel.yaxes[1];
|
||||
if (left.show && left.label) { gridMargin.left = 20; }
|
||||
if (right.show && right.label) { gridMargin.right = 20; }
|
||||
|
||||
// apply y-axis min/max options
|
||||
var yaxis = plot.getYAxes();
|
||||
for (var i = 0; i < yaxis.length; i++) {
|
||||
var axis = yaxis[i];
|
||||
var panelOptions = panel.yaxes[i];
|
||||
axis.options.max = panelOptions.max;
|
||||
axis.options.min = panelOptions.min;
|
||||
}
|
||||
}
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
panelWidth = elem.width();
|
||||
|
||||
if (shouldAbortRender()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// give space to alert editing
|
||||
thresholdManager.prepare(elem, data);
|
||||
|
||||
var stack = panel.stack ? true : null;
|
||||
|
||||
// Populate element
|
||||
var options: any = {
|
||||
hooks: {
|
||||
draw: [drawHook],
|
||||
processOffset: [processOffsetHook],
|
||||
},
|
||||
legend: { show: false },
|
||||
series: {
|
||||
stackpercent: panel.stack ? panel.percentage : false,
|
||||
stack: panel.percentage ? null : stack,
|
||||
lines: {
|
||||
show: panel.lines,
|
||||
zero: false,
|
||||
fill: translateFillOption(panel.fill),
|
||||
lineWidth: panel.linewidth,
|
||||
steps: panel.steppedLine
|
||||
},
|
||||
bars: {
|
||||
show: panel.bars,
|
||||
fill: 1,
|
||||
barWidth: 1,
|
||||
zero: false,
|
||||
lineWidth: 0
|
||||
},
|
||||
points: {
|
||||
show: panel.points,
|
||||
fill: 1,
|
||||
fillColor: false,
|
||||
radius: panel.points ? panel.pointradius : 2
|
||||
},
|
||||
shadowSize: 0
|
||||
},
|
||||
yaxes: [],
|
||||
xaxis: {},
|
||||
grid: {
|
||||
minBorderMargin: 0,
|
||||
markings: [],
|
||||
backgroundColor: null,
|
||||
borderWidth: 0,
|
||||
hoverable: true,
|
||||
color: '#c8c8c8',
|
||||
margin: { left: 0, right: 0 },
|
||||
},
|
||||
selection: {
|
||||
mode: "x",
|
||||
color: '#666'
|
||||
},
|
||||
crosshair: {
|
||||
mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
|
||||
|
||||
// if hidden remove points and disable stack
|
||||
if (ctrl.hiddenSeries[series.alias]) {
|
||||
series.data = [];
|
||||
series.stack = false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (panel.xaxis.mode) {
|
||||
case 'series': {
|
||||
options.series.bars.barWidth = 0.7;
|
||||
options.series.bars.align = 'center';
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
|
||||
}
|
||||
|
||||
addXSeriesAxis(options);
|
||||
break;
|
||||
}
|
||||
case 'table': {
|
||||
options.series.bars.barWidth = 0.7;
|
||||
options.series.bars.align = 'center';
|
||||
addXTableAxis(options);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (data.length && data[0].stats.timeStep) {
|
||||
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
|
||||
}
|
||||
addTimeAxis(options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
thresholdManager.addPlotOptions(options, panel);
|
||||
addAnnotations(options);
|
||||
configureAxisOptions(data, options);
|
||||
|
||||
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
|
||||
|
||||
function callPlot(incrementRenderCounter) {
|
||||
try {
|
||||
$.plot(elem, sortedSeries, options);
|
||||
if (ctrl.renderError) {
|
||||
delete ctrl.error;
|
||||
delete ctrl.inspector;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('flotcharts error', e);
|
||||
ctrl.error = e.message || "Render Error";
|
||||
ctrl.renderError = true;
|
||||
ctrl.inspector = {error: e};
|
||||
}
|
||||
|
||||
if (incrementRenderCounter) {
|
||||
ctrl.renderingCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelayDraw(panel)) {
|
||||
// temp fix for legends on the side, need to render twice to get dimensions right
|
||||
callPlot(false);
|
||||
setTimeout(function() { callPlot(true); }, 50);
|
||||
legendSideLastValue = panel.legend.rightSide;
|
||||
} else {
|
||||
callPlot(true);
|
||||
}
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
function shouldDelayDraw(panel) {
|
||||
if (panel.legend.rightSide) {
|
||||
return true;
|
||||
}
|
||||
if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function addTimeAxis(options) {
|
||||
var ticks = panelWidth / 100;
|
||||
var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
|
||||
var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.getTimezone(),
|
||||
show: panel.xaxis.show,
|
||||
mode: "time",
|
||||
min: min,
|
||||
max: max,
|
||||
label: "Datetime",
|
||||
ticks: ticks,
|
||||
timeformat: time_format(ticks, min, max),
|
||||
};
|
||||
}
|
||||
|
||||
function addXSeriesAxis(options) {
|
||||
var ticks = _.map(data, function(series, index) {
|
||||
return [index + 1, series.alias];
|
||||
});
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.getTimezone(),
|
||||
show: panel.xaxis.show,
|
||||
mode: null,
|
||||
min: 0,
|
||||
max: ticks.length + 1,
|
||||
label: "Datetime",
|
||||
ticks: ticks
|
||||
};
|
||||
}
|
||||
|
||||
function addXTableAxis(options) {
|
||||
var ticks = _.map(data, function(series, seriesIndex) {
|
||||
return _.map(series.datapoints, function(point, pointIndex) {
|
||||
var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
|
||||
return [tickIndex + 1, point[1]];
|
||||
});
|
||||
});
|
||||
ticks = _.flatten(ticks, true);
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.getTimezone(),
|
||||
show: panel.xaxis.show,
|
||||
mode: null,
|
||||
min: 0,
|
||||
max: ticks.length + 1,
|
||||
label: "Datetime",
|
||||
ticks: ticks
|
||||
};
|
||||
}
|
||||
|
||||
function addAnnotations(options) {
|
||||
if (!annotations || annotations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var types = {};
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var item = annotations[i];
|
||||
|
||||
if (!types[item.source.name]) {
|
||||
types[item.source.name] = {
|
||||
color: item.source.iconColor,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
options.events = {
|
||||
levels: _.keys(types).length + 1,
|
||||
data: annotations,
|
||||
types: types,
|
||||
};
|
||||
}
|
||||
|
||||
function configureAxisOptions(data, options) {
|
||||
var defaults = {
|
||||
position: 'left',
|
||||
show: panel.yaxes[0].show,
|
||||
index: 1,
|
||||
logBase: panel.yaxes[0].logBase || 1,
|
||||
max: 100, // correct later
|
||||
};
|
||||
|
||||
options.yaxes.push(defaults);
|
||||
|
||||
if (_.find(data, {yaxis: 2})) {
|
||||
var secondY = _.clone(defaults);
|
||||
secondY.index = 2;
|
||||
secondY.show = panel.yaxes[1].show;
|
||||
secondY.logBase = panel.yaxes[1].logBase || 1;
|
||||
secondY.position = 'right';
|
||||
options.yaxes.push(secondY);
|
||||
configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format);
|
||||
}
|
||||
|
||||
applyLogScale(options.yaxes[0], data);
|
||||
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
|
||||
}
|
||||
|
||||
function applyLogScale(axis, data) {
|
||||
if (axis.logBase === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var series, i;
|
||||
var max = axis.max;
|
||||
|
||||
if (max === null) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
series = data[i];
|
||||
if (series.yaxis === axis.index) {
|
||||
if (max < series.stats.max) {
|
||||
max = series.stats.max;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (max === void 0) {
|
||||
max = Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
axis.min = axis.min !== null ? axis.min : 0;
|
||||
axis.ticks = [0, 1];
|
||||
var nextTick = 1;
|
||||
|
||||
while (true) {
|
||||
nextTick = nextTick * axis.logBase;
|
||||
axis.ticks.push(nextTick);
|
||||
if (nextTick > max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (axis.logBase === 10) {
|
||||
axis.transform = function(v) { return Math.log(v+0.1); };
|
||||
axis.inverseTransform = function (v) { return Math.pow(10,v); };
|
||||
} else {
|
||||
axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
|
||||
axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); };
|
||||
}
|
||||
}
|
||||
|
||||
function configureAxisMode(axis, format) {
|
||||
axis.tickFormatter = function(val, axis) {
|
||||
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
|
||||
};
|
||||
}
|
||||
|
||||
function time_format(ticks, min, max) {
|
||||
if (min && max && ticks) {
|
||||
var range = max - min;
|
||||
var secPerTick = (range/ticks) / 1000;
|
||||
var oneDay = 86400000;
|
||||
var oneYear = 31536000000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return "%H:%M:%S";
|
||||
}
|
||||
if (secPerTick <= 7200 || range <= oneDay) {
|
||||
return "%H:%M";
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return "%m/%d %H:%M";
|
||||
}
|
||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||
return "%m/%d";
|
||||
}
|
||||
return "%Y-%m";
|
||||
}
|
||||
|
||||
return "%H:%M";
|
||||
}
|
||||
|
||||
new GraphTooltip(elem, dashboard, scope, function() {
|
||||
return sortedSeries;
|
||||
});
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
scope.$apply(function() {
|
||||
timeSrv.setTime({
|
||||
from : moment.utc(ranges.xaxis.from),
|
||||
to : moment.utc(ranges.xaxis.to),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
@ -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();
|
||||
|
||||
|
@ -8,26 +8,26 @@ import './thresholds_form';
|
||||
import template from './template';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import _ from 'lodash';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import config from 'app/core/config';
|
||||
import * as fileExport from 'app/core/utils/file_export';
|
||||
import {MetricsPanelCtrl, alertTab} from 'app/plugins/sdk';
|
||||
import {DataProcessor} from './data_processor';
|
||||
import {axesEditorComponent} from './axes_editor';
|
||||
|
||||
class GraphCtrl extends MetricsPanelCtrl {
|
||||
static template = template;
|
||||
|
||||
hiddenSeries: any = {};
|
||||
seriesList: any = [];
|
||||
logScales: any;
|
||||
unitFormats: any;
|
||||
dataList: any = [];
|
||||
annotationsPromise: any;
|
||||
datapointsCount: number;
|
||||
datapointsOutside: boolean;
|
||||
datapointsWarning: boolean;
|
||||
colors: any = [];
|
||||
subTabIndex: number;
|
||||
processor: DataProcessor;
|
||||
|
||||
panelDefaults = {
|
||||
// datasource name, null = default datasource
|
||||
@ -53,7 +53,10 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
],
|
||||
xaxis: {
|
||||
show: true
|
||||
show: true,
|
||||
mode: 'time',
|
||||
name: null,
|
||||
values: [],
|
||||
},
|
||||
// show/hide lines
|
||||
lines : true,
|
||||
@ -111,8 +114,9 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
_.defaults(this.panel, this.panelDefaults);
|
||||
_.defaults(this.panel.tooltip, this.panelDefaults.tooltip);
|
||||
_.defaults(this.panel.legend, this.panelDefaults.legend);
|
||||
_.defaults(this.panel.xaxis, this.panelDefaults.xaxis);
|
||||
|
||||
this.colors = $scope.$root.colors;
|
||||
this.processor = new DataProcessor(this.panel);
|
||||
|
||||
this.events.on('render', this.onRender.bind(this));
|
||||
this.events.on('data-received', this.onDataReceived.bind(this));
|
||||
@ -123,23 +127,13 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
this.addEditorTab('Axes', 'public/app/plugins/panel/graph/tab_axes.html', 2);
|
||||
this.addEditorTab('Axes', axesEditorComponent, 2);
|
||||
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
|
||||
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
|
||||
|
||||
if (config.alertingEnabled) {
|
||||
this.addEditorTab('Alert', alertTab, 5);
|
||||
}
|
||||
|
||||
this.logScales = {
|
||||
'linear': 1,
|
||||
'log (base 2)': 2,
|
||||
'log (base 10)': 10,
|
||||
'log (base 32)': 32,
|
||||
'log (base 1024)': 1024
|
||||
};
|
||||
|
||||
this.unitFormats = kbn.getUnitFormats();
|
||||
this.subTabIndex = 0;
|
||||
}
|
||||
|
||||
@ -149,11 +143,6 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
actions.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'});
|
||||
}
|
||||
|
||||
setUnitFormat(axis, subItem) {
|
||||
axis.format = subItem.value;
|
||||
this.render();
|
||||
}
|
||||
|
||||
issueQueries(datasource) {
|
||||
this.annotationsPromise = this.annotationsSrv.getAnnotations({
|
||||
dashboard: this.dashboard,
|
||||
@ -182,11 +171,20 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
onDataReceived(dataList) {
|
||||
this.datapointsWarning = false;
|
||||
this.datapointsCount = 0;
|
||||
|
||||
this.dataList = dataList;
|
||||
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
|
||||
|
||||
this.datapointsCount = this.seriesList.reduce((prev, series) => {
|
||||
return prev + series.datapoints.length;
|
||||
}, 0);
|
||||
|
||||
this.datapointsOutside = false;
|
||||
this.seriesList = dataList.map(this.seriesHandler.bind(this));
|
||||
this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside;
|
||||
for (let series of this.seriesList) {
|
||||
if (series.isOutsideRange) {
|
||||
this.datapointsOutside = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.annotationsPromise.then(annotations => {
|
||||
this.loading = false;
|
||||
@ -198,34 +196,6 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
seriesHandler(seriesData, index) {
|
||||
var datapoints = seriesData.datapoints;
|
||||
var alias = seriesData.target;
|
||||
var colorIndex = index % this.colors.length;
|
||||
var color = this.panel.aliasColors[alias] || this.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;
|
||||
}
|
||||
|
||||
onRender() {
|
||||
if (!this.seriesList) { return; }
|
||||
|
||||
@ -309,13 +279,11 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
// Called from panel menu
|
||||
toggleLegend() {
|
||||
this.panel.legend.show = !this.panel.legend.show;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
||||
legendValuesOptionChanged() {
|
||||
var legend = this.panel.legend;
|
||||
legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total;
|
||||
|
64
public/app/plugins/panel/graph/specs/data_processor_specs.ts
Normal file
64
public/app/plugins/panel/graph/specs/data_processor_specs.ts
Normal file
@ -0,0 +1,64 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
|
||||
|
||||
import {DataProcessor} from '../data_processor';
|
||||
|
||||
describe('Graph DataProcessor', function() {
|
||||
var panel: any = {
|
||||
xaxis: {}
|
||||
};
|
||||
var processor = new DataProcessor(panel);
|
||||
var seriesList;
|
||||
|
||||
describe('Given default xaxis options and query that returns docs', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
panel.xaxis.mode = 'time';
|
||||
panel.xaxis.name = 'hostname';
|
||||
panel.xaxis.values = [];
|
||||
|
||||
seriesList = processor.getSeriesList({
|
||||
dataList: [
|
||||
{
|
||||
type: 'docs',
|
||||
datapoints: [{hostname: "server1", avg: 10}]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('Should automatically set xaxis mode to field', () => {
|
||||
expect(panel.xaxis.mode).to.be('field');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getDataFieldNames(', () => {
|
||||
var dataList = [{
|
||||
type: 'docs', datapoints: [
|
||||
{
|
||||
hostname: "server1",
|
||||
valueField: 11,
|
||||
nested: {
|
||||
prop1: 'server2', value2: 23}
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
||||
it('Should return all field names', () => {
|
||||
var fields = processor.getDataFieldNames(dataList, false);
|
||||
expect(fields).to.contain('hostname');
|
||||
expect(fields).to.contain('valueField');
|
||||
expect(fields).to.contain('nested.prop1');
|
||||
expect(fields).to.contain('nested.value2');
|
||||
});
|
||||
|
||||
it('Should return all number fields', () => {
|
||||
var fields = processor.getDataFieldNames(dataList, true);
|
||||
expect(fields).to.contain('valueField');
|
||||
expect(fields).to.contain('nested.value2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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('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('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('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('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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -219,145 +219,145 @@ describe('grafanaGraph', function() {
|
||||
|
||||
}, 10);
|
||||
|
||||
graphScenario('when using flexible Y-Min and Y-Max settings', function(ctx) {
|
||||
describe('and Y-Min is <100 and Y-Max is >200 and values within range', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '<100';
|
||||
ctrl.panel.yaxes[0].max = '>200';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[120,10],[160,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to 100 and max to 200', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(100);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(200);
|
||||
});
|
||||
});
|
||||
describe('and Y-Min is <100 and Y-Max is >200 and values outside range', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '<100';
|
||||
ctrl.panel.yaxes[0].max = '>200';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[99,10],[201,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to auto and max to auto', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(null);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(null);
|
||||
});
|
||||
});
|
||||
describe('and Y-Min is =10.5 and Y-Max is =10.5', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '=10.5';
|
||||
ctrl.panel.yaxes[0].max = '=10.5';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[100,10],[120,20], [110,30]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to last value + 10.5 and max to last value + 10.5', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(99.5);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(120.5);
|
||||
});
|
||||
});
|
||||
describe('and Y-Min is ~10.5 and Y-Max is ~10.5', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '~10.5';
|
||||
ctrl.panel.yaxes[0].max = '~10.5';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[102,10],[104,20], [110,30]], //Also checks precision
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to average value + 10.5 and max to average value + 10.5', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(94.8);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(115.8);
|
||||
});
|
||||
});
|
||||
});
|
||||
graphScenario('when using regular Y-Min and Y-Max settings', function(ctx) {
|
||||
describe('and Y-Min is 100 and Y-Max is 200', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '100';
|
||||
ctrl.panel.yaxes[0].max = '200';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[120,10],[160,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to 100 and max to 200', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(100);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(200);
|
||||
});
|
||||
});
|
||||
describe('and Y-Min is 0 and Y-Max is 0', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '0';
|
||||
ctrl.panel.yaxes[0].max = '0';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[120,10],[160,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to 0 and max to 0', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(0);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(0);
|
||||
});
|
||||
});
|
||||
describe('and negative values used', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = '-10';
|
||||
ctrl.panel.yaxes[0].max = '-13.14';
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[120,10],[160,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min and max to negative', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(-10);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14);
|
||||
});
|
||||
});
|
||||
});
|
||||
graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) {
|
||||
describe('and Y-Min is 0 and Y-Max is 100', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = 0;
|
||||
ctrl.panel.yaxes[0].max = 100;
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[120,10],[160,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to 0 and max to 100', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(0);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(100);
|
||||
});
|
||||
});
|
||||
describe('and Y-Min is -100 and Y-Max is -10.5', function() {
|
||||
ctx.setup(function(ctrl, data) {
|
||||
ctrl.panel.yaxes[0].min = -100;
|
||||
ctrl.panel.yaxes[0].max = -10.5;
|
||||
data[0] = new TimeSeries({
|
||||
datapoints: [[120,10],[160,20]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set min to -100 and max to -10.5', function() {
|
||||
expect(ctx.plotOptions.yaxes[0].min).to.be(-100);
|
||||
expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
// graphScenario('when using flexible Y-Min and Y-Max settings', function(ctx) {
|
||||
// describe('and Y-Min is <100 and Y-Max is >200 and values within range', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '<100';
|
||||
// ctrl.panel.yaxes[0].max = '>200';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[120,10],[160,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to 100 and max to 200', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(100);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(200);
|
||||
// });
|
||||
// });
|
||||
// describe('and Y-Min is <100 and Y-Max is >200 and values outside range', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '<100';
|
||||
// ctrl.panel.yaxes[0].max = '>200';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[99,10],[201,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to auto and max to auto', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(null);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(null);
|
||||
// });
|
||||
// });
|
||||
// describe('and Y-Min is =10.5 and Y-Max is =10.5', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '=10.5';
|
||||
// ctrl.panel.yaxes[0].max = '=10.5';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[100,10],[120,20], [110,30]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to last value + 10.5 and max to last value + 10.5', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(99.5);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(120.5);
|
||||
// });
|
||||
// });
|
||||
// describe('and Y-Min is ~10.5 and Y-Max is ~10.5', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '~10.5';
|
||||
// ctrl.panel.yaxes[0].max = '~10.5';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[102,10],[104,20], [110,30]], //Also checks precision
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to average value + 10.5 and max to average value + 10.5', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(94.8);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(115.8);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// graphScenario('when using regular Y-Min and Y-Max settings', function(ctx) {
|
||||
// describe('and Y-Min is 100 and Y-Max is 200', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '100';
|
||||
// ctrl.panel.yaxes[0].max = '200';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[120,10],[160,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to 100 and max to 200', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(100);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(200);
|
||||
// });
|
||||
// });
|
||||
// describe('and Y-Min is 0 and Y-Max is 0', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '0';
|
||||
// ctrl.panel.yaxes[0].max = '0';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[120,10],[160,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to 0 and max to 0', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(0);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(0);
|
||||
// });
|
||||
// });
|
||||
// describe('and negative values used', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = '-10';
|
||||
// ctrl.panel.yaxes[0].max = '-13.14';
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[120,10],[160,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min and max to negative', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(-10);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) {
|
||||
// describe('and Y-Min is 0 and Y-Max is 100', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = 0;
|
||||
// ctrl.panel.yaxes[0].max = 100;
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[120,10],[160,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to 0 and max to 100', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(0);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(100);
|
||||
// });
|
||||
// });
|
||||
// describe('and Y-Min is -100 and Y-Max is -10.5', function() {
|
||||
// ctx.setup(function(ctrl, data) {
|
||||
// ctrl.panel.yaxes[0].min = -100;
|
||||
// ctrl.panel.yaxes[0].max = -10.5;
|
||||
// data[0] = new TimeSeries({
|
||||
// datapoints: [[120,10],[160,20]],
|
||||
// alias: 'series1',
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('should set min to -100 and max to -10.5', function() {
|
||||
// expect(ctx.plotOptions.yaxes[0].min).to.be(-100);
|
||||
// expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
@ -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"
|
||||
|
@ -1,2 +0,0 @@
|
||||
<grafana-panel-table-editor>
|
||||
</grafana-panel-table-editor>
|
@ -135,7 +135,7 @@ $gf-form-margin: 0.25rem;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
right: $input-padding-x/2;
|
||||
right: $input-padding-x;
|
||||
background-color: transparent;
|
||||
color: $input-color;
|
||||
font: normal normal normal $font-size-sm/1 FontAwesome;
|
||||
|
@ -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,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() {
|
||||
|
10
vendor/gopkg.in/guregu/null.v3/LICENSE
generated
vendored
Normal file
10
vendor/gopkg.in/guregu/null.v3/LICENSE
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
Copyright (c) 2014, Greg Roseberry
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
75
vendor/gopkg.in/guregu/null.v3/README.md
generated
vendored
Normal file
75
vendor/gopkg.in/guregu/null.v3/README.md
generated
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
## null [](https://godoc.org/github.com/guregu/null) [](http://gocover.io/github.com/guregu/null)
|
||||
`import "gopkg.in/guregu/null.v3"`
|
||||
|
||||
null is a library with reasonable options for dealing with nullable SQL and JSON values
|
||||
|
||||
There are two packages: `null` and its subpackage `zero`.
|
||||
|
||||
Types in `null` will only be considered null on null input, and will JSON encode to `null`. If you need zero and null be considered separate values, use these.
|
||||
|
||||
Types in `zero` are treated like zero values in Go: blank string input will produce a null `zero.String`, and null Strings will JSON encode to `""`. Zero values of these types will be considered null to SQL. If you need zero and null treated the same, use these.
|
||||
|
||||
All types implement `sql.Scanner` and `driver.Valuer`, so you can use this library in place of `sql.NullXXX`. All types also implement: `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, `json.Marshaler`, and `json.Unmarshaler`.
|
||||
|
||||
### null package
|
||||
|
||||
`import "gopkg.in/guregu/null.v3"`
|
||||
|
||||
#### null.String
|
||||
Nullable string.
|
||||
|
||||
Marshals to JSON null if SQL source data is null. Zero (blank) input will not produce a null String. Can unmarshal from `sql.NullString` JSON input or string input.
|
||||
|
||||
#### null.Int
|
||||
Nullable int64.
|
||||
|
||||
Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int. Can unmarshal from `sql.NullInt64` JSON input.
|
||||
|
||||
#### null.Float
|
||||
Nullable float64.
|
||||
|
||||
Marshals to JSON null if SQL source data is null. Zero input will not produce a null Float. Can unmarshal from `sql.NullFloat64` JSON input.
|
||||
|
||||
#### null.Bool
|
||||
Nullable bool.
|
||||
|
||||
Marshals to JSON null if SQL source data is null. False input will not produce a null Bool. Can unmarshal from `sql.NullBool` JSON input.
|
||||
|
||||
#### null.Time
|
||||
|
||||
Marshals to JSON null if SQL source data is null. Uses `time.Time`'s marshaler. Can unmarshal from `pq.NullTime` and similar JSON input.
|
||||
|
||||
### zero package
|
||||
|
||||
`import "gopkg.in/guregu/null.v3/zero"`
|
||||
|
||||
#### zero.String
|
||||
Nullable string.
|
||||
|
||||
Will marshal to a blank string if null. Blank string input produces a null String. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullString` JSON input.
|
||||
|
||||
#### zero.Int
|
||||
Nullable int64.
|
||||
|
||||
Will marshal to 0 if null. 0 produces a null Int. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullInt64` JSON input.
|
||||
|
||||
#### zero.Float
|
||||
Nullable float64.
|
||||
|
||||
Will marshal to 0 if null. 0.0 produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullFloat64` JSON input.
|
||||
|
||||
#### zero.Bool
|
||||
Nullable bool.
|
||||
|
||||
Will marshal to false if null. `false` produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullBool` JSON input.
|
||||
|
||||
#### zero.Time
|
||||
|
||||
Will marshal to the zero time if null. Uses `time.Time`'s marshaler. Can unmarshal from `pq.NullTime` and similar JSON input.
|
||||
|
||||
|
||||
### Bugs
|
||||
`json`'s `",omitempty"` struct tag does not work correctly right now. It will never omit a null or empty String. This might be [fixed eventually](https://github.com/golang/go/issues/4357).
|
||||
|
||||
### License
|
||||
BSD
|
129
vendor/gopkg.in/guregu/null.v3/bool.go
generated
vendored
Normal file
129
vendor/gopkg.in/guregu/null.v3/bool.go
generated
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Bool is a nullable bool.
|
||||
// It does not consider false values to be null.
|
||||
// It will decode to null, not false, if null.
|
||||
type Bool struct {
|
||||
sql.NullBool
|
||||
}
|
||||
|
||||
// NewBool creates a new Bool
|
||||
func NewBool(b bool, valid bool) Bool {
|
||||
return Bool{
|
||||
NullBool: sql.NullBool{
|
||||
Bool: b,
|
||||
Valid: valid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BoolFrom creates a new Bool that will always be valid.
|
||||
func BoolFrom(b bool) Bool {
|
||||
return NewBool(b, true)
|
||||
}
|
||||
|
||||
// BoolFromPtr creates a new Bool that will be null if f is nil.
|
||||
func BoolFromPtr(b *bool) Bool {
|
||||
if b == nil {
|
||||
return NewBool(false, false)
|
||||
}
|
||||
return NewBool(*b, true)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
// It supports number and null input.
|
||||
// 0 will not be considered a null Bool.
|
||||
// It also supports unmarshalling a sql.NullBool.
|
||||
func (b *Bool) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var v interface{}
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case bool:
|
||||
b.Bool = x
|
||||
case map[string]interface{}:
|
||||
err = json.Unmarshal(data, &b.NullBool)
|
||||
case nil:
|
||||
b.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Bool", reflect.TypeOf(v).Name())
|
||||
}
|
||||
b.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
// It will unmarshal to a null Bool if the input is a blank or not an integer.
|
||||
// It will return an error if the input is not an integer, blank, or "null".
|
||||
func (b *Bool) UnmarshalText(text []byte) error {
|
||||
str := string(text)
|
||||
switch str {
|
||||
case "", "null":
|
||||
b.Valid = false
|
||||
return nil
|
||||
case "true":
|
||||
b.Bool = true
|
||||
case "false":
|
||||
b.Bool = false
|
||||
default:
|
||||
b.Valid = false
|
||||
return errors.New("invalid input:" + str)
|
||||
}
|
||||
b.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
// It will encode null if this Bool is null.
|
||||
func (b Bool) MarshalJSON() ([]byte, error) {
|
||||
if !b.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
if !b.Bool {
|
||||
return []byte("false"), nil
|
||||
}
|
||||
return []byte("true"), nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string if this Bool is null.
|
||||
func (b Bool) MarshalText() ([]byte, error) {
|
||||
if !b.Valid {
|
||||
return []byte{}, nil
|
||||
}
|
||||
if !b.Bool {
|
||||
return []byte("false"), nil
|
||||
}
|
||||
return []byte("true"), nil
|
||||
}
|
||||
|
||||
// SetValid changes this Bool's value and also sets it to be non-null.
|
||||
func (b *Bool) SetValid(v bool) {
|
||||
b.Bool = v
|
||||
b.Valid = true
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to this Bool's value, or a nil pointer if this Bool is null.
|
||||
func (b Bool) Ptr() *bool {
|
||||
if !b.Valid {
|
||||
return nil
|
||||
}
|
||||
return &b.Bool
|
||||
}
|
||||
|
||||
// IsZero returns true for invalid Bools, for future omitempty support (Go 1.4?)
|
||||
// A non-null Bool with a 0 value will not be considered zero.
|
||||
func (b Bool) IsZero() bool {
|
||||
return !b.Valid
|
||||
}
|
117
vendor/gopkg.in/guregu/null.v3/float.go
generated
vendored
Normal file
117
vendor/gopkg.in/guregu/null.v3/float.go
generated
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Float is a nullable float64.
|
||||
// It does not consider zero values to be null.
|
||||
// It will decode to null, not zero, if null.
|
||||
type Float struct {
|
||||
sql.NullFloat64
|
||||
}
|
||||
|
||||
// NewFloat creates a new Float
|
||||
func NewFloat(f float64, valid bool) Float {
|
||||
return Float{
|
||||
NullFloat64: sql.NullFloat64{
|
||||
Float64: f,
|
||||
Valid: valid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FloatFrom creates a new Float that will always be valid.
|
||||
func FloatFrom(f float64) Float {
|
||||
return NewFloat(f, true)
|
||||
}
|
||||
|
||||
// FloatFromPtr creates a new Float that be null if f is nil.
|
||||
func FloatFromPtr(f *float64) Float {
|
||||
if f == nil {
|
||||
return NewFloat(0, false)
|
||||
}
|
||||
return NewFloat(*f, true)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
// It supports number and null input.
|
||||
// 0 will not be considered a null Float.
|
||||
// It also supports unmarshalling a sql.NullFloat64.
|
||||
func (f *Float) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var v interface{}
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
f.Float64 = float64(x)
|
||||
case map[string]interface{}:
|
||||
err = json.Unmarshal(data, &f.NullFloat64)
|
||||
case nil:
|
||||
f.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name())
|
||||
}
|
||||
f.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
// It will unmarshal to a null Float if the input is a blank or not an integer.
|
||||
// It will return an error if the input is not an integer, blank, or "null".
|
||||
func (f *Float) UnmarshalText(text []byte) error {
|
||||
str := string(text)
|
||||
if str == "" || str == "null" {
|
||||
f.Valid = false
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
f.Float64, err = strconv.ParseFloat(string(text), 64)
|
||||
f.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
// It will encode null if this Float is null.
|
||||
func (f Float) MarshalJSON() ([]byte, error) {
|
||||
if !f.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string if this Float is null.
|
||||
func (f Float) MarshalText() ([]byte, error) {
|
||||
if !f.Valid {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||
}
|
||||
|
||||
// SetValid changes this Float's value and also sets it to be non-null.
|
||||
func (f *Float) SetValid(n float64) {
|
||||
f.Float64 = n
|
||||
f.Valid = true
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to this Float's value, or a nil pointer if this Float is null.
|
||||
func (f Float) Ptr() *float64 {
|
||||
if !f.Valid {
|
||||
return nil
|
||||
}
|
||||
return &f.Float64
|
||||
}
|
||||
|
||||
// IsZero returns true for invalid Floats, for future omitempty support (Go 1.4?)
|
||||
// A non-null Float with a 0 value will not be considered zero.
|
||||
func (f Float) IsZero() bool {
|
||||
return !f.Valid
|
||||
}
|
118
vendor/gopkg.in/guregu/null.v3/int.go
generated
vendored
Normal file
118
vendor/gopkg.in/guregu/null.v3/int.go
generated
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Int is an nullable int64.
|
||||
// It does not consider zero values to be null.
|
||||
// It will decode to null, not zero, if null.
|
||||
type Int struct {
|
||||
sql.NullInt64
|
||||
}
|
||||
|
||||
// NewInt creates a new Int
|
||||
func NewInt(i int64, valid bool) Int {
|
||||
return Int{
|
||||
NullInt64: sql.NullInt64{
|
||||
Int64: i,
|
||||
Valid: valid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IntFrom creates a new Int that will always be valid.
|
||||
func IntFrom(i int64) Int {
|
||||
return NewInt(i, true)
|
||||
}
|
||||
|
||||
// IntFromPtr creates a new Int that be null if i is nil.
|
||||
func IntFromPtr(i *int64) Int {
|
||||
if i == nil {
|
||||
return NewInt(0, false)
|
||||
}
|
||||
return NewInt(*i, true)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
// It supports number and null input.
|
||||
// 0 will not be considered a null Int.
|
||||
// It also supports unmarshalling a sql.NullInt64.
|
||||
func (i *Int) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var v interface{}
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
// Unmarshal again, directly to int64, to avoid intermediate float64
|
||||
err = json.Unmarshal(data, &i.Int64)
|
||||
case map[string]interface{}:
|
||||
err = json.Unmarshal(data, &i.NullInt64)
|
||||
case nil:
|
||||
i.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int", reflect.TypeOf(v).Name())
|
||||
}
|
||||
i.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
// It will unmarshal to a null Int if the input is a blank or not an integer.
|
||||
// It will return an error if the input is not an integer, blank, or "null".
|
||||
func (i *Int) UnmarshalText(text []byte) error {
|
||||
str := string(text)
|
||||
if str == "" || str == "null" {
|
||||
i.Valid = false
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
i.Int64, err = strconv.ParseInt(string(text), 10, 64)
|
||||
i.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
// It will encode null if this Int is null.
|
||||
func (i Int) MarshalJSON() ([]byte, error) {
|
||||
if !i.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return []byte(strconv.FormatInt(i.Int64, 10)), nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string if this Int is null.
|
||||
func (i Int) MarshalText() ([]byte, error) {
|
||||
if !i.Valid {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return []byte(strconv.FormatInt(i.Int64, 10)), nil
|
||||
}
|
||||
|
||||
// SetValid changes this Int's value and also sets it to be non-null.
|
||||
func (i *Int) SetValid(n int64) {
|
||||
i.Int64 = n
|
||||
i.Valid = true
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to this Int's value, or a nil pointer if this Int is null.
|
||||
func (i Int) Ptr() *int64 {
|
||||
if !i.Valid {
|
||||
return nil
|
||||
}
|
||||
return &i.Int64
|
||||
}
|
||||
|
||||
// IsZero returns true for invalid Ints, for future omitempty support (Go 1.4?)
|
||||
// A non-null Int with a 0 value will not be considered zero.
|
||||
func (i Int) IsZero() bool {
|
||||
return !i.Valid
|
||||
}
|
110
vendor/gopkg.in/guregu/null.v3/string.go
generated
vendored
Normal file
110
vendor/gopkg.in/guregu/null.v3/string.go
generated
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
// Package null contains SQL types that consider zero input and null input as separate values,
|
||||
// with convenient support for JSON and text marshaling.
|
||||
// Types in this package will always encode to their null value if null.
|
||||
// Use the zero subpackage if you want zero values and null to be treated the same.
|
||||
package null
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// String is a nullable string. It supports SQL and JSON serialization.
|
||||
// It will marshal to null if null. Blank string input will be considered null.
|
||||
type String struct {
|
||||
sql.NullString
|
||||
}
|
||||
|
||||
// StringFrom creates a new String that will never be blank.
|
||||
func StringFrom(s string) String {
|
||||
return NewString(s, true)
|
||||
}
|
||||
|
||||
// StringFromPtr creates a new String that be null if s is nil.
|
||||
func StringFromPtr(s *string) String {
|
||||
if s == nil {
|
||||
return NewString("", false)
|
||||
}
|
||||
return NewString(*s, true)
|
||||
}
|
||||
|
||||
// NewString creates a new String
|
||||
func NewString(s string, valid bool) String {
|
||||
return String{
|
||||
NullString: sql.NullString{
|
||||
String: s,
|
||||
Valid: valid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
// It supports string and null input. Blank string input does not produce a null String.
|
||||
// It also supports unmarshalling a sql.NullString.
|
||||
func (s *String) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var v interface{}
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
s.String = x
|
||||
case map[string]interface{}:
|
||||
err = json.Unmarshal(data, &s.NullString)
|
||||
case nil:
|
||||
s.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.String", reflect.TypeOf(v).Name())
|
||||
}
|
||||
s.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
// It will encode null if this String is null.
|
||||
func (s String) MarshalJSON() ([]byte, error) {
|
||||
if !s.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return json.Marshal(s.String)
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string when this String is null.
|
||||
func (s String) MarshalText() ([]byte, error) {
|
||||
if !s.Valid {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return []byte(s.String), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
// It will unmarshal to a null String if the input is a blank string.
|
||||
func (s *String) UnmarshalText(text []byte) error {
|
||||
s.String = string(text)
|
||||
s.Valid = s.String != ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetValid changes this String's value and also sets it to be non-null.
|
||||
func (s *String) SetValid(v string) {
|
||||
s.String = v
|
||||
s.Valid = true
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to this String's value, or a nil pointer if this String is null.
|
||||
func (s String) Ptr() *string {
|
||||
if !s.Valid {
|
||||
return nil
|
||||
}
|
||||
return &s.String
|
||||
}
|
||||
|
||||
// IsZero returns true for null strings, for potential future omitempty support.
|
||||
func (s String) IsZero() bool {
|
||||
return !s.Valid
|
||||
}
|
135
vendor/gopkg.in/guregu/null.v3/time.go
generated
vendored
Normal file
135
vendor/gopkg.in/guregu/null.v3/time.go
generated
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Time is a nullable time.Time. It supports SQL and JSON serialization.
|
||||
// It will marshal to null if null.
|
||||
type Time struct {
|
||||
Time time.Time
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (t *Time) Scan(value interface{}) error {
|
||||
var err error
|
||||
switch x := value.(type) {
|
||||
case time.Time:
|
||||
t.Time = x
|
||||
case nil:
|
||||
t.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("null: cannot scan type %T into null.Time: %v", value, value)
|
||||
}
|
||||
t.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (t Time) Value() (driver.Value, error) {
|
||||
if !t.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return t.Time, nil
|
||||
}
|
||||
|
||||
// NewTime creates a new Time.
|
||||
func NewTime(t time.Time, valid bool) Time {
|
||||
return Time{
|
||||
Time: t,
|
||||
Valid: valid,
|
||||
}
|
||||
}
|
||||
|
||||
// TimeFrom creates a new Time that will always be valid.
|
||||
func TimeFrom(t time.Time) Time {
|
||||
return NewTime(t, true)
|
||||
}
|
||||
|
||||
// TimeFromPtr creates a new Time that will be null if t is nil.
|
||||
func TimeFromPtr(t *time.Time) Time {
|
||||
if t == nil {
|
||||
return NewTime(time.Time{}, false)
|
||||
}
|
||||
return NewTime(*t, true)
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
// It will encode null if this time is null.
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
if !t.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return t.Time.MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
// It supports string, object (e.g. pq.NullTime and friends)
|
||||
// and null input.
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var v interface{}
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
err = t.Time.UnmarshalJSON(data)
|
||||
case map[string]interface{}:
|
||||
ti, tiOK := x["Time"].(string)
|
||||
valid, validOK := x["Valid"].(bool)
|
||||
if !tiOK || !validOK {
|
||||
return fmt.Errorf(`json: unmarshalling object into Go value of type null.Time requires key "Time" to be of type string and key "Valid" to be of type bool; found %T and %T, respectively`, x["Time"], x["Valid"])
|
||||
}
|
||||
err = t.Time.UnmarshalText([]byte(ti))
|
||||
t.Valid = valid
|
||||
return err
|
||||
case nil:
|
||||
t.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Time", reflect.TypeOf(v).Name())
|
||||
}
|
||||
t.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (t Time) MarshalText() ([]byte, error) {
|
||||
if !t.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return t.Time.MarshalText()
|
||||
}
|
||||
|
||||
func (t *Time) UnmarshalText(text []byte) error {
|
||||
str := string(text)
|
||||
if str == "" || str == "null" {
|
||||
t.Valid = false
|
||||
return nil
|
||||
}
|
||||
if err := t.Time.UnmarshalText(text); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetValid changes this Time's value and sets it to be non-null.
|
||||
func (t *Time) SetValid(v time.Time) {
|
||||
t.Time = v
|
||||
t.Valid = true
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to this Time's value, or a nil pointer if this Time is null.
|
||||
func (t Time) Ptr() *time.Time {
|
||||
if !t.Valid {
|
||||
return nil
|
||||
}
|
||||
return &t.Time
|
||||
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@ -61,6 +61,12 @@
|
||||
"path": "golang.org/x/sync/errgroup",
|
||||
"revision": "316e794f7b5e3df4e95175a45a5fb8b12f85cb4f",
|
||||
"revisionTime": "2016-07-15T18:54:39Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "PoHLopxwkiXxa3uVhezeq/qJ/Vo=",
|
||||
"path": "gopkg.in/guregu/null.v3",
|
||||
"revision": "41961cea0328defc5f95c1c473f89ebf0d1813f6",
|
||||
"revisionTime": "2016-02-28T00:53:16Z"
|
||||
}
|
||||
],
|
||||
"rootPath": "github.com/grafana/grafana"
|
||||
|
Loading…
Reference in New Issue
Block a user