Merge branch 'master' into external-plugins

Conflicts:
	pkg/api/login.go
	public/app/core/routes/all.js
	public/app/core/table_model.ts
	public/app/panels/table/table_model.ts
	public/app/plugins/panels/table/editor.ts
	public/app/plugins/panels/table/table_model.ts
This commit is contained in:
Torkel Ödegaard 2015-12-14 17:28:57 +01:00
commit 201f50b121
127 changed files with 3139 additions and 651 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules node_modules
npm-debug.log
coverage/ coverage/
.aws-config.json .aws-config.json
awsconfig awsconfig

View File

@ -1,4 +1,18 @@
# 2.6.0 (unreleased) # 2.6.0 (2015-12-14)
### New Features
* **Elasticsearch**: Support for pipeline aggregations Moving average and derivative, closes [#2715](https://github.com/grafana/grafana/issues/2715)
* **Elasticsearch**: Support for inline script and missing options for metrics, closes [#3500](https://github.com/grafana/grafana/issues/3500)
* **Syslog**: Support for syslog logging, closes [#3161](https://github.com/grafana/grafana/pull/3161)
* **Timepicker**: Always show refresh button even with refresh rate, closes [#3498](https://github.com/grafana/grafana/pull/3498)
* **Login**: Make it possible to change the login hint on the login page, closes [#2571](https://github.com/grafana/grafana/pull/2571)
### Bug Fixes
* **metric editors**: Fix for clicking typeahead auto dropdown option, fixes [#3428](https://github.com/grafana/grafana/issues/3428)
* **influxdb**: Fixed issue showing Group By label only on first query, fixes [#3453](https://github.com/grafana/grafana/issues/3453)
* **logging**: Add more verbose info logging for http reqeusts, closes [#3405](https://github.com/grafana/grafana/pull/3405)
# 2.6.0-Beta1 (2015-12-04)
### New Table Panel ### New Table Panel
* **table**: New powerful and flexible table panel, closes [#215](https://github.com/grafana/grafana/issues/215) * **table**: New powerful and flexible table panel, closes [#215](https://github.com/grafana/grafana/issues/215)
@ -6,9 +20,9 @@
### Enhancements ### Enhancements
* **CloudWatch**: Support for multiple AWS Credentials, closes [#3053](https://github.com/grafana/grafana/issues/3053), [#3080](https://github.com/grafana/grafana/issues/3080) * **CloudWatch**: Support for multiple AWS Credentials, closes [#3053](https://github.com/grafana/grafana/issues/3053), [#3080](https://github.com/grafana/grafana/issues/3080)
* **Elasticsearch**: Support for dynamic daily indices for annotations, closes [#3061](https://github.com/grafana/grafana/issues/3061) * **Elasticsearch**: Support for dynamic daily indices for annotations, closes [#3061](https://github.com/grafana/grafana/issues/3061)
* **Elasticsearch**: Support for setting min_doc_count for date histogram, closes [#3416](https://github.com/grafana/grafana/issues/3416)
* **Graph Panel**: Option to hide series with all zeroes from legend and tooltip, closes [#1381](https://github.com/grafana/grafana/issues/1381), [#3336](https://github.com/grafana/grafana/issues/3336) * **Graph Panel**: Option to hide series with all zeroes from legend and tooltip, closes [#1381](https://github.com/grafana/grafana/issues/1381), [#3336](https://github.com/grafana/grafana/issues/3336)
### Bug Fixes ### Bug Fixes
* **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086) * **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086)
* **dashboard**: fix for collapse row by clicking on row title, fixes [#3065](https://github.com/grafana/grafana/issues/3065) * **dashboard**: fix for collapse row by clicking on row title, fixes [#3065](https://github.com/grafana/grafana/issues/3065)
@ -16,6 +30,9 @@
* **graph**: layout fix for color picker when right side legend was enabled, fixes [#3093](https://github.com/grafana/grafana/issues/3093) * **graph**: layout fix for color picker when right side legend was enabled, fixes [#3093](https://github.com/grafana/grafana/issues/3093)
* **elasticsearch**: disabling elastic query (via eye) caused error, fixes [#3300](https://github.com/grafana/grafana/issues/3300) * **elasticsearch**: disabling elastic query (via eye) caused error, fixes [#3300](https://github.com/grafana/grafana/issues/3300)
### Breaking changes
* **elasticsearch**: Manual json edited queries are not supported any more (They very barely worked in 2.5)
# 2.5 (2015-10-28) # 2.5 (2015-10-28)
**New Feature: Mix data sources** **New Feature: Mix data sources**

40
Godeps/Godeps.json generated
View File

@ -20,53 +20,53 @@
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/aws", "ImportPath": "github.com/aws/aws-sdk-go/aws",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/endpoints", "ImportPath": "github.com/aws/aws-sdk-go/private/endpoints",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/ec2query", "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/ec2query",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query", "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest", "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil", "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/signer/v4", "ImportPath": "github.com/aws/aws-sdk-go/private/signer/v4",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/private/waiter", "ImportPath": "github.com/aws/aws-sdk-go/private/waiter",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatch", "ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatch",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/aws/aws-sdk-go/service/ec2", "ImportPath": "github.com/aws/aws-sdk-go/service/ec2",
"Comment": "v0.10.4-18-gce51895", "Comment": "v1.0.0",
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
}, },
{ {
"ImportPath": "github.com/davecgh/go-spew/spew", "ImportPath": "github.com/davecgh/go-spew/spew",

View File

@ -13,11 +13,11 @@ var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
// rValuesAtPath returns a slice of values found in value v. The values // rValuesAtPath returns a slice of values found in value v. The values
// in v are explored recursively so all nested values are collected. // in v are explored recursively so all nested values are collected.
func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) []reflect.Value { func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value {
pathparts := strings.Split(path, "||") pathparts := strings.Split(path, "||")
if len(pathparts) > 1 { if len(pathparts) > 1 {
for _, pathpart := range pathparts { for _, pathpart := range pathparts {
vals := rValuesAtPath(v, pathpart, create, caseSensitive) vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
if len(vals) > 0 { if len(vals) > 0 {
return vals return vals
} }
@ -76,7 +76,16 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
return false return false
}) })
if create && value.Kind() == reflect.Ptr && value.IsNil() { if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 {
if !value.IsNil() {
value.Set(reflect.Zero(value.Type()))
}
return []reflect.Value{value}
}
if createPath && value.Kind() == reflect.Ptr && value.IsNil() {
// TODO if the value is the terminus it should not be created
// if the value to be set to its position is nil.
value.Set(reflect.New(value.Type().Elem())) value.Set(reflect.New(value.Type().Elem()))
value = value.Elem() value = value.Elem()
} else { } else {
@ -84,7 +93,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
} }
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map { if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
if !create && value.IsNil() { if !createPath && value.IsNil() {
value = reflect.ValueOf(nil) value = reflect.ValueOf(nil)
} }
} }
@ -116,7 +125,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
// pull out index // pull out index
i := int(*index) i := int(*index)
if i >= value.Len() { // check out of bounds if i >= value.Len() { // check out of bounds
if create { if createPath {
// TODO resize slice // TODO resize slice
} else { } else {
continue continue
@ -127,7 +136,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
value = reflect.Indirect(value.Index(i)) value = reflect.Indirect(value.Index(i))
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map { if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
if !create && value.IsNil() { if !createPath && value.IsNil() {
value = reflect.ValueOf(nil) value = reflect.ValueOf(nil)
} }
} }
@ -176,8 +185,11 @@ func ValuesAtPath(i interface{}, path string) ([]interface{}, error) {
// SetValueAtPath sets a value at the case insensitive lexical path inside // SetValueAtPath sets a value at the case insensitive lexical path inside
// of a structure. // of a structure.
func SetValueAtPath(i interface{}, path string, v interface{}) { func SetValueAtPath(i interface{}, path string, v interface{}) {
if rvals := rValuesAtPath(i, path, true, false); rvals != nil { if rvals := rValuesAtPath(i, path, true, false, v == nil); rvals != nil {
for _, rval := range rvals { for _, rval := range rvals {
if rval.Kind() == reflect.Ptr && rval.IsNil() {
continue
}
setValue(rval, v) setValue(rval, v)
} }
} }

View File

@ -105,4 +105,38 @@ func TestSetValueAtPathSuccess(t *testing.T) {
assert.Equal(t, "test0", s2.B.B.C) assert.Equal(t, "test0", s2.B.B.C)
awsutil.SetValueAtPath(&s2, "A", []Struct{{}}) awsutil.SetValueAtPath(&s2, "A", []Struct{{}})
assert.Equal(t, []Struct{{}}, s2.A) assert.Equal(t, []Struct{{}}, s2.A)
str := "foo"
s3 := Struct{}
awsutil.SetValueAtPath(&s3, "b.b.c", str)
assert.Equal(t, "foo", s3.B.B.C)
s3 = Struct{B: &Struct{B: &Struct{C: str}}}
awsutil.SetValueAtPath(&s3, "b.b.c", nil)
assert.Equal(t, "", s3.B.B.C)
s3 = Struct{}
awsutil.SetValueAtPath(&s3, "b.b.c", nil)
assert.Equal(t, "", s3.B.B.C)
s3 = Struct{}
awsutil.SetValueAtPath(&s3, "b.b.c", &str)
assert.Equal(t, "foo", s3.B.B.C)
var s4 struct{ Name *string }
awsutil.SetValueAtPath(&s4, "Name", str)
assert.Equal(t, str, *s4.Name)
s4 = struct{ Name *string }{}
awsutil.SetValueAtPath(&s4, "Name", nil)
assert.Equal(t, (*string)(nil), s4.Name)
s4 = struct{ Name *string }{Name: &str}
awsutil.SetValueAtPath(&s4, "Name", nil)
assert.Equal(t, (*string)(nil), s4.Name)
s4 = struct{ Name *string }{}
awsutil.SetValueAtPath(&s4, "Name", &str)
assert.Equal(t, str, *s4.Name)
} }

View File

@ -41,11 +41,20 @@ func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, op
Handlers: handlers, Handlers: handlers,
} }
maxRetries := aws.IntValue(cfg.MaxRetries) switch retryer, ok := cfg.Retryer.(request.Retryer); {
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries { case ok:
maxRetries = 3 svc.Retryer = retryer
case cfg.Retryer != nil && cfg.Logger != nil:
s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer)
cfg.Logger.Log(s)
fallthrough
default:
maxRetries := aws.IntValue(cfg.MaxRetries)
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
maxRetries = 3
}
svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
} }
svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
svc.AddDebugHandlers() svc.AddDebugHandlers()

View File

@ -12,6 +12,9 @@ import (
// is nil also. // is nil also.
const UseServiceDefaultRetries = -1 const UseServiceDefaultRetries = -1
// RequestRetryer is an alias for a type that implements the request.Retryer interface.
type RequestRetryer interface{}
// A Config provides service configuration for service clients. By default, // A Config provides service configuration for service clients. By default,
// all clients will use the {defaults.DefaultConfig} structure. // all clients will use the {defaults.DefaultConfig} structure.
type Config struct { type Config struct {
@ -59,6 +62,21 @@ type Config struct {
// configuration. // configuration.
MaxRetries *int MaxRetries *int
// Retryer guides how HTTP requests should be retried in case of recoverable failures.
//
// When nil or the value does not implement the request.Retryer interface,
// the request.DefaultRetryer will be used.
//
// When both Retryer and MaxRetries are non-nil, the former is used and
// the latter ignored.
//
// To set the Retryer field in a type-safe manner and with chaining, use
// the request.WithRetryer helper function:
//
// cfg := request.WithRetryer(aws.NewConfig(), myRetryer)
//
Retryer RequestRetryer
// Disables semantic parameter validation, which validates input for missing // Disables semantic parameter validation, which validates input for missing
// required fields and/or other semantic request input errors. // required fields and/or other semantic request input errors.
DisableParamValidation *bool DisableParamValidation *bool
@ -217,6 +235,10 @@ func mergeInConfig(dst *Config, other *Config) {
dst.MaxRetries = other.MaxRetries dst.MaxRetries = other.MaxRetries
} }
if other.Retryer != nil {
dst.Retryer = other.Retryer
}
if other.DisableParamValidation != nil { if other.DisableParamValidation != nil {
dst.DisableParamValidation = other.DisableParamValidation dst.DisableParamValidation = other.DisableParamValidation
} }

View File

@ -44,12 +44,19 @@ func (r *Request) nextPageTokens() []interface{} {
} }
tokens := []interface{}{} tokens := []interface{}{}
tokenAdded := false
for _, outToken := range r.Operation.OutputTokens { for _, outToken := range r.Operation.OutputTokens {
v, _ := awsutil.ValuesAtPath(r.Data, outToken) v, _ := awsutil.ValuesAtPath(r.Data, outToken)
if len(v) > 0 { if len(v) > 0 {
tokens = append(tokens, v[0]) tokens = append(tokens, v[0])
tokenAdded = true
} else {
tokens = append(tokens, nil)
} }
} }
if !tokenAdded {
return nil
}
return tokens return tokens
} }
@ -85,9 +92,10 @@ func (r *Request) NextPage() *Request {
// return true to keep iterating or false to stop. // return true to keep iterating or false to stop.
func (r *Request) EachPage(fn func(data interface{}, isLastPage bool) (shouldContinue bool)) error { func (r *Request) EachPage(fn func(data interface{}, isLastPage bool) (shouldContinue bool)) error {
for page := r; page != nil; page = page.NextPage() { for page := r; page != nil; page = page.NextPage() {
page.Send() if err := page.Send(); err != nil {
shouldContinue := fn(page.Data, !page.HasNextPage()) return err
if page.Error != nil || !shouldContinue { }
if getNextPage := fn(page.Data, !page.HasNextPage()); !getNextPage {
return page.Error return page.Error
} }
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/awstesting/unit" "github.com/aws/aws-sdk-go/awstesting/unit"
"github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3"
) )
@ -314,7 +315,69 @@ func TestPaginationTruncation(t *testing.T) {
assert.Equal(t, []string{"Key1", "Key2"}, results) assert.Equal(t, []string{"Key1", "Key2"}, results)
assert.Nil(t, err) assert.Nil(t, err)
}
func TestPaginationNilToken(t *testing.T) {
client := route53.New(unit.Session)
reqNum := 0
resps := []*route53.ListResourceRecordSetsOutput{
{
ResourceRecordSets: []*route53.ResourceRecordSet{
{Name: aws.String("first.example.com.")},
},
IsTruncated: aws.Bool(true),
NextRecordName: aws.String("second.example.com."),
NextRecordType: aws.String("MX"),
NextRecordIdentifier: aws.String("second"),
MaxItems: aws.String("1"),
},
{
ResourceRecordSets: []*route53.ResourceRecordSet{
{Name: aws.String("second.example.com.")},
},
IsTruncated: aws.Bool(true),
NextRecordName: aws.String("third.example.com."),
NextRecordType: aws.String("MX"),
MaxItems: aws.String("1"),
},
{
ResourceRecordSets: []*route53.ResourceRecordSet{
{Name: aws.String("third.example.com.")},
},
IsTruncated: aws.Bool(false),
MaxItems: aws.String("1"),
},
}
client.Handlers.Send.Clear() // mock sending
client.Handlers.Unmarshal.Clear()
client.Handlers.UnmarshalMeta.Clear()
client.Handlers.ValidateResponse.Clear()
idents := []string{}
client.Handlers.Build.PushBack(func(r *request.Request) {
p := r.Params.(*route53.ListResourceRecordSetsInput)
idents = append(idents, aws.StringValue(p.StartRecordIdentifier))
})
client.Handlers.Unmarshal.PushBack(func(r *request.Request) {
r.Data = resps[reqNum]
reqNum++
})
params := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String("id-zone"),
}
results := []string{}
err := client.ListResourceRecordSetsPages(params, func(p *route53.ListResourceRecordSetsOutput, last bool) bool {
results = append(results, *p.ResourceRecordSets[0].Name)
return true
})
assert.NoError(t, err)
assert.Equal(t, []string{"", "second", ""}, idents)
assert.Equal(t, []string{"first.example.com.", "second.example.com.", "third.example.com."}, results)
} }
// Benchmarks // Benchmarks

View File

@ -3,6 +3,7 @@ package request
import ( import (
"time" "time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
) )
@ -15,6 +16,13 @@ type Retryer interface {
MaxRetries() int MaxRetries() int
} }
// WithRetryer sets a config Retryer value to the given Config returning it
// for chaining.
func WithRetryer(cfg *aws.Config, retryer Retryer) *aws.Config {
cfg.Retryer = retryer
return cfg
}
// retryableCodes is a collection of service response codes which are retry-able // retryableCodes is a collection of service response codes which are retry-able
// without any further action. // without any further action.
var retryableCodes = map[string]struct{}{ var retryableCodes = map[string]struct{}{

View File

@ -5,4 +5,4 @@ package aws
const SDKName = "aws-sdk-go" const SDKName = "aws-sdk-go"
// SDKVersion is the version of this SDK // SDKVersion is the version of this SDK
const SDKVersion = "0.10.4" const SDKVersion = "1.0.0"

View File

@ -5,6 +5,7 @@ import (
"reflect" "reflect"
"time" "time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/request"
@ -47,52 +48,74 @@ func (w *Waiter) Wait() error {
res := method.Call([]reflect.Value{in}) res := method.Call([]reflect.Value{in})
req := res[0].Interface().(*request.Request) req := res[0].Interface().(*request.Request)
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("Waiter")) req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("Waiter"))
if err := req.Send(); err != nil {
return err
}
err := req.Send()
for _, a := range w.Acceptors { for _, a := range w.Acceptors {
if err != nil && a.Matcher != "error" {
// Only matcher error is valid if there is a request error
continue
}
result := false result := false
var vals []interface{}
switch a.Matcher { switch a.Matcher {
case "pathAll": case "pathAll", "path":
if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil { // Require all matches to be equal for result to match
result = true vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
for _, val := range vals { result = true
if !awsutil.DeepEqual(val, a.Expected) { for _, val := range vals {
result = false if !awsutil.DeepEqual(val, a.Expected) {
break result = false
} break
} }
} }
case "pathAny": case "pathAny":
if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil { // Only a single match needs to equal for the result to match
for _, val := range vals { vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
if awsutil.DeepEqual(val, a.Expected) { for _, val := range vals {
result = true if awsutil.DeepEqual(val, a.Expected) {
break result = true
} break
} }
} }
case "status": case "status":
s := a.Expected.(int) s := a.Expected.(int)
result = s == req.HTTPResponse.StatusCode result = s == req.HTTPResponse.StatusCode
case "error":
if aerr, ok := err.(awserr.Error); ok {
result = aerr.Code() == a.Expected.(string)
}
case "pathList":
// ignored matcher
default:
logf(client, "WARNING: Waiter for %s encountered unexpected matcher: %s",
w.Config.Operation, a.Matcher)
} }
if result { if !result {
switch a.State { // If there was no matching result found there is nothing more to do
case "success": // for this response, retry the request.
return nil // waiter completed continue
case "failure":
if req.Error == nil {
return awserr.New("ResourceNotReady",
fmt.Sprintf("failed waiting for successful resource state"), nil)
}
return req.Error // waiter failed
case "retry":
// do nothing, just retry
}
break
} }
switch a.State {
case "success":
// waiter completed
return nil
case "failure":
// Waiter failure state triggered
return awserr.New("ResourceNotReady",
fmt.Sprintf("failed waiting for successful resource state"), err)
case "retry":
// clear the error and retry the operation
err = nil
default:
logf(client, "WARNING: Waiter for %s encountered unexpected state: %s",
w.Config.Operation, a.State)
}
}
if err != nil {
return err
} }
time.Sleep(time.Second * time.Duration(w.Delay)) time.Sleep(time.Second * time.Duration(w.Delay))
@ -101,3 +124,13 @@ func (w *Waiter) Wait() error {
return awserr.New("ResourceNotReady", return awserr.New("ResourceNotReady",
fmt.Sprintf("exceeded %d wait attempts", w.MaxAttempts), nil) fmt.Sprintf("exceeded %d wait attempts", w.MaxAttempts), nil)
} }
func logf(client reflect.Value, msg string, args ...interface{}) {
cfgVal := client.FieldByName("Config")
if !cfgVal.IsValid() {
return
}
if cfg, ok := cfgVal.Interface().(*aws.Config); ok && cfg.Logger != nil {
cfg.Logger.Log(fmt.Sprintf(msg, args...))
}
}

View File

@ -1,6 +1,9 @@
package waiter_test package waiter_test
import ( import (
"bytes"
"io/ioutil"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -41,22 +44,7 @@ func (c *mockClient) MockRequest(input *MockInput) (*request.Request, *MockOutpu
return req, output return req, output
} }
var mockAcceptors = []waiter.WaitAcceptor{ func TestWaiterPathAll(t *testing.T) {
{
State: "success",
Matcher: "pathAll",
Argument: "States[].State",
Expected: "running",
},
{
State: "failure",
Matcher: "pathAny",
Argument: "States[].State",
Expected: "stopping",
},
}
func TestWaiter(t *testing.T) {
svc := &mockClient{Client: awstesting.NewClient(&aws.Config{ svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
Region: aws.String("mock-region"), Region: aws.String("mock-region"),
})} })}
@ -73,13 +61,13 @@ func TestWaiter(t *testing.T) {
{State: aws.String("pending")}, {State: aws.String("pending")},
}, },
}, },
{ // Request 1 { // Request 2
States: []*MockState{ States: []*MockState{
{State: aws.String("running")}, {State: aws.String("running")},
{State: aws.String("pending")}, {State: aws.String("pending")},
}, },
}, },
{ // Request 1 { // Request 3
States: []*MockState{ States: []*MockState{
{State: aws.String("running")}, {State: aws.String("running")},
{State: aws.String("running")}, {State: aws.String("running")},
@ -104,7 +92,83 @@ func TestWaiter(t *testing.T) {
Operation: "Mock", Operation: "Mock",
Delay: 0, Delay: 0,
MaxAttempts: 10, MaxAttempts: 10,
Acceptors: mockAcceptors, Acceptors: []waiter.WaitAcceptor{
{
State: "success",
Matcher: "pathAll",
Argument: "States[].State",
Expected: "running",
},
},
}
w := waiter.Waiter{
Client: svc,
Input: &MockInput{},
Config: waiterCfg,
}
err := w.Wait()
assert.NoError(t, err)
assert.Equal(t, 3, numBuiltReq)
assert.Equal(t, 3, reqNum)
}
func TestWaiterPath(t *testing.T) {
svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
Region: aws.String("mock-region"),
})}
svc.Handlers.Send.Clear() // mock sending
svc.Handlers.Unmarshal.Clear()
svc.Handlers.UnmarshalMeta.Clear()
svc.Handlers.ValidateResponse.Clear()
reqNum := 0
resps := []*MockOutput{
{ // Request 1
States: []*MockState{
{State: aws.String("pending")},
{State: aws.String("pending")},
},
},
{ // Request 2
States: []*MockState{
{State: aws.String("running")},
{State: aws.String("pending")},
},
},
{ // Request 3
States: []*MockState{
{State: aws.String("running")},
{State: aws.String("running")},
},
},
}
numBuiltReq := 0
svc.Handlers.Build.PushBack(func(r *request.Request) {
numBuiltReq++
})
svc.Handlers.Unmarshal.PushBack(func(r *request.Request) {
if reqNum >= len(resps) {
assert.Fail(t, "too many polling requests made")
return
}
r.Data = resps[reqNum]
reqNum++
})
waiterCfg := waiter.Config{
Operation: "Mock",
Delay: 0,
MaxAttempts: 10,
Acceptors: []waiter.WaitAcceptor{
{
State: "success",
Matcher: "path",
Argument: "States[].State",
Expected: "running",
},
},
} }
w := waiter.Waiter{ w := waiter.Waiter{
Client: svc, Client: svc,
@ -135,13 +199,13 @@ func TestWaiterFailure(t *testing.T) {
{State: aws.String("pending")}, {State: aws.String("pending")},
}, },
}, },
{ // Request 1 { // Request 2
States: []*MockState{ States: []*MockState{
{State: aws.String("running")}, {State: aws.String("running")},
{State: aws.String("pending")}, {State: aws.String("pending")},
}, },
}, },
{ // Request 1 { // Request 3
States: []*MockState{ States: []*MockState{
{State: aws.String("running")}, {State: aws.String("running")},
{State: aws.String("stopping")}, {State: aws.String("stopping")},
@ -166,7 +230,20 @@ func TestWaiterFailure(t *testing.T) {
Operation: "Mock", Operation: "Mock",
Delay: 0, Delay: 0,
MaxAttempts: 10, MaxAttempts: 10,
Acceptors: mockAcceptors, Acceptors: []waiter.WaitAcceptor{
{
State: "success",
Matcher: "pathAll",
Argument: "States[].State",
Expected: "running",
},
{
State: "failure",
Matcher: "pathAny",
Argument: "States[].State",
Expected: "stopping",
},
},
} }
w := waiter.Waiter{ w := waiter.Waiter{
Client: svc, Client: svc,
@ -181,3 +258,134 @@ func TestWaiterFailure(t *testing.T) {
assert.Equal(t, 3, numBuiltReq) assert.Equal(t, 3, numBuiltReq)
assert.Equal(t, 3, reqNum) assert.Equal(t, 3, reqNum)
} }
func TestWaiterError(t *testing.T) {
svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
Region: aws.String("mock-region"),
})}
svc.Handlers.Send.Clear() // mock sending
svc.Handlers.Unmarshal.Clear()
svc.Handlers.UnmarshalMeta.Clear()
svc.Handlers.ValidateResponse.Clear()
reqNum := 0
resps := []*MockOutput{
{ // Request 1
States: []*MockState{
{State: aws.String("pending")},
{State: aws.String("pending")},
},
},
{ // Request 2, error case
},
{ // Request 3
States: []*MockState{
{State: aws.String("running")},
{State: aws.String("running")},
},
},
}
numBuiltReq := 0
svc.Handlers.Build.PushBack(func(r *request.Request) {
numBuiltReq++
})
svc.Handlers.Send.PushBack(func(r *request.Request) {
if reqNum == 1 {
r.Error = awserr.New("MockException", "mock exception message", nil)
r.HTTPResponse = &http.Response{
StatusCode: 400,
Status: http.StatusText(400),
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
}
reqNum++
}
})
svc.Handlers.Unmarshal.PushBack(func(r *request.Request) {
if reqNum >= len(resps) {
assert.Fail(t, "too many polling requests made")
return
}
r.Data = resps[reqNum]
reqNum++
})
waiterCfg := waiter.Config{
Operation: "Mock",
Delay: 0,
MaxAttempts: 10,
Acceptors: []waiter.WaitAcceptor{
{
State: "success",
Matcher: "pathAll",
Argument: "States[].State",
Expected: "running",
},
{
State: "retry",
Matcher: "error",
Argument: "",
Expected: "MockException",
},
},
}
w := waiter.Waiter{
Client: svc,
Input: &MockInput{},
Config: waiterCfg,
}
err := w.Wait()
assert.NoError(t, err)
assert.Equal(t, 3, numBuiltReq)
assert.Equal(t, 3, reqNum)
}
func TestWaiterStatus(t *testing.T) {
svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
Region: aws.String("mock-region"),
})}
svc.Handlers.Send.Clear() // mock sending
svc.Handlers.Unmarshal.Clear()
svc.Handlers.UnmarshalMeta.Clear()
svc.Handlers.ValidateResponse.Clear()
reqNum := 0
svc.Handlers.Build.PushBack(func(r *request.Request) {
reqNum++
})
svc.Handlers.Send.PushBack(func(r *request.Request) {
code := 200
if reqNum == 3 {
code = 404
}
r.HTTPResponse = &http.Response{
StatusCode: code,
Status: http.StatusText(code),
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
}
})
waiterCfg := waiter.Config{
Operation: "Mock",
Delay: 0,
MaxAttempts: 10,
Acceptors: []waiter.WaitAcceptor{
{
State: "success",
Matcher: "status",
Argument: "",
Expected: 404,
},
},
}
w := waiter.Waiter{
Client: svc,
Input: &MockInput{},
Config: waiterCfg,
}
err := w.Wait()
assert.NoError(t, err)
assert.Equal(t, 3, reqNum)
}

View File

@ -90,7 +90,7 @@ Replace X.Y.Z by actual version number.
cd $GOPATH/src/github.com/grafana/grafana cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep) go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependencies in your current GOPATH) godep restore (will pull down all golang lib dependencies in your current GOPATH)
godep go run build.go build go run build.go build
``` ```
### Building frontend assets ### Building frontend assets

View File

@ -5,7 +5,7 @@ os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\grafana\grafana clone_folder: c:\gopath\src\github.com\grafana\grafana
environment: environment:
nodejs_version: "0.12.2" nodejs_version: "4"
GOPATH: c:\gopath GOPATH: c:\gopath
install: install:

View File

@ -76,6 +76,14 @@ func main() {
grunt("release") grunt("release")
createLinuxPackages() createLinuxPackages()
case "pkg-rpm":
grunt("release")
createRpmPackages()
case "pkg-deb":
grunt("release")
createDebPackages()
case "latest": case "latest":
makeLatestDistCopies() makeLatestDistCopies()
@ -147,7 +155,7 @@ type linuxPackageOptions struct {
depends []string depends []string
} }
func createLinuxPackages() { func createDebPackages() {
createPackage(linuxPackageOptions{ createPackage(linuxPackageOptions{
packageType: "deb", packageType: "deb",
homeDir: "/usr/share/grafana", homeDir: "/usr/share/grafana",
@ -167,7 +175,9 @@ func createLinuxPackages() {
depends: []string{"adduser", "libfontconfig"}, depends: []string{"adduser", "libfontconfig"},
}) })
}
func createRpmPackages() {
createPackage(linuxPackageOptions{ createPackage(linuxPackageOptions{
packageType: "rpm", packageType: "rpm",
homeDir: "/usr/share/grafana", homeDir: "/usr/share/grafana",
@ -189,6 +199,11 @@ func createLinuxPackages() {
}) })
} }
func createLinuxPackages() {
createDebPackages()
createRpmPackages()
}
func createPackage(options linuxPackageOptions) { func createPackage(options linuxPackageOptions) {
packageRoot, _ := ioutil.TempDir("", "grafana-linux-pack") packageRoot, _ := ioutil.TempDir("", "grafana-linux-pack")
@ -315,6 +330,8 @@ func build(pkg string, tags []string) {
args = append(args, "-o", binary) args = append(args, "-o", binary)
args = append(args, pkg) args = append(args, pkg)
setBuildEnv() setBuildEnv()
runPrint("go", "version")
runPrint("go", args...) runPrint("go", args...)
// Create an md5 checksum of the binary, to be included in the archive for // Create an md5 checksum of the binary, to be included in the archive for

View File

@ -142,6 +142,9 @@ auto_assign_org_role = Viewer
# Require email validation before sign up completes # Require email validation before sign up completes
verify_email_enabled = false verify_email_enabled = false
# Background text for the user field on the login page
login_hint = email or username
#################################### Anonymous Auth ########################## #################################### Anonymous Auth ##########################
[auth.anonymous] [auth.anonymous]
# enable anonymous access # enable anonymous access
@ -245,6 +248,18 @@ daily_rotate = true
# Expired days of log file(delete after max days), default is 7 # Expired days of log file(delete after max days), default is 7
max_days = 7 max_days = 7
[log.syslog]
level =
# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
network =
address =
# Syslog facility. user, daemon and local0 through local7 are valid.
facility =
# Syslog tag. By default, the process' argv[0] is used.
tag =
#################################### AMPQ Event Publisher ########################## #################################### AMPQ Event Publisher ##########################
[event_publisher] [event_publisher]
enabled = false enabled = false

View File

@ -134,6 +134,9 @@
# Default role new users will be automatically assigned (if disabled above is set to true) # Default role new users will be automatically assigned (if disabled above is set to true)
;auto_assign_org_role = Viewer ;auto_assign_org_role = Viewer
# Background text for the user field on the login page
;login_hint = email or username
#################################### Anonymous Auth ########################## #################################### Anonymous Auth ##########################
[auth.anonymous] [auth.anonymous]
# enable anonymous access # enable anonymous access

View File

@ -1 +1 @@
2.5.0 2.6.0

View File

@ -45,6 +45,7 @@ pages:
- ['guides/basic_concepts.md', 'User Guides', 'Basic Concepts'] - ['guides/basic_concepts.md', 'User Guides', 'Basic Concepts']
- ['guides/gettingstarted.md', 'User Guides', 'Getting Started'] - ['guides/gettingstarted.md', 'User Guides', 'Getting Started']
- ['guides/whats-new-in-v2-6.md', 'User Guides', "What's New in Grafana v2.6"]
- ['guides/whats-new-in-v2-5.md', 'User Guides', "What's New in Grafana v2.5"] - ['guides/whats-new-in-v2-5.md', 'User Guides', "What's New in Grafana v2.5"]
- ['guides/whats-new-in-v2-1.md', 'User Guides', "What's New in Grafana v2.1"] - ['guides/whats-new-in-v2-1.md', 'User Guides', "What's New in Grafana v2.1"]
- ['guides/whats-new-in-v2.md', 'User Guides', "What's New in Grafana v2.0"] - ['guides/whats-new-in-v2.md', 'User Guides', "What's New in Grafana v2.0"]
@ -52,13 +53,14 @@ pages:
- ['reference/graph.md', 'Reference', 'Graph Panel'] - ['reference/graph.md', 'Reference', 'Graph Panel']
- ['reference/singlestat.md', 'Reference', 'Singlestat Panel'] - ['reference/singlestat.md', 'Reference', 'Singlestat Panel']
- ['reference/table_panel.md', 'Reference', 'Table Panel']
- ['reference/dashlist.md', 'Reference', 'Dashboard List Panel'] - ['reference/dashlist.md', 'Reference', 'Dashboard List Panel']
- ['reference/sharing.md', 'Reference', 'Sharing'] - ['reference/sharing.md', 'Reference', 'Sharing']
- ['reference/annotations.md', 'Reference', 'Annotations'] - ['reference/annotations.md', 'Reference', 'Annotations']
- ['reference/timerange.md', 'Reference', 'Time Range Controls'] - ['reference/timerange.md', 'Reference', 'Time Range']
- ['reference/search.md', 'Reference', 'Dashboard Search'] - ['reference/search.md', 'Reference', 'Search']
- ['reference/templating.md', 'Reference', 'Templated Dashboards'] - ['reference/templating.md', 'Reference', 'Templating']
- ['reference/scripting.md', 'Reference', 'Scripted Dashboards'] - ['reference/scripting.md', 'Reference', 'Scripting']
- ['reference/playlist.md', 'Reference', 'Playlist'] - ['reference/playlist.md', 'Reference', 'Playlist']
- ['reference/export_import.md', 'Reference', 'Import & Export'] - ['reference/export_import.md', 'Reference', 'Import & Export']
- ['reference/admin.md', 'Reference', 'Administration'] - ['reference/admin.md', 'Reference', 'Administration']

View File

@ -63,15 +63,10 @@ Name | Description
`namespaces()` | Returns a list of namespaces CloudWatch support. `namespaces()` | Returns a list of namespaces CloudWatch support.
`metrics(namespace)` | Returns a list of metrics in the namespace. `metrics(namespace)` | Returns a list of metrics in the namespace.
`dimension_keys(namespace)` | Returns a list of dimension keys in the namespace. `dimension_keys(namespace)` | Returns a list of dimension keys in the namespace.
`dimension_values(region, namespace, metric)` | Returns a list of dimension values matching the specified `region`, `namespace` and `metric`. `dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
If you want to filter dimension values by other dimension key/value pair, you can specify optional parameter like this.
```sql
dimension_values(region, namespace, metric, dim_key1=dim_val1,dim_key2=dim_val2,...)
```
![](/img/v2/cloudwatch_templating.png) ![](/img/v2/cloudwatch_templating.png)
## Cost ## Cost

View File

@ -53,6 +53,35 @@ a time pattern for the index name or a wildcard.
The Elasticsearch query editor allows you to select multiple metrics and group by multiple terms or filters. Use the plus and minus icons to the right to add / remove The Elasticsearch query editor allows you to select multiple metrics and group by multiple terms or filters. Use the plus and minus icons to the right to add / remove
metrics or group bys. Some metrics and group by have options, click the option text to expand the the row to view and edit metric or group by options. metrics or group bys. Some metrics and group by have options, click the option text to expand the the row to view and edit metric or group by options.
## Pipeline metrics
If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like
**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric
to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used
in a pipeline metric.
![](/img/elasticsearch/pipeline_metrics_editor.png)
## Templating
The Elasticsearch datasource supports two types of queries you can use to fill template variables with values.
### Possible values for a field
```json
{"find": "terms", "field": "@hostname"}
```
### Fields filtered by type
```json
{"find": "fields", "type": "string"}
```
### Multi format / All format
Use lucene format.
## Annotations ## Annotations
TODO TODO

View File

@ -38,29 +38,47 @@ Password | Database user's password
> Direct access is still supported because in some cases it may be useful to access a Data Source directly depending on the use case and topology of Grafana, the user, and the Data Source. > Direct access is still supported because in some cases it may be useful to access a Data Source directly depending on the use case and topology of Grafana, the user, and the Data Source.
## InfluxDB 0.9.x ## Query Editor
![](/img/influxdb/InfluxDB_09_editor.png) ![](/img/influxdb/editor_v3.png)
You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
panel title, then edit. The editor allows you to select metrics and tags. panel title, then edit. The editor allows you to select metrics and tags.
### Editor tag filters ### Filter data (WHERE)
To add a tag filter click the plus icon to the right of the `WHERE` condition. You can remove tag filters by clicking on To add a tag filter click the plus icon to the right of the `WHERE` condition. You can remove tag filters by clicking on
the tag key and select `--remove tag filter--`. the tag key and select `--remove tag filter--`.
### Regex matching **Regex matching**
You can type in regex patterns for metric names or tag filter values, be sure to wrap the regex pattern in forward slashes (`/`). Grafana You can type in regex patterns for metric names or tag filter values, be sure to wrap the regex pattern in forward slashes (`/`). Grafana
will automatically adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`). will automatically adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`).
### Editor group by ### Field & Aggregation functions
To group by a tag click the plus icon after the `GROUP BY ($interval)` text. Pick a tag from the dropdown that appears. In the `SELECT` row you can specify what fields and functions you want to use. If you have a
You can remove the group by by clicking on the tag and then select `--remove group by--` from the dropdown. group by time you need an aggregation function. Some functions like derivative require an aggregation function.
### Editor RAW Query The editor tries simplify and unify this part of the query. For example:
You can switch to raw query mode by pressing the pen icon. ![](/img/influxdb/select_editor.png)
> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` clause and ends with `order by asc`. The above will generate the following InfluxDB `SELECT` clause:
```sql
SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM ....
```
#### Select multiple fields
Use the plus button and select Field > field to add another SELECT clause. You can also
specify an asterix `*` to select all fields.
### Group By
To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears.
You can remove the group by by clicking on the `tag` and then click on the x icon.
### Text Editor Mode (RAW)
You can switch to raw query mode by clicking hamburger icon and then `Switch editor mode`.
> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter`
> Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands > Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands
> of data points that will hang the browser. > of data points that will hang the browser.
@ -72,7 +90,15 @@ You can switch to raw query mode by pressing the pen icon.
- $tag_hostname = replaced with the value of the hostname tag - $tag_hostname = replaced with the value of the hostname tag
- You can also use [[tag_hostname]] pattern replacement syntax - You can also use [[tag_hostname]] pattern replacement syntax
### Templating ### Table query / raw data
![](/img/influxdb/raw_data.png)
You can remove the group by time by clicking on the `time` part and then the `x` icon. You can
change the option `Format As` to `Table` if you want to show raw data in the `Table` panel.
## Templating
You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query. You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query.
You can then use this variable in your InfluxDB metric queries. You can then use this variable in your InfluxDB metric queries.
@ -93,7 +119,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/
![](/img/influxdb/templating_simple_ex1.png) ![](/img/influxdb/templating_simple_ex1.png)
### Annotations ## Annotations
Annotations allows you to overlay rich event information on top of graphs. Annotations allows you to overlay rich event information on top of graphs.
An example query: An example query:
@ -102,10 +128,4 @@ An example query:
SELECT title, description from events WHERE $timeFilter order asc SELECT title, description from events WHERE $timeFilter order asc
``` ```
### InfluxDB 0.8.x
![](/img/v1/influxdb_editor.png)

View File

@ -0,0 +1,124 @@
---
page_title: What's New in Grafana v2.6
page_description: What's new in Grafana v2.6
page_keywords: grafana, new, changes, features, documentation, table
---
# What's new in Grafana v2.6
## Release highlights
The release includes a new Table panel, a new InfluxDB query editor, support for Elasticsearch Pipeline Metrics and
support for multiple Cloudwatch credentials.
## Table Panel
<img src="/img/v2/table-panel.png">
The new table panel is very flexible, supporting both multiple modes for time series as well as for
table, annotation and raw JSON data. It also provides date formating and value formating and coloring options.
### Time series to rows
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column.
Where `Metric` is the name of the time series.
<img src="/img/v2.6/table_ts_to_rows.png">
### Table Transform
Above you see the options tab for the **Table Panel**. The most important option is the `To Table Transform`.
This option controls how the result of the metric/data query is turned into a table.
### Column Styles
The column styles allow you control how dates and numbers are formatted.
### Time series to columns
This transform allows you to take multiple time series and group them by time. Which will result in a `Time` column
and a column for each time series.
<img src="/img/v2.6/table_ts_to_columns.png">
In the screenshot above you can see how the same time series query as in the previous example can be transformed into
a different table by changing the `To Table Transform` to `Time series to columns`.
### Time series to aggregations
This transform works very similar to the legend values in the Graph panel. Each series gets its own row. In the Options
tab you can select which aggregations you want using the plus button the Columns section.
<img src="/img/v2.6/table_ts_to_aggregations.png">
You have to think about how accurate the aggregations will be. It depends on what aggregation is used in the time series query,
how many data points are fetched, etc. The time series aggregations are calculated by Grafana after aggregation is performed
by the time series database.
### Raw logs queries
If you want to show documents from Elasticsearch pick `Raw Document` as the first metric.
<img src="/img/v2.6/elastic_raw_doc.png">
This in combination with the `JSON Data` table transform will allow you to pick which fields in the document
you want to show in the table.
<img src="/img/v2.6/table_json_data.png">
### Elasticsearch aggregations
You can also make Elasticsearch aggregation queries without a `Date Histogram`. This allows you to
use Elasticsearch metric aggregations to get accurate aggregations for the selected time range.
<img src="/img/v2.6/elastic_aggregations.png">
### Annotations
The table can also show any annotations you have enabled in the dashboard.
<img src="/img/v2.6/table_annotations.png">
## The New InfluxDB Editor
The new InfluxDB editor is a lot more flexible and powerful. It supports nested functions, like `derivative`.
It also uses the same technique as the Graphite query editor in that it presents nested functions as chain of function
transformations. It tries to simplify and unify the complicated nature of InfluxDB's query language.
<img src="/img/v2.6/influxdb_editor_v3.gif">
In the `SELECT` row you can specify what fields and functions you want to use. If you have a
group by time you need an aggregation function. Some functions like derivative require an aggregation function.
The editor tries simplify and unify this part of the query. For example:
![](/img/influxdb/select_editor.png)
The above will generate the following InfluxDB `SELECT` clause:
```sql
SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM ....
```
### Select multiple fields
Use the plus button and select Field > field to add another SELECT clause. You can also
specify an asterix `*` to select all fields.
### Group By
To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears.
You can remove the group by by clicking on the `tag` and then click on the x icon.
The new editor also allows you to remove group by time and select `raw` table data. Which is very useful
in combination with the new Table panel to show raw log data stored in InfluxDB.
<img src="/img/v2.6/table_influxdb_logs.png">
## Pipeline metrics
If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like
**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric
to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used
in a pipeline metric.
![](/img/elasticsearch/pipeline_metrics_editor.png)
## Changelog
For a detailed list and link to github issues for everything included in the 2.6 release please
view the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file.
- - -
<a href="http://grafana.org/download">Download Grafana 2.6 now</a>

View File

@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
Description | Download Description | Download
------------ | ------------- ------------ | -------------
.deb for Debian-based Linux | [grafana_2.5.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.5.0_amd64.deb) .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
## Install ## Install
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.5.0_amd64.deb $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb
$ sudo apt-get install -y adduser libfontconfig $ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_2.5.0_amd64.deb $ sudo dpkg -i grafana_2.6.0_amd64.deb
## APT Repository ## APT Repository

View File

@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
Description | Download Description | Download
------------ | ------------- ------------ | -------------
.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.5.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.5.0-1.x86_64.rpm) .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm)
## Install from package file ## Install from package file
You can install Grafana using Yum directly. You can install Grafana using Yum directly.
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.5.0-1.x86_64.rpm $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm
Or install manually using `rpm`. Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat: #### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig $ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-2.5.0-1.x86_64.rpm $ sudo rpm -Uvh grafana-2.6.0-1.x86_64.rpm
#### On OpenSuse: #### On OpenSuse:
$ sudo rpm -i --nodeps grafana-2.5.0-1.x86_64.rpm $ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm
## Install via YUM Repository ## Install via YUM Repository

View File

@ -0,0 +1,86 @@
----
page_title: Table Panel
page_description: Table Panel Reference
page_keywords: grafana, table, panel, documentation
---
# Table Panel
<img src="/img/v2/table-panel.png">
The new table panel is very flexible, supporting both multiple modes for time series as well as for
table, annotation and raw JSON data. It also provides date formating and value formating and coloring options.
To view table panels in action and test different configurations with sample data, check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase).
## Options overview
The table panel has many ways to manipulate your data for optimal presentation.
<img class="no-shadow" src="/img/v2/table-config.png">
1. `Data`: Control how your query is transformed into a table.
2. `Table Display`: Table display options.
3. `Column Styles`: Column value formating and display options.
## Data to Table
<img class="no-shadow" src="/img/v2/table-data-options.png">
The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric
query should be transformed into a table format. The **Columns (2)** option allows you to select what columns
you want in the table. Only applicable for some transforms.
### Time series to rows
<img src="/img/v2/table_ts_to_rows.png">
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
### Time series to columns
![](/img/v2/table_ts_to_columns.png)
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
### Time series aggregations
![](/img/v2/table_ts_to_aggregations.png)
This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
### Annotations
![](/img/v2/table_annotations.png)
If you have annotations enabled in the dashboard you can have the table show them. If you configure this
mode then any queries you have in the metrics tab will be ignored.
### JSON Data
![](/img/v2/table_json_data.png)
If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this
transform mode and pick the columns using the **Columns** section.
![](/img/v2/elastic_raw_doc.png)
## Table Display
<img class="no-shadow" src="/img/v2/table-display.png">
1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages.
2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
3. `Font Size`: The `font size` field allows you to increase or decrease the size for the panel, relative to the default font size.
## Column Styles
The column styles allow you control how dates and numbers are formatted.
<img class="no-shadow" src="/img/v2/Column-Options.png">
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
3. `Format`: Specify date format. Only available when `Type` is set to `Date`.
4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
6. `Add column style rule`: Add new column rule.

View File

@ -1,3 +1,4 @@
<li><a class='version' href='/v2.6'>Version v2.6</a></li>
<li><a class='version' href='/v2.5'>Version v2.5</a></li> <li><a class='version' href='/v2.5'>Version v2.5</a></li>
<li><a class='version' href='/v2.1'>Version v2.1</a></li> <li><a class='version' href='/v2.1'>Version v2.1</a></li>
<li><a class='version' href='/v2.0'>Version v2.0</a></li> <li><a class='version' href='/v2.0'>Version v2.0</a></li>

View File

@ -4,7 +4,7 @@
"company": "Coding Instinct AB" "company": "Coding Instinct AB"
}, },
"name": "grafana", "name": "grafana",
"version": "2.6.0-pre1", "version": "3.0.0-pre1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://github.com/torkelo/grafana.git" "url": "http://github.com/torkelo/grafana.git"

View File

@ -1,6 +1,6 @@
#! /usr/bin/env bash #! /usr/bin/env bash
version=2.5.0 version=2.6.0
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/ec2metadata"
@ -119,7 +120,15 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
Dimensions: reqParam.Parameters.Dimensions, Dimensions: reqParam.Parameters.Dimensions,
} }
resp, err := svc.ListMetrics(params) var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
for _, metric := range metrics {
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
}
return !lastPage
})
if err != nil { if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err) c.JsonApiErr(500, "Unable to call AWS API", err)
return return
@ -160,7 +169,15 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
params.InstanceIds = reqParam.Parameters.InstanceIds params.InstanceIds = reqParam.Parameters.InstanceIds
} }
resp, err := svc.DescribeInstances(params) var resp ec2.DescribeInstancesOutput
err := svc.DescribeInstancesPages(params,
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
for _, reservation := range reservations {
resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
}
return !lastPage
})
if err != nil { if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err) c.JsonApiErr(500, "Unable to call AWS API", err)
return return

View File

@ -15,31 +15,47 @@ func init() {
metricsMap = map[string][]string{ metricsMap = map[string][]string{
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, "AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
"AWS/Billing": {"EstimatedCharges"}, "AWS/Billing": {"EstimatedCharges"},
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
"AWS/ECS": {"CPUUtilization", "MemoryUtilization"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
"AWS/ECS": {"CPUUtilization", "MemoryUtilization"},
"AWS/ElastiCache": { "AWS/ElastiCache": {
"CPUUtilization", "SwapUsage", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "CPUUtilization", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "SwapUsage",
"BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed", "BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed",
"CurrConnections", "Evictions", "Reclaimed", "NewConnections", "BytesUsedForCache", "CacheHits", "CacheMisses", "ReplicationLag", "GetTypeCmds", "SetTypeCmds", "KeyBasedCmds", "StringBasedCmds", "HashBasedCmds", "ListBasedCmds", "SetBasedCmds", "SortedSetBasedCmds", "CurrItems", "BytesUsedForHash", "CmdConfigGet", "CmdConfigSet", "CmdTouch", "CurrConfig", "EvictedUnfetched", "ExpiredUnfetched", "SlabsMoved", "TouchHits", "TouchMisses",
"NewConnections", "NewItems", "UnusedMemory",
"BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress",
"CurrItems", "GetTypeCmds", "HashBasedCmds", "KeyBasedCmds", "ListBasedCmds", "SetBasedCmds", "SetTypeCmds", "SortedSetBasedCmds", "StringBasedCmds",
}, },
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"}, "AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"}, "AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
"AWS/ElasticMapReduce": {"CoreNodesPending", "CoreNodesRunning", "HBaseBackupFailed", "HBaseMostRecentBackupDuration", "HBaseTimeSinceLastSuccessfulBackup", "HDFSBytesRead", "HDFSBytesWritten", "HDFSUtilization", "IsIdle", "JobsFailed", "JobsRunning", "LiveDataNodes", "LiveTaskTrackers", "MapSlotsOpen", "MissingBlocks", "ReduceSlotsOpen", "RemainingMapTasks", "RemainingMapTasksPerSlot", "RemainingReduceTasks", "RunningMapTasks", "RunningReduceTasks", "S3BytesRead", "S3BytesWritten", "TaskNodesPending", "TaskNodesRunning", "TotalLoad"}, "AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
"AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, "AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
"AWS/ML": {"PredictCount", "PredictFailureCount"}, "MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "TotalLoad",
"AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"}, "BackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup",
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy"}, "IsIdle", "ContainerAllocated", "ContainerReserved", "ContainerPending", "AppsCompleted", "AppsFailed", "AppsKilled", "AppsPending", "AppsRunning", "AppsSubmitted",
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"}, "CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "MRTotalNodes", "MRActiveNodes", "MRLostNodes", "MRUnhealthyNodes", "MRDecommissionedNodes", "MRRebootedNodes",
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"}, "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB",
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"}, "HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"},
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut"}, "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"},
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed", "CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"}, "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"},
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"},
"AWS/ML": {"PredictCount", "PredictFailureCount"},
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy"},
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
} }
dimensionsMap = map[string][]string{ dimensionsMap = map[string][]string{
"AWS/AutoScaling": {"AutoScalingGroupName"}, "AWS/AutoScaling": {"AutoScalingGroupName"},
@ -47,13 +63,15 @@ func init() {
"AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {}, "AWS/CloudSearch": {},
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation"}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation"},
"AWS/ECS": {"ClusterName", "ServiceName"},
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"}, "AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
"AWS/EBS": {"VolumeId"}, "AWS/EBS": {"VolumeId"},
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, "AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
"AWS/ECS": {"ClusterName", "ServiceName"},
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"}, "AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
"AWS/ElasticMapReduce": {"ClusterId", "JobId"}, "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ES": {},
"AWS/Kinesis": {"StreamName"}, "AWS/Kinesis": {"StreamName"},
"AWS/Lambda": {"FunctionName"},
"AWS/ML": {"MLModelId", "RequestMode"}, "AWS/ML": {"MLModelId", "RequestMode"},
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
"AWS/Redshift": {"NodeID", "ClusterIdentifier"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"},
@ -62,8 +80,9 @@ func init() {
"AWS/SNS": {"Application", "Platform", "TopicName"}, "AWS/SNS": {"Application", "Platform", "TopicName"},
"AWS/SQS": {"QueueName"}, "AWS/SQS": {"QueueName"},
"AWS/S3": {"BucketName", "StorageType"}, "AWS/S3": {"BucketName", "StorageType"},
"AWS/SWF": {"Domain", "ActivityTypeName", "ActivityTypeVersion"}, "AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"}, "AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
"AWS/WAF": {"Rule", "WebACL"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"}, "AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
} }
} }
@ -113,6 +132,7 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil) c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
return return
} }
sort.Sort(sort.StringSlice(namespaceMetrics))
result := []interface{}{} result := []interface{}{}
for _, name := range namespaceMetrics { for _, name := range namespaceMetrics {
@ -136,6 +156,7 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil) c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
return return
} }
sort.Sort(sort.StringSlice(dimensionValues))
result := []interface{}{} result := []interface{}{}
for _, name := range dimensionValues { for _, name := range dimensionValues {

View File

@ -65,6 +65,7 @@ func GetDataSourceById(c *middleware.Context) Response {
BasicAuth: ds.BasicAuth, BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser, BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword, BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault, IsDefault: ds.IsDefault,
JsonData: ds.JsonData, JsonData: ds.JsonData,
}) })

View File

@ -61,6 +61,7 @@ type DataSource struct {
BasicAuth bool `json:"basicAuth"` BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"` BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"` BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
JsonData map[string]interface{} `json:"jsonData"` JsonData map[string]interface{} `json:"jsonData"`
} }

View File

@ -69,6 +69,9 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
if ds.BasicAuth { if ds.BasicAuth {
dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword) dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
} }
if ds.WithCredentials {
dsMap["withCredentials"] = ds.WithCredentials
}
if ds.Type == m.DS_INFLUXDB_08 { if ds.Type == m.DS_INFLUXDB_08 {
dsMap["username"] = ds.User dsMap["username"] = ds.User

View File

@ -28,6 +28,7 @@ func LoginView(c *middleware.Context) {
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
viewData.Settings["loginHint"] = setting.LoginHint
if !tryLoginUsingRememberCookie(c) { if !tryLoginUsingRememberCookie(c) {
c.HTML(200, VIEW_INDEX, viewData) c.HTML(200, VIEW_INDEX, viewData)

95
pkg/log/syslog.go Normal file
View File

@ -0,0 +1,95 @@
//+build !windows,!nacl,!plan9
package log
import (
"encoding/json"
"errors"
"log/syslog"
)
type SyslogWriter struct {
syslog *syslog.Writer
Network string `json:"network"`
Address string `json:"address"`
Facility string `json:"facility"`
Tag string `json:"tag"`
}
func NewSyslog() LoggerInterface {
return new(SyslogWriter)
}
func (sw *SyslogWriter) Init(config string) error {
if err := json.Unmarshal([]byte(config), sw); err != nil {
return err
}
prio, err := parseFacility(sw.Facility)
if err != nil {
return err
}
w, err := syslog.Dial(sw.Network, sw.Address, prio, sw.Tag)
if err != nil {
return err
}
sw.syslog = w
return nil
}
func (sw *SyslogWriter) WriteMsg(msg string, skip, level int) error {
var err error
switch level {
case TRACE, DEBUG:
err = sw.syslog.Debug(msg)
case INFO:
err = sw.syslog.Info(msg)
case WARN:
err = sw.syslog.Warning(msg)
case ERROR:
err = sw.syslog.Err(msg)
case CRITICAL:
err = sw.syslog.Crit(msg)
case FATAL:
err = sw.syslog.Alert(msg)
default:
err = errors.New("invalid syslog level")
}
return err
}
func (sw *SyslogWriter) Destroy() {
sw.syslog.Close()
}
func (sw *SyslogWriter) Flush() {}
var facilities = map[string]syslog.Priority{
"user": syslog.LOG_USER,
"daemon": syslog.LOG_DAEMON,
"local0": syslog.LOG_LOCAL0,
"local1": syslog.LOG_LOCAL1,
"local2": syslog.LOG_LOCAL2,
"local3": syslog.LOG_LOCAL3,
"local4": syslog.LOG_LOCAL4,
"local5": syslog.LOG_LOCAL5,
"local6": syslog.LOG_LOCAL6,
"local7": syslog.LOG_LOCAL7,
}
func parseFacility(facility string) (syslog.Priority, error) {
prio, ok := facilities[facility]
if !ok {
return syslog.LOG_LOCAL0, errors.New("invalid syslog facility")
}
return prio, nil
}
func init() {
Register("syslog", NewSyslog)
}

View File

@ -32,7 +32,12 @@ func Logger() macaron.Handler {
rw := res.(macaron.ResponseWriter) rw := res.(macaron.ResponseWriter)
c.Next() c.Next()
content := fmt.Sprintf("Completed %s %v %s in %v", req.URL.Path, rw.Status(), http.StatusText(rw.Status()), time.Since(start)) uname := c.GetCookie(setting.CookieUserName)
if len(uname) == 0 {
uname = "-"
}
content := fmt.Sprintf("Completed %s %s \"%s %s %s\" %v %s %d bytes in %dus", c.RemoteAddr(), uname, req.Method, req.URL.Path, req.Proto, rw.Status(), http.StatusText(rw.Status()), rw.Size(), time.Since(start)/time.Microsecond)
switch rw.Status() { switch rw.Status() {
case 200, 304: case 200, 304:

View File

@ -40,6 +40,7 @@ type DataSource struct {
BasicAuth bool BasicAuth bool
BasicAuthUser string BasicAuthUser string
BasicAuthPassword string BasicAuthPassword string
WithCredentials bool
IsDefault bool IsDefault bool
JsonData map[string]interface{} JsonData map[string]interface{}
@ -83,6 +84,7 @@ type AddDataSourceCommand struct {
BasicAuth bool `json:"basicAuth"` BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"` BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"` BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
JsonData map[string]interface{} `json:"jsonData"` JsonData map[string]interface{} `json:"jsonData"`
@ -103,6 +105,7 @@ type UpdateDataSourceCommand struct {
BasicAuth bool `json:"basicAuth"` BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"` BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"` BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
JsonData map[string]interface{} `json:"jsonData"` JsonData map[string]interface{} `json:"jsonData"`

View File

@ -114,12 +114,14 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
BasicAuth: cmd.BasicAuth, BasicAuth: cmd.BasicAuth,
BasicAuthUser: cmd.BasicAuthUser, BasicAuthUser: cmd.BasicAuthUser,
BasicAuthPassword: cmd.BasicAuthPassword, BasicAuthPassword: cmd.BasicAuthPassword,
WithCredentials: cmd.WithCredentials,
JsonData: cmd.JsonData, JsonData: cmd.JsonData,
Updated: time.Now(), Updated: time.Now(),
} }
sess.UseBool("is_default") sess.UseBool("is_default")
sess.UseBool("basic_auth") sess.UseBool("basic_auth")
sess.UseBool("with_credentials")
_, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds) _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds)
if err != nil { if err != nil {

View File

@ -96,4 +96,9 @@ func addDataSourceMigration(mg *Migrator) {
})) }))
mg.AddMigration("Drop old table data_source_v1 #2", NewDropTableMigration("data_source_v1")) mg.AddMigration("Drop old table data_source_v1 #2", NewDropTableMigration("data_source_v1"))
// add column to activate withCredentials option
mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{
Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0",
}))
} }

View File

@ -55,7 +55,7 @@ func (col *Column) StringNoPk(d Dialect) string {
} }
if col.Default != "" { if col.Default != "" {
sql += "DEFAULT " + col.Default + " " sql += "DEFAULT " + d.Default(col) + " "
} }
return sql return sql

View File

@ -17,10 +17,11 @@ type Dialect interface {
SqlType(col *Column) string SqlType(col *Column) string
SupportEngine() bool SupportEngine() bool
LikeStr() string LikeStr() string
Default(col *Column) string
CreateIndexSql(tableName string, index *Index) string CreateIndexSql(tableName string, index *Index) string
CreateTableSql(table *Table) string CreateTableSql(table *Table) string
AddColumnSql(tableName string, Col *Column) string AddColumnSql(tableName string, col *Column) string
CopyTableData(sourceTable string, targetTable string, sourceCols []string, targetCols []string) string CopyTableData(sourceTable string, targetTable string, sourceCols []string, targetCols []string) string
DropTable(tableName string) string DropTable(tableName string) string
DropIndexSql(tableName string, index *Index) string DropIndexSql(tableName string, index *Index) string
@ -71,6 +72,10 @@ func (b *BaseDialect) EqStr() string {
return "=" return "="
} }
func (b *BaseDialect) Default(col *Column) string {
return col.Default
}
func (b *BaseDialect) CreateTableSql(table *Table) string { func (b *BaseDialect) CreateTableSql(table *Table) string {
var sql string var sql string
sql = "CREATE TABLE IF NOT EXISTS " sql = "CREATE TABLE IF NOT EXISTS "

View File

@ -64,6 +64,10 @@ type AddColumnMigration struct {
column *Column column *Column
} }
func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration {
return &AddColumnMigration{tableName: table.Name, column: col}
}
func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration { func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
m.tableName = tableName m.tableName = tableName
return m return m

View File

@ -36,6 +36,17 @@ func (db *Postgres) AutoIncrStr() string {
return "" return ""
} }
func (b *Postgres) Default(col *Column) string {
if col.Type == DB_Bool {
if col.Default == "0" {
return "FALSE"
} else {
return "TRUE"
}
}
return col.Default
}
func (db *Postgres) SqlType(c *Column) string { func (db *Postgres) SqlType(c *Column) string {
var res string var res string
switch t := c.Type; t { switch t := c.Type; t {

View File

@ -82,6 +82,7 @@ var (
AutoAssignOrg bool AutoAssignOrg bool
AutoAssignOrgRole string AutoAssignOrgRole string
VerifyEmailEnabled bool VerifyEmailEnabled bool
LoginHint string
// Http auth // Http auth
AdminUser string AdminUser string
@ -174,6 +175,9 @@ func applyEnvVariableOverrides() {
if len(envValue) > 0 { if len(envValue) > 0 {
key.SetValue(envValue) key.SetValue(envValue)
if strings.Contains(envKey, "PASSWORD") {
envValue = "*********"
}
appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, envValue)) appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, envValue))
} }
} }
@ -188,6 +192,9 @@ func applyCommandLineDefaultProperties(props map[string]string) {
value, exists := props[keyString] value, exists := props[keyString]
if exists { if exists {
key.SetValue(value) key.SetValue(value)
if strings.Contains(keyString, "password") {
value = "*********"
}
appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value)) appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value))
} }
} }
@ -428,6 +435,7 @@ func NewConfigContext(args *CommandLineArgs) error {
AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"}) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"})
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
LoginHint = users.Key("login_hint").String()
// anonymous access // anonymous access
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
@ -565,6 +573,14 @@ func initLogging(args *CommandLineArgs) {
"driver": sec.Key("driver").String(), "driver": sec.Key("driver").String(),
"conn": sec.Key("conn").String(), "conn": sec.Key("conn").String(),
} }
case "syslog":
LogConfigs[i] = util.DynMap{
"level": level,
"network": sec.Key("network").MustString(""),
"address": sec.Key("address").MustString(""),
"facility": sec.Key("facility").MustString("local7"),
"tag": sec.Key("tag").MustString(""),
}
} }
cfgJsonBytes, _ := json.Marshal(LogConfigs[i]) cfgJsonBytes, _ := json.Marshal(LogConfigs[i])

View File

@ -18,6 +18,7 @@ function (angular, coreModule, config) {
$scope.googleAuthEnabled = config.googleAuthEnabled; $scope.googleAuthEnabled = config.googleAuthEnabled;
$scope.githubAuthEnabled = config.githubAuthEnabled; $scope.githubAuthEnabled = config.githubAuthEnabled;
$scope.disableUserSignUp = config.disableUserSignUp; $scope.disableUserSignUp = config.disableUserSignUp;
$scope.loginHint = config.loginHint;
$scope.loginMode = true; $scope.loginMode = true;
$scope.submitBtnText = 'Log in'; $scope.submitBtnText = 'Log in';

View File

@ -55,8 +55,8 @@ function (_, $, coreModule) {
}); });
}; };
$scope.switchToLink = function() { $scope.switchToLink = function(fromClick) {
if (linkMode) { return; } if (linkMode && !fromClick) { return; }
clearTimeout(cancelBlur); clearTimeout(cancelBlur);
cancelBlur = null; cancelBlur = null;
@ -69,7 +69,7 @@ function (_, $, coreModule) {
$scope.inputBlur = function() { $scope.inputBlur = function() {
// happens long before the click event on the typeahead options // happens long before the click event on the typeahead options
// need to have long delay because the blur // need to have long delay because the blur
cancelBlur = setTimeout($scope.switchToLink, 100); cancelBlur = setTimeout($scope.switchToLink, 200);
}; };
$scope.source = function(query, callback) { $scope.source = function(query, callback) {
@ -100,7 +100,7 @@ function (_, $, coreModule) {
} }
$input.val(value); $input.val(value);
$scope.switchToLink(); $scope.switchToLink(true);
return value; return value;
}; };

View File

@ -156,7 +156,7 @@ function (angular, _, coreModule) {
vm.selectionsChanged = function(commitChange) { vm.selectionsChanged = function(commitChange) {
vm.selectedValues = _.filter(vm.options, {selected: true}); vm.selectedValues = _.filter(vm.options, {selected: true});
if (vm.selectedValues.length > 1 && vm.selectedValues.length !== vm.options.length) { if (vm.selectedValues.length > 1) {
if (vm.selectedValues[0].text === 'All') { if (vm.selectedValues[0].text === 'All') {
vm.selectedValues[0].selected = false; vm.selectedValues[0].selected = false;
vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length); vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length);

View File

@ -141,6 +141,9 @@ define([
controller: 'PluginEditCtrl', controller: 'PluginEditCtrl',
resolve: loadOrgBundle, resolve: loadOrgBundle,
}) })
.when('/global-alerts', {
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
})
.otherwise({ .otherwise({
templateUrl: 'app/partials/error.html', templateUrl: 'app/partials/error.html',
controller: 'ErrorCtrl' controller: 'ErrorCtrl'

View File

@ -0,0 +1,39 @@
class TableModel {
columns: any[];
rows: any[];
type: string;
constructor() {
this.columns = [];
this.rows = [];
this.type = 'table';
}
sort(options) {
if (options.col === null || this.columns.length <= options.col) {
return;
}
this.rows.sort(function(a, b) {
a = a[options.col];
b = b[options.col];
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
this.columns[options.col].sort = true;
if (options.desc) {
this.rows.reverse();
this.columns[options.col].desc = true;
}
}
}
export = TableModel;

View File

@ -17,9 +17,9 @@ var spans = {
var rangeOptions = [ var rangeOptions = [
{ from: 'now/d', to: 'now/d', display: 'Today', section: 2 }, { from: 'now/d', to: 'now/d', display: 'Today', section: 2 },
{ from: 'now/d', to: 'now', display: 'The day so far', section: 2 }, { from: 'now/d', to: 'now', display: 'Today so far', section: 2 },
{ from: 'now/w', to: 'now/w', display: 'This week', section: 2 }, { from: 'now/w', to: 'now/w', display: 'This week', section: 2 },
{ from: 'now/w', to: 'now', display: 'Week to date', section: 2 }, { from: 'now/w', to: 'now', display: 'This week so far', section: 2 },
{ from: 'now/M', to: 'now/M', display: 'This month', section: 2 }, { from: 'now/M', to: 'now/M', display: 'This month', section: 2 },
{ from: 'now/y', to: 'now/y', display: 'This year', section: 2 }, { from: 'now/y', to: 'now/y', display: 'This year', section: 2 },

View File

@ -4,33 +4,35 @@
</ul> </ul>
</topnav> </topnav>
<div class="page-container"> <div class="page-container" style="background: transparent; border: 0;">
<div class="page"> <div class="page-wide">
<h2> <h2>
Organizations Organizations
</h2> </h2>
<table class="filter-table form-inline">
<table class="grafana-options-table"> <thead>
<tr> <tr>
<th style="text-align:left">Id</th> <th>Id</th>
<th>Name</th> <th>Name</th>
<th></th> <th></th>
</tr> </tr>
<tr ng-repeat="org in orgs"> </thead>
<td>{{org.id}}</td> <tbody>
<td>{{org.name}}</td> <tr ng-repeat="org in orgs">
<td style="width: 1%"> <td>{{org.id}}</td>
<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small"> <td>{{org.name}}</td>
<i class="fa fa-edit"></i> <td class="text-right">
Edit <a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">
</a> <i class="fa fa-edit"></i>
&nbsp;&nbsp; Edit
<a ng-click="deleteOrg(org)" class="btn btn-danger btn-small"> </a>
<i class="fa fa-remove"></i> &nbsp;&nbsp;
</a> <a ng-click="deleteOrg(org)" class="btn btn-danger btn-small">
</td> <i class="fa fa-remove"></i>
</tr> </a>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>

View File

@ -5,38 +5,42 @@
</ul> </ul>
</topnav> </topnav>
<div class="page-container"> <div class="page-container" style="background: transparent; border: 0;">
<div class="page"> <div class="page-wide">
<h2> <h2>
Users Users
</h2> </h2>
<table class="grafana-options-table"> <table class="filter-table form-inline">
<tr> <thead>
<th style="text-align:left">Id</th> <tr>
<th>Name</th> <th>Id</th>
<th>Login</th> <th>Name</th>
<th>Email</th> <th>Login</th>
<th style="white-space: nowrap">Grafana Admin</th> <th>Email</th>
<th></th> <th style="white-space: nowrap">Grafana Admin</th>
</tr> <th></th>
<tr ng-repeat="user in users"> </tr>
<td>{{user.id}}</td> </thead>
<td>{{user.name}}</td> <tbody>
<td>{{user.login}}</td> <tr ng-repeat="user in users">
<td>{{user.email}}</td> <td>{{user.id}}</td>
<td>{{user.isAdmin}}</td> <td>{{user.name}}</td>
<td style="width: 1%"> <td>{{user.login}}</td>
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small"> <td>{{user.email}}</td>
<i class="fa fa-edit"></i> <td>{{user.isAdmin}}</td>
Edit <td class="text-right">
</a> <a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
&nbsp;&nbsp; <i class="fa fa-edit"></i>
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small"> Edit
<i class="fa fa-remove"></i> </a>
</a> &nbsp;&nbsp;
</td> <a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
</tr> <i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>

View File

@ -0,0 +1,282 @@
<topnav title="Alerting" subnav="false">
<ul class="nav">
<li class="active" ><a href="global-alerts">Global Alerts</a></li>
</ul>
</topnav>
<div class="page-container" style="background: transparent; border: 0;">
<div class="page-wide">
<h2>Global alerts</h2>
<div class="filter-controls-filters">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">Filters:</li>
<li class="tight-form-item">Alert State</li>
<li><!-- <value-select-dropdown></value-select-dropdown> --></li>
<li class="tight-form-item">Dashboards</li>
<li><!-- <value-select-dropdown></value-select-dropdown> --></li>
<li class="tight-form-item">
<a class="pointer">
<i class="fa fa-pencil"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<ul class="filter-controls-actions">
<li>
<div class="dropdown">
<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
<input class="cr1" id="state-enabled" type="checkbox">
<label for="state-enabled" class="cr1"></label> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a>All</a></li>
</ul>
</div>
</li>
<li>
<div class="dropdown">
<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
Bulk Actions &nbsp; <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a>Update notifications</a></li>
</ul>
</div>
</li>
<li>
<button class="btn btn-inverse" data-toggle="dropdown">
<i class="fa fa-fw fa-th-large"></i> New Dashboard from selected
</button>
</li>
<li>
<span class="filter-controls-actions-selected">2 selected, showing 6 of 6 total</span>
</li>
</ul>
<ul class="filter-list">
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert1" type="checkbox">
<label for="alert1" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">Prod CPU Data Writes</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert1.expanded = !alert1.expanded">
<i class="fa fa-angle-right" ng-show="!alert1.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert1.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Prod CPU Data Writes</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state online">Online</span> for 19 hours
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert1.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert2" type="checkbox" checked>
<label for="alert2" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Insanely Super Duper Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">client side full page load</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert2.expanded = !alert2.expanded">
<i class="fa fa-angle-right" ng-show="!alert2.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert2.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Prod DB Reads</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state warn">Warn</span> for 1 hour
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert2.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert3" type="checkbox" checked>
<label for="alert3" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Mildly Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">Memory/CPU</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert3.expanded = !alert3.expanded">
<i class="fa fa-angle-right" ng-show="!alert3.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert3.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Prod CPU Data Writes</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state critical">Online</span> for 10 minutes
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert3.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert4" type="checkbox">
<label for="alert4" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">Stacked lines</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert4.expanded = !alert4.expanded">
<i class="fa fa-angle-right" ng-show="!alert4.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert4.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Critical Thing</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state online">Online</span> for 5 weeks
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert4.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert5" type="checkbox">
<label for="alert5" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Public</a></span>
<span class="filter-list-card-link">Panel: <a href="">More Critical Thing</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert5.expanded = !alert5.expanded">
<i class="fa fa-angle-right" ng-show="!alert5.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert5.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">More Critical Thing</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state online">Online</span> for 2 months
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert5.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
</ul>
</div>
</div>

View File

@ -47,8 +47,9 @@ define([
if (value.length === 15) { if (value.length === 15) {
return moment.utc(value, 'YYYYMMDDTHHmmss'); return moment.utc(value, 'YYYYMMDDTHHmmss');
} }
var epoch = parseInt(value);
if (!_.isNaN(epoch)) { if (!isNaN(value)) {
var epoch = parseInt(value);
return moment(epoch); return moment(epoch);
} }

View File

@ -16,14 +16,12 @@
<span ng-show="ctrl.dashboard.refresh" class="text-warning"> <span ng-show="ctrl.dashboard.refresh" class="text-warning">
&nbsp; &nbsp;
&nbsp;
<i class="fa fa-refresh"></i>
Refresh every {{ctrl.dashboard.refresh}} Refresh every {{ctrl.dashboard.refresh}}
</span> </span>
</a> </a>
</li> </li>
<li class="grafana-menu-refresh" ng-show="!ctrl.dashboard.refresh"> <li class="grafana-menu-refresh">
<a ng-click="ctrl.timeSrv.refreshDashboard()"> <a ng-click="ctrl.timeSrv.refreshDashboard()">
<i class="fa fa-refresh"></i> <i class="fa fa-refresh"></i>
</a> </a>

View File

@ -13,7 +13,7 @@ function (angular, _, config) {
$scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html'; $scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html';
var defaults = {name: '', type: 'graphite', url: '', access: 'proxy' }; var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}};
$scope.indexPatternTypes = [ $scope.indexPatternTypes = [
{name: 'No pattern', value: undefined}, {name: 'No pattern', value: undefined},
@ -24,6 +24,11 @@ function (angular, _, config) {
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'}, {name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
]; ];
$scope.esVersions = [
{name: '1.x', value: 1},
{name: '2.x', value: 2},
];
$scope.init = function() { $scope.init = function() {
$scope.isNew = true; $scope.isNew = true;
$scope.datasources = []; $scope.datasources = [];

View File

@ -1,44 +1,55 @@
<br> <br>
<h5>Http settings</h5> <h5>Http settings</h5>
<div class="tight-form"> <div class="tight-form-container">
<ul class="tight-form-list"> <div class="tight-form">
<li class="tight-form-item" style="width: 80px"> <ul class="tight-form-list">
Url <li class="tight-form-item" style="width: 80px">
</li> Url
<li> </li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input> <li>
</li> <input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
<li class="tight-form-item"> </li>
Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</tip> <li class="tight-form-item">
</li> Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</tip>
<li> </li>
<select class="input-medium tight-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select> <li>
</li> <select class="input-medium tight-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
</ul> </li>
<div class="clearfix"></div> </ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Http Auth
</li>
<li class="tight-form-item">
<editor-checkbox text="Basic Auth" model="current.basicAuth"></editor-checkbox>
</li>
<li class="tight-form-item">
<editor-checkbox text="With Credentials" model="current.withCredentials"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="current.basicAuth">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<i class="fa fa-remove invisible"></i>
</li>
<li class="tight-form-item">
User
</li>
<li ng-if="current.basicAuth">
<input type="text" class="tight-form-input input-medium" style="width: 136px" ng-model='current.basicAuthUser' placeholder="user" required></input>
</li>
<li class="tight-form-item" style="width: 66px" ng-if="current.basicAuth">
Password
</li>
<li ng-if="current.basicAuth">
<input type="password" class="tight-form-input input-medium" ng-model='current.basicAuthPassword' placeholder="password" required></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div> </div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Basic Auth
</li>
<li class="tight-form-item">
<editor-checkbox text="Enable" model="current.basicAuth"></editor-checkbox>
</li>
<li class="tight-form-item" ng-if="current.basicAuth">
User
</li>
<li ng-if="current.basicAuth">
<input type="text" class="tight-form-input input-medium" style="width: 139px" ng-model='current.basicAuthUser' placeholder="user" required></input>
</li>
<li class="tight-form-item" style="width: 67px" ng-if="current.basicAuth">
Password
</li>
<li ng-if="current.basicAuth">
<input type="password" class="tight-form-input input-medium" ng-model='current.basicAuthPassword' placeholder="password" required></input>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -5,47 +5,52 @@
</ul> </ul>
</topnav> </topnav>
<div class="page-container"> <div class="page-container" style="background: transparent; border: 0;">
<div class="page"> <div class="page-wide">
<h2>Data sources</h2> <h2>Data sources</h2>
<div ng-if="datasources.length === 0"> <div ng-if="datasources.length === 0">
<em>No datasources defined</em> <em>No datasources defined</em>
</div> </div>
<table class="grafana-options-table" ng-if="datasources.length > 0"> <table class="filter-table" ng-if="datasources.length > 0">
<tr> <thead>
<td><strong>Name</strong></td> <tr>
<td><strong>Url</strong></td> <th><strong>Name</strong></th>
<td></td> <th><strong>Url</strong></th>
<td></td> <th style="width: 60px;"></th>
<td></td> <th style="width: 65px;"></th>
</tr> <th style="width: 34px;"></th>
<tr ng-repeat="ds in datasources"> </tr>
<td style="width:1%"> </thead>
<i class="fa fa-database"></i> &nbsp; <tbody>
{{ds.name}} <tr ng-repeat="ds in datasources">
</td> <td>
<td style="width:90%"> <a href="datasources/edit/{{ds.id}}">
{{ds.url}} <i class="fa fa-database"></i> &nbsp; {{ds.name}}
</td> </a>
<td style="width:2%" class="text-center"> </td>
<span ng-if="ds.isDefault"> <td>
<span class="label label-info">default</span> <span class="ellipsis">{{ds.url}}</span>
</span> </td>
</td> <td class="text-center">
<td style="width: 1%"> <span ng-if="ds.isDefault">
<a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini"> <span class="label label-info">default</span>
<i class="fa fa-edit"></i> </span>
Edit </td>
</a> <td class="text-right">
</td> <a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini">
<td style="width: 1%"> <i class="fa fa-edit"></i>
<a ng-click="remove(ds)" class="btn btn-danger btn-mini"> Edit
<i class="fa fa-remove"></i> </a>
</a> </td>
</td> <td class="text-right">
</tr> <a ng-click="remove(ds)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>

View File

@ -4,9 +4,8 @@
</ul> </ul>
</topnav> </topnav>
<div class="page-container"> <div class="page-container" style="background: transparent; border: 0;">
<div class="page"> <div class="page-wide">
<h2> <h2>
API Keys API Keys
</h2> </h2>
@ -32,21 +31,25 @@
</ul> </ul>
</form> </form>
<table class="grafana-options-table" style="width: 250px"> <table class="filter-table">
<tr> <thead>
<th style="text-align: left">Name</th> <tr>
<th style="text-align: left">Role</th> <th>Name</th>
<th></th> <th>Role</th>
</tr> <th style="width: 34px;"></th>
<tr ng-repeat="t in tokens"> </tr>
<td>{{t.name}}</td> </thead>
<td>{{t.role}}</td> <tbody>
<td style="width: 1%"> <tr ng-repeat="t in tokens">
<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini"> <td>{{t.name}}</td>
<i class="fa fa-remove"></i> <td>{{t.role}}</td>
</a> <td>
</td> <a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
</tr> <i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>

View File

@ -4,8 +4,8 @@
</ul> </ul>
</topnav> </topnav>
<div class="page-container"> <div class="page-container" style="background: transparent; border: 0;">
<div class="page"> <div class="page-wide">
<h2>Organization users</h2> <h2>Organization users</h2>
@ -18,21 +18,23 @@
<tabset> <tabset>
<tab heading="Users ({{users.length}})"> <tab heading="Users ({{users.length}})">
<table class="grafana-options-table form-inline"> <table class="filter-table form-inline">
<tr> <thead>
<th>Login</th> <tr>
<th>Email</th> <th>Login</th>
<th>Role</th> <th>Email</th>
<th></th> <th>Role</th>
</tr> <th style="width: 34px;"></th>
</tr>
</thead>
<tr ng-repeat="user in users"> <tr ng-repeat="user in users">
<td>{{user.login}}</td> <td>{{user.login}}</td>
<td>{{user.email}}</td> <td><span class="ellipsis">{{user.email}}</span></td>
<td> <td>
<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(user)"> <select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(user)">
</select> </select>
</td> </td>
<td style="width: 1%"> <td>
<a ng-click="removeUser(user)" class="btn btn-danger btn-mini"> <a ng-click="removeUser(user)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i> <i class="fa fa-remove"></i>
</a> </a>
@ -41,36 +43,46 @@
</table> </table>
</tab> </tab>
<tab heading="Pending Invitations ({{pendingInvites.length}})"> <tab heading="Pending Invitations ({{pendingInvites.length}})">
<div class="grafana-list-item" ng-repeat="invite in pendingInvites" ng-click="invite.expanded = !invite.expanded"> <table class="filter-table form-inline">
{{invite.email}} <thead>
<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span> <tr>
<span class="pull-right"> <th>Email</th>
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)"> <th>Name</th>
<i class="fa fa-clipboard"></i> Copy Invite <th></th>
</button> </tr>
&nbsp; </thead>
<a class="pointer"> <tbody ng-repeat="invite in pendingInvites">
<i ng-show="!invite.expanded" class="fa fa-caret-right"></i> <tr ng-click="invite.expanded = !invite.expanded" ng-class="{'expanded': invite.expanded}">
<i ng-show="invite.expanded" class="fa fa-caret-down"></i> <td>{{invite.email}}</td>
</a> <td>{{invite.name}}</td>
</span> <td class="text-right">
<div ng-show="invite.expanded"> <button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
<a href="{{invite.url}}">{{invite.url}}</a><br> <i class="fa fa-clipboard"></i> Copy Invite
<button class="btn btn-inverse btn-mini"> </button>
<i class="fa fa-envelope-o"></i> Resend invite &nbsp;
</button> <button class="btn btn-inverse btn-mini">
&nbsp; Details
<button class="btn btn-inverse btn-mini" ng-click="revokeInvite(invite, $event)"> <i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
<i class="fa fa-remove" style="color: red"></i> Revoke invite <i ng-show="invite.expanded" class="fa fa-caret-down"></i>
</button> </button>
<span style="padding-left: 15px"> </td>
Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em> </tr>
</span> <tr ng-show="invite.expanded">
<div> <td colspan="3">
</div> <a href="{{invite.url}}">{{invite.url}}</a><br><br>
&nbsp;
<button class="btn btn-inverse btn-mini" ng-click="revokeInvite(invite, $event)">
<i class="fa fa-remove" style="color: red"></i> Revoke invite
</button>
<span style="padding-left: 15px">
Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em>
</span>
</td>
</tr>
</tbody>
</table>
</tab> </tab>
</tabset> </tabset>
</div> </div>
</div> </div>

View File

@ -5,8 +5,8 @@
</ul> </ul>
</topnav> </topnav>
<div class="page-container"> <div class="page-container" style="background: transparent; border: 0;">
<div class="page"> <div class="page-wide">
<h2>Profile</h2> <h2>Profile</h2>
@ -62,19 +62,28 @@
<h3>Organizations</h3> <h3>Organizations</h3>
<table class="grafana-options-table"> <table class="filter-table form-inline">
<tr ng-repeat="org in orgs"> <thead>
<td style="width: 98%"><strong>Name: </strong> {{org.name}}</td> <tr>
<td><strong>Role: </strong> {{org.role}}</td> <th>Name</th>
<td class="nobg max-width-btns"> <th>Role</th>
<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId"> <th></th>
Current </tr>
</span> </thead>
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId"> <tbody>
Select <tr ng-repeat="org in orgs">
</a> <td>{{org.name}}</td>
</td> <td>{{org.role}}</td>
</tr> <td class="text-right">
<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
Current
</span>
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
Select
</a>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>

View File

@ -129,7 +129,7 @@
<editor-checkbox text="Include auto interval" model="current.auto" change="runQuery()"></editor-checkbox> <editor-checkbox text="Include auto interval" model="current.auto" change="runQuery()"></editor-checkbox>
</li> </li>
<li class="tight-form-item" ng-show="current.auto"> <li class="tight-form-item" ng-show="current.auto">
Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip> Auto interval steps <tip>How many times should the current time range be divided to calculate the value</tip>
</li> </li>
<li> <li>
<select class="input-mini tight-form-input last" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select> <select class="input-mini tight-form-input last" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>

View File

@ -79,6 +79,7 @@ function (angular, _) {
this.highlightVariablesAsHtml = function(str) { this.highlightVariablesAsHtml = function(str) {
if (!str || !_.isString(str)) { return str; } if (!str || !_.isString(str)) { return str; }
str = _.escape(str);
this._regex.lastIndex = 0; this._regex.lastIndex = 0;
return str.replace(this._regex, function(match, g1, g2) { return str.replace(this._regex, function(match, g1, g2) {
if (self._values[g1 || g2]) { if (self._values[g1 || g2]) {

View File

@ -0,0 +1,220 @@
<div class="editor-row" style="margin-bottom: 20px;">
<span style="float: right; font-size: 12px;"><i>Last updated by Grafana October 4, 2015 12:15:04 by $username</i></span>
<div class="section">
<h5>General Alerting Options</h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">
Alert Title
</li>
<li>
<input type="text" class="input-xlarge tight-form-input"></input>
</li>
<li class="tight-form-item">
Alerting Backend
</li>
<li>
<select class="input-medium tight-form-input">
<option>Grafana Alerting</option>
</select>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="alerting-enabled">Enabled</label>
<input class="cr1" id="alerting-enabled" type="checkbox">
<label for="alerting-enabled" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 20px;">
<h5>Choose your query:</h5>
<p>Select an exising query to alert on:</p>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item">None</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item last">aliasByNode(2)</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">B</li>
<li class="tight-form-item last"><span class="query-keyword">Metric:</span> us-west-2 AWS/EC2 CPUUtilization <span class="query-keyword">Stats:</span> Minimum Maximum <span class="query-keyword">Dimensions</span> InstanceIS <span class="query-segment-operator">=</span> i-b0e8a447 <span class="query-keyword">Alias</span> {{stat}} <span class="query-keyword">Period</span> 60</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">C</li>
<li class="tight-form-item last"><span class="query-keyword">Query:</span> avg(counters_logins) by(server) <span class="query-keyword">Legend Format:</span> {{app}} - {{server}} <span class="query-keyword">Step:</span> 1s <span class="query-keyword">Resolution:</span> 1/2</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">D</li>
<li class="tight-form-item last"><span class="query-keyword">SELECT</span> mean(value) <span class="query-keyword">FROM</span> logins.count <span class="query-keyword">WHERE</span> hostname <span class="query-segment-operator">=</span> /$Hostname$/ <span class="query-keyword">GROUP BY</span> time($internal) hostname</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" checked /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">E</li>
<li class="tight-form-item last"><span class="query-keyword">Metric:</span> apps.backend.backend_01.counters.requests.count <span class="query-keyword">Alias:</span> Bristow <span class="query-keyword">Aggregator:</span> Sum <span class="query-keyword">Downsample:</span> 1m <span class="query-keyword">Aggregator</span> Sum <span class="query-keyword">Tags</span> host = test</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 20px;">
<p>Or write a new custom alerting query:</p>
<div class="section">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item">
<a class="pointer">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
select metric
</li>
<li>
<a class="tight-form-item tight-form-func last dropdown-toggle"><i class="fa fa-plus"></i></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 10px;">
<div class="section">
<h5>Define Your States</h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">
by
</li>
<li>
<select class="input-medium tight-form-input">
<option>Averaging</option>
</select>
</li>
<li class="tight-form-item">
the values in the query over the last
</li>
<li>
<input type="text" class="input-mini tight-form-input last"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 20px;">
<div class="section">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
<span class="alert-state alert-state-warning">Warn</span>
</li>
<li>
<input type="text" class="input-mini tight-form-input" value=">" style="text-align: center;"></input>
</li>
<li>
<input type="text" class="input-mini tight-form-input" value="#B" style="text-align: center;"></input>
</li>
<li class="tight-form-item">
.notify
</li>
<li class="alert-notify-emails">
<bootstrap-tagsinput tagclass="label label-tag label-tag-email"></bootstrap-tagsinput>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="state-enabled">Enabled</label>
<input class="cr1" id="state-enabled" type="checkbox">
<label for="state-enabled" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
<span class="alert-state alert-state-critical">Critical</span>
</li>
<li>
<input type="text" class="input-mini tight-form-input"></input>
</li>
<li>
<input type="text" class="input-mini tight-form-input"></input>
</li>
<li class="tight-form-item">
.notify
</li>
<li class="alert-notify-emails">
<bootstrap-tagsinput tagclass="label label-tag label-tag-email"></bootstrap-tagsinput>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="state-enabled2">Enabled</label>
<input class="cr1" id="state-enabled2" type="checkbox">
<label for="state-enabled2" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>What to Say <span style="float: right; font-size: 12px; font-weight: normal;"><a href="#">Variables</a> | <a href="#">Preview</a></span></h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
Summary
</li>
<li>
<input type="text" class="input-xxlarge tight-form-input last"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
Description
</li>
<li>
<textarea class="tight-form-textarea input-xxlarge last"></textarea>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -24,7 +24,7 @@
<strong>User</strong> <strong>User</strong>
</li> </li>
<li> <li>
<input type="text" name="username" class="tight-form-input last" required ng-model='formModel.user' placeholder="email or username" style="width: 253px"> <input type="text" name="username" class="tight-form-input last" required ng-model='formModel.user' placeholder={{loginHint}} style="width: 253px">
</li> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -25,7 +25,7 @@ function (angular, _) {
var end = convertToCloudWatchTime(options.range.to); var end = convertToCloudWatchTime(options.range.to);
var queries = []; var queries = [];
options = _.clone(options); options = angular.copy(options);
_.each(options.targets, _.bind(function(target) { _.each(options.targets, _.bind(function(target) {
if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) { if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
return; return;
@ -113,23 +113,28 @@ function (angular, _) {
}); });
}; };
CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensions) { CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
var request = { var request = {
region: templateSrv.replace(region), region: templateSrv.replace(region),
action: 'ListMetrics', action: 'ListMetrics',
parameters: { parameters: {
namespace: templateSrv.replace(namespace), namespace: templateSrv.replace(namespace),
metricName: templateSrv.replace(metricName), metricName: templateSrv.replace(metricName),
dimensions: convertDimensionFormat(dimensions, {}), dimensions: convertDimensionFormat(filterDimensions, {}),
} }
}; };
return this.awsRequest(request).then(function(result) { return this.awsRequest(request).then(function(result) {
return _.chain(result.Metrics).map(function(metric) { return _.chain(result.Metrics)
return _.pluck(metric.Dimensions, 'Value'); .pluck('Dimensions')
}).flatten().uniq().sortBy(function(name) { .flatten()
return name; .filter(function(dimension) {
}).map(function(value) { return dimension !== null && dimension.Name === dimensionKey;
})
.pluck('Value')
.uniq()
.sortBy()
.map(function(value) {
return {value: value, text: value}; return {value: value, text: value};
}).value(); }).value();
}); });
@ -174,25 +179,14 @@ function (angular, _) {
return this.getDimensionKeys(dimensionKeysQuery[1]); return this.getDimensionKeys(dimensionKeysQuery[1]);
} }
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?([^)]*))?\)/); var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
if (dimensionValuesQuery) { if (dimensionValuesQuery) {
region = templateSrv.replace(dimensionValuesQuery[1]); region = templateSrv.replace(dimensionValuesQuery[1]);
namespace = templateSrv.replace(dimensionValuesQuery[2]); namespace = templateSrv.replace(dimensionValuesQuery[2]);
metricName = templateSrv.replace(dimensionValuesQuery[3]); metricName = templateSrv.replace(dimensionValuesQuery[3]);
var dimensionPart = templateSrv.replace(dimensionValuesQuery[5]); var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]);
var dimensions = {}; return this.getDimensionValues(region, namespace, metricName, dimensionKey, {});
if (!_.isEmpty(dimensionPart)) {
_.each(dimensionPart.split(','), function(v) {
var t = v.split('=');
if (t.length !== 2) {
throw new Error('Invalid query format');
}
dimensions[t[0]] = t[1];
});
}
return this.getDimensionValues(region, namespace, metricName, dimensions);
} }
var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/); var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
@ -222,7 +216,7 @@ function (angular, _) {
var metricName = 'EstimatedCharges'; var metricName = 'EstimatedCharges';
var dimensions = {}; var dimensions = {};
return this.getDimensionValues(region, namespace, metricName, dimensions).then(function () { return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(function () {
return { status: 'success', message: 'Data source is working', title: 'Success' }; return { status: 'success', message: 'Data source is working', title: 'Success' };
}); });
}; };

View File

@ -76,7 +76,7 @@ function (angular, _) {
} }
}; };
$scope.getDimSegments = function(segment) { $scope.getDimSegments = function(segment, $index) {
if (segment.type === 'operator') { return $q.when([]); } if (segment.type === 'operator') { return $q.when([]); }
var target = $scope.target; var target = $scope.target;
@ -85,7 +85,8 @@ function (angular, _) {
if (segment.type === 'key' || segment.type === 'plus-button') { if (segment.type === 'key' || segment.type === 'plus-button') {
query = $scope.datasource.getDimensionKeys($scope.target.namespace); query = $scope.datasource.getDimensionKeys($scope.target.namespace);
} else if (segment.type === 'value') { } else if (segment.type === 'value') {
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, {}); var dimensionKey = $scope.dimSegments[$index-2].value;
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
} }
return query.then($scope.transformToSegments(true)).then(function(results) { return query.then($scope.transformToSegments(true)).then(function(results) {

View File

@ -165,7 +165,7 @@ describe('CloudWatchDatasource', function() {
}); });
}); });
describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization)', scenario => { describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization,InstanceId)', scenario => {
scenario.setup(() => { scenario.setup(() => {
scenario.requestResponse = { scenario.requestResponse = {
Metrics: [ Metrics: [

View File

@ -92,8 +92,10 @@ function (angular, _, queryDef) {
} }
case 'date_histogram': { case 'date_histogram': {
settings.interval = settings.interval || 'auto'; settings.interval = settings.interval || 'auto';
settings.min_doc_count = settings.min_doc_count || 0;
$scope.agg.field = $scope.target.timeField; $scope.agg.field = $scope.target.timeField;
settingsLinkText = 'Interval: ' + settings.interval; settingsLinkText = 'Interval: ' + settings.interval;
settingsLinkText += ', Min Doc Count: ' + settings.min_doc_count;
} }
} }

View File

@ -19,13 +19,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
function ElasticDatasource(datasource) { function ElasticDatasource(datasource) {
this.type = 'elasticsearch'; this.type = 'elasticsearch';
this.basicAuth = datasource.basicAuth; this.basicAuth = datasource.basicAuth;
this.withCredentials = datasource.withCredentials;
this.url = datasource.url; this.url = datasource.url;
this.name = datasource.name; this.name = datasource.name;
this.index = datasource.index; this.index = datasource.index;
this.timeField = datasource.jsonData.timeField; this.timeField = datasource.jsonData.timeField;
this.esVersion = datasource.jsonData.esVersion;
this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval); this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval);
this.queryBuilder = new ElasticQueryBuilder({ this.queryBuilder = new ElasticQueryBuilder({
timeField: this.timeField timeField: this.timeField,
esVersion: this.esVersion,
}); });
} }
@ -36,8 +39,10 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
data: data data: data
}; };
if (this.basicAuth) { if (this.basicAuth || this.withCredentials) {
options.withCredentials = true; options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = { options.headers = {
"Authorization": this.basicAuth "Authorization": this.basicAuth
}; };
@ -94,7 +99,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n'; var payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n';
return this._post('/_msearch', payload).then(function(res) { return this._post('_msearch', payload).then(function(res) {
var list = []; var list = [];
var hits = res.responses[0].hits.hits; var hits = res.responses[0].hits.hits;
@ -107,7 +112,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
for (var i = 0; i < fieldNames.length; i++) { for (var i = 0; i < fieldNames.length; i++) {
fieldValue = fieldValue[fieldNames[i]]; fieldValue = fieldValue[fieldNames[i]];
if (!fieldValue) { if (!fieldValue) {
console.log('could not find field in annotatation: ', fieldName); console.log('could not find field in annotation: ', fieldName);
return ''; return '';
} }
} }
@ -183,12 +188,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
sentTargets.push(target); sentTargets.push(target);
} }
if (sentTargets.length === 0) {
return $q.when([]);
}
payload = payload.replace(/\$interval/g, options.interval); payload = payload.replace(/\$interval/g, options.interval);
payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf()); payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf());
payload = payload.replace(/\$timeTo/g, options.range.to.valueOf()); payload = payload.replace(/\$timeTo/g, options.range.to.valueOf());
payload = templateSrv.replace(payload, options.scopedVars); payload = templateSrv.replace(payload, options.scopedVars);
return this._post('/_msearch', payload).then(function(res) { return this._post('_msearch', payload).then(function(res) {
return new ElasticResponse(sentTargets, res).getTimeSeries(); return new ElasticResponse(sentTargets, res).getTimeSeries();
}); });
}; };

View File

@ -22,7 +22,7 @@ function (angular) {
module.directive('elasticMetricAgg', function() { module.directive('elasticMetricAgg', function() {
return { return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metricAgg.html', templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: 'ElasticMetricAggCtrl', controller: 'ElasticMetricAggCtrl',
restrict: 'E', restrict: 'E',
scope: { scope: {
@ -30,13 +30,14 @@ function (angular) {
index: "=", index: "=",
onChange: "&", onChange: "&",
getFields: "&", getFields: "&",
esVersion: '='
} }
}; };
}); });
module.directive('elasticBucketAgg', function() { module.directive('elasticBucketAgg', function() {
return { return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucketAgg.html', templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
controller: 'ElasticBucketAggCtrl', controller: 'ElasticBucketAggCtrl',
restrict: 'E', restrict: 'E',
scope: { scope: {

View File

@ -15,6 +15,9 @@ function (_, queryDef) {
for (y = 0; y < target.metrics.length; y++) { for (y = 0; y < target.metrics.length; y++) {
metric = target.metrics[y]; metric = target.metrics[y];
if (metric.hide) {
continue;
}
switch(metric.type) { switch(metric.type) {
case 'count': { case 'count': {
@ -76,8 +79,12 @@ function (_, queryDef) {
newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props}; newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props};
for (i = 0; i < esAgg.buckets.length; i++) { for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i]; bucket = esAgg.buckets[i];
value = bucket[metric.id].value;
newSeries.datapoints.push([value, bucket.key]); value = bucket[metric.id];
if (value !== undefined) {
newSeries.datapoints.push([value.value, bucket.key]);
}
} }
seriesList.push(newSeries); seriesList.push(newSeries);
break; break;
@ -193,7 +200,14 @@ function (_, queryDef) {
}); });
} }
if (series.field) { if (series.field && queryDef.isPipelineAgg(series.metric)) {
var appliedAgg = _.findWhere(target.metrics, { id: series.field });
if (appliedAgg) {
metricName += ' ' + queryDef.describeMetric(appliedAgg);
} else {
metricName = 'Unset';
}
} else if (series.field) {
metricName += ' ' + series.field; metricName += ' ' + series.field;
} }

View File

@ -11,16 +11,23 @@ function (angular, _, queryDef) {
module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) { module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
var metricAggs = $scope.target.metrics; var metricAggs = $scope.target.metrics;
$scope.metricAggTypes = queryDef.metricAggTypes; $scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion);
$scope.extendedStats = queryDef.extendedStats; $scope.extendedStats = queryDef.extendedStats;
$scope.pipelineAggOptions = [];
$scope.init = function() { $scope.init = function() {
$scope.agg = metricAggs[$scope.index]; $scope.agg = metricAggs[$scope.index];
$scope.validateModel(); $scope.validateModel();
$scope.updatePipelineAggOptions();
};
$scope.updatePipelineAggOptions = function() {
$scope.pipelineAggOptions = queryDef.getPipelineAggOptions($scope.target);
}; };
$rootScope.onAppEvent('elastic-query-updated', function() { $rootScope.onAppEvent('elastic-query-updated', function() {
$scope.index = _.indexOf(metricAggs, $scope.agg); $scope.index = _.indexOf(metricAggs, $scope.agg);
$scope.updatePipelineAggOptions();
$scope.validateModel(); $scope.validateModel();
}, $scope); }, $scope);
@ -30,17 +37,33 @@ function (angular, _, queryDef) {
$scope.settingsLinkText = ''; $scope.settingsLinkText = '';
$scope.aggDef = _.findWhere($scope.metricAggTypes, {value: $scope.agg.type}); $scope.aggDef = _.findWhere($scope.metricAggTypes, {value: $scope.agg.type});
if (!$scope.agg.field) { if (queryDef.isPipelineAgg($scope.agg.type)) {
$scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
$scope.agg.field = $scope.agg.pipelineAgg;
var pipelineOptions = queryDef.getPipelineOptions($scope.agg);
if (pipelineOptions.length > 0) {
_.each(pipelineOptions, function(opt) {
$scope.agg.settings[opt.text] = $scope.agg.settings[opt.text] || opt.default;
});
$scope.settingsLinkText = 'Options';
}
} else if (!$scope.agg.field) {
$scope.agg.field = 'select field'; $scope.agg.field = 'select field';
} }
switch($scope.agg.type) { switch($scope.agg.type) {
case 'percentiles': { case 'percentiles': {
$scope.agg.settings.percents = $scope.agg.settings.percents || [25,50,75,95,99]; $scope.agg.settings.percents = $scope.agg.settings.percents || [25,50,75,95,99];
$scope.settingsLinkText = 'values: ' + $scope.agg.settings.percents.join(','); $scope.settingsLinkText = 'Values: ' + $scope.agg.settings.percents.join(',');
break; break;
} }
case 'extended_stats': { case 'extended_stats': {
if (_.keys($scope.agg.meta).length === 0) {
$scope.agg.meta.std_deviation_bounds_lower = true;
$scope.agg.meta.std_deviation_bounds_upper = true;
}
var stats = _.reduce($scope.agg.meta, function(memo, val, key) { var stats = _.reduce($scope.agg.meta, function(memo, val, key) {
if (val) { if (val) {
var def = _.findWhere($scope.extendedStats, {value: key}); var def = _.findWhere($scope.extendedStats, {value: key});
@ -48,29 +71,47 @@ function (angular, _, queryDef) {
} }
return memo; return memo;
}, []); }, []);
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
if (stats.length === 0) { $scope.settingsLinkText = 'Stats: ' + stats.join(', ');
$scope.agg.meta.std_deviation_bounds_lower = true;
$scope.agg.meta.std_deviation_bounds_upper = true;
}
break; break;
} }
case 'raw_document': { case 'raw_document': {
$scope.target.metrics = [$scope.agg]; $scope.target.metrics = [$scope.agg];
$scope.target.bucketAggs = []; $scope.target.bucketAggs = [];
break;
}
}
if ($scope.aggDef.supportsInlineScript) {
// I know this stores the inline script twice
// but having it like this simplifes the query_builder
var inlineScript = $scope.agg.inlineScript;
if (inlineScript) {
$scope.agg.settings.script = {inline: inlineScript};
} else {
delete $scope.agg.settings.script;
}
if ($scope.settingsLinkText === '') {
$scope.settingsLinkText = 'Options';
} }
} }
}; };
$scope.toggleOptions = function() { $scope.toggleOptions = function() {
$scope.showOptions = !$scope.showOptions; $scope.showOptions = !$scope.showOptions;
$scope.updatePipelineAggOptions();
};
$scope.onChangeInternal = function() {
$scope.onChange();
}; };
$scope.onTypeChange = function() { $scope.onTypeChange = function() {
$scope.agg.settings = {}; $scope.agg.settings = {};
$scope.agg.meta = {}; $scope.agg.meta = {};
$scope.showOptions = false; $scope.showOptions = false;
$scope.updatePipelineAggOptions();
$scope.onChange(); $scope.onChange();
}; };
@ -94,6 +135,14 @@ function (angular, _, queryDef) {
$scope.onChange(); $scope.onChange();
}; };
$scope.toggleShowMetric = function() {
$scope.agg.hide = !$scope.agg.hide;
if (!$scope.agg.hide) {
delete $scope.agg.hide;
}
$scope.onChange();
};
$scope.init(); $scope.init();
}); });

View File

@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="section"> <div class="section">
<h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</h5> <h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></h5>
<div class="editor-option"> <div class="editor-option">
<input type="text" class="span6" ng-model='currentAnnotation.query' placeholder="tags:deploy"></input> <input type="text" class="span6" ng-model='currentAnnotation.query' placeholder="tags:deploy"></input>
</div> </div>

View File

@ -35,9 +35,9 @@
<div class="tight-form" ng-if="showOptions"> <div class="tight-form" ng-if="showOptions">
<div class="tight-form-inner-box" ng-if="agg.type === 'date_histogram'"> <div class="tight-form-inner-box" ng-if="agg.type === 'date_histogram'">
<div class="tight-form last"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 60px"> <li class="tight-form-item" style="width: 94px">
Interval Interval
</li> </li>
<li> <li>
@ -46,6 +46,17 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 94px">
Min Doc Count
</li>
<li>
<input type="number" class="tight-form-input" ng-model="agg.settings.min_doc_count" ng-blur="onChangeInternal()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div> </div>
<div class="tight-form-inner-box" ng-if="agg.type === 'terms'"> <div class="tight-form-inner-box" ng-if="agg.type === 'terms'">
<div class="tight-form"> <div class="tight-form">

View File

@ -20,7 +20,7 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form last"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 144px"> <li class="tight-form-item" style="width: 144px">
Time field name Time field name
@ -31,3 +31,14 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 144px">
Version
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.esVersion" ng-options="f.value as f.name for f in esVersions"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -1,66 +0,0 @@
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
Metric
</li>
<li>
<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
</li>
<li ng-if="aggDef.requiresField">
<metric-segment-model property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment>
</li>
<li class="tight-form-item last" ng-if="settingsLinkText">
<a ng-click="toggleOptions()">{{settingsLinkText}}</a>
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-if="isFirst">
<a class="pointer" ng-click="addMetricAgg()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-if="!isSingle">
<a class="pointer" ng-click="removeMetricAgg()"><i class="fa fa-minus"></i></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="showOptions">
<div class="tight-form-inner-box">
<div class="tight-form last" ng-if="agg.type === 'percentiles'">
<ul class="tight-form-list">
<li class="tight-form-item">
Percentiles
</li>
<li>
<input type="text" class="input-xlarge tight-form-input last" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div ng-if="agg.type === 'extended_stats'">
<div class="tight-form" ng-repeat="stat in extendedStats">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
{{stat.text}}
</li>
<li class="tight-form-item last">
<editor-checkbox text="" model="agg.meta.{{stat.value}}" change="onChange()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="tight-form last" ng-if="agg.type === 'extended_stats'">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
Sigma
</li>
<li>
<input type="number" class="input-mini tight-form-input last" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,126 @@
<div class="tight-form" ng-class="{'tight-form-disabled': agg.hide}">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
Metric
&nbsp;
<a ng-click="toggleShowMetric()" bs-tooltip="'Click to toggle show / hide metric'">
<i class="fa fa-eye" ng-hide="agg.hide"></i>
<i class="fa fa-eye-slash" ng-show="agg.hide"></i>
</a>
</li>
<li>
<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
</li>
<li ng-if="aggDef.requiresField">
<metric-segment-model property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment-model>
</li>
<li ng-if="aggDef.isPipelineAgg">
<metric-segment-model property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="tight-form-item-xxlarge"></metric-segment-model>
</li>
<li class="tight-form-item last" ng-if="settingsLinkText">
<a ng-click="toggleOptions()">
<i class="fa fa-caret-down" ng-show="showOptions"></i>
<i class="fa fa-caret-right" ng-hide="showOptions"></i>
{{settingsLinkText}}
</a>
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-if="isFirst">
<a class="pointer" ng-click="addMetricAgg()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-if="!isSingle">
<a class="pointer" ng-click="removeMetricAgg()"><i class="fa fa-minus"></i></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="showOptions">
<div class="tight-form-inner-box tight-form-container">
<div class="tight-form" ng-if="agg.type === 'moving_avg'">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 75px;">
Window
</li>
<li>
<input type="number" class="input-medium tight-form-input last" ng-model="agg.settings.window" ng-blur="onChangeInternal()" spellcheck='false'>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="agg.type === 'moving_avg'">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 75px;">
Model
</li>
<li>
<input type="text" class="input-medium tight-form-input last" ng-change="onChangeInternal()" ng-model="agg.settings.model" blur="onChange()" spellcheck='false'>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last" ng-if="agg.type === 'percentiles'">
<ul class="tight-form-list">
<li class="tight-form-item">
Percentiles
</li>
<li>
<input type="text" class="input-xlarge tight-form-input last" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div ng-if="agg.type === 'extended_stats'">
<div class="tight-form" ng-repeat="stat in extendedStats">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
{{stat.text}}
</li>
<li class="tight-form-item last">
<editor-checkbox text="" model="agg.meta.{{stat.value}}" change="onChange()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="tight-form" ng-if="agg.type === 'extended_stats'">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
Sigma
</li>
<li>
<input type="number" class="input-mini tight-form-input last" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="aggDef.supportsInlineScript">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
Script
</li>
<li>
<input type="text" class="input-medium tight-form-input last" empty-to-null ng-model="agg.inlineScript" ng-blur="onChangeInternal()" spellcheck='false' placeholder="_value * 1">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="aggDef.supportsMissing">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
Missing
<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
</li>
<li>
<input type="number" class="input-medium tight-form-input last" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -14,7 +14,6 @@
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
</a> </a>
<ul class="dropdown-menu pull-right" role="menu"> <ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="toggleQueryMode()">Switch editor mode</a></li>
<li role="menuitem"><a tabindex="1" ng-click="duplicateDataQuery(target)">Duplicate</a></li> <li role="menuitem"><a tabindex="1" ng-click="duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index-1)">Move up</a></li> <li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index+1)">Move down</a></li> <li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index+1)">Move down</a></li>
@ -66,7 +65,8 @@
<elastic-metric-agg <elastic-metric-agg
target="target" index="$index" target="target" index="$index"
get-fields="getFields($fieldType)" get-fields="getFields($fieldType)"
on-change="queryUpdated()"> on-change="queryUpdated()"
es-version="esVersion">
</elastic-metric-agg> </elastic-metric-agg>
</div> </div>

View File

@ -1,16 +1,22 @@
define([ define([
"angular" './query_def'
], ],
function (angular) { function (queryDef) {
'use strict'; 'use strict';
function ElasticQueryBuilder(options) { function ElasticQueryBuilder(options) {
this.timeField = options.timeField; this.timeField = options.timeField;
this.esVersion = options.esVersion;
} }
ElasticQueryBuilder.prototype.getRangeFilter = function() { ElasticQueryBuilder.prototype.getRangeFilter = function() {
var filter = {}; var filter = {};
filter[this.timeField] = {"gte": "$timeFrom", "lte": "$timeTo"}; filter[this.timeField] = {"gte": "$timeFrom", "lte": "$timeTo"};
if (this.esVersion >= 2) {
filter[this.timeField]["format"] = "epoch_millis";
}
return filter; return filter;
}; };
@ -45,12 +51,23 @@ function (angular) {
return queryNode; return queryNode;
}; };
ElasticQueryBuilder.prototype.getInterval = function(agg) { ElasticQueryBuilder.prototype.getDateHistogramAgg = function(aggDef) {
if (agg.settings && agg.settings.interval !== 'auto') { var esAgg = {};
return agg.settings.interval; var settings = aggDef.settings || {};
} else { esAgg.interval = settings.interval;
return '$interval'; esAgg.field = this.timeField;
esAgg.min_doc_count = settings.min_doc_count || 0;
esAgg.extended_bounds = {min: "$timeFrom", max: "$timeTo"};
if (esAgg.interval === 'auto') {
esAgg.interval = "$interval";
} }
if (this.esVersion >= 2) {
esAgg.format = "epoch_millis";
}
return esAgg;
}; };
ElasticQueryBuilder.prototype.getFiltersAgg = function(aggDef) { ElasticQueryBuilder.prototype.getFiltersAgg = function(aggDef) {
@ -82,9 +99,11 @@ function (angular) {
}; };
ElasticQueryBuilder.prototype.build = function(target) { ElasticQueryBuilder.prototype.build = function(target) {
if (target.rawQuery) { // make sure query has defaults;
return angular.fromJson(target.rawQuery); target.metrics = target.metrics || [{ type: 'count', id: '1' }];
} target.dsType = 'elasticsearch';
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
target.timeField = this.timeField;
var i, nestedAggs, metric; var i, nestedAggs, metric;
var query = { var query = {
@ -123,12 +142,7 @@ function (angular) {
switch(aggDef.type) { switch(aggDef.type) {
case 'date_histogram': { case 'date_histogram': {
esAgg["date_histogram"] = { esAgg["date_histogram"] = this.getDateHistogramAgg(aggDef);
"interval": this.getInterval(aggDef),
"field": this.timeField,
"min_doc_count": 0,
"extended_bounds": { "min": "$timeFrom", "max": "$timeTo" }
};
break; break;
} }
case 'filters': { case 'filters': {
@ -154,14 +168,25 @@ function (angular) {
continue; continue;
} }
var metricAgg = {field: metric.field}; var aggField = {};
var metricAgg = null;
if (queryDef.isPipelineAgg(metric.type)) {
if (metric.pipelineAgg && /^\d*$/.test(metric.pipelineAgg)) {
metricAgg = { buckets_path: metric.pipelineAgg };
} else {
continue;
}
} else {
metricAgg = {field: metric.field};
}
for (var prop in metric.settings) { for (var prop in metric.settings) {
if (metric.settings.hasOwnProperty(prop) && metric.settings[prop] !== null) { if (metric.settings.hasOwnProperty(prop) && metric.settings[prop] !== null) {
metricAgg[prop] = metric.settings[prop]; metricAgg[prop] = metric.settings[prop];
} }
} }
var aggField = {};
aggField[metric.type] = metricAgg; aggField[metric.type] = metricAgg;
nestedAggs.aggs[metric.id] = aggField; nestedAggs.aggs[metric.id] = aggField;
} }
@ -204,5 +229,4 @@ function (angular) {
}; };
return ElasticQueryBuilder; return ElasticQueryBuilder;
}); });

View File

@ -7,14 +7,13 @@ function (angular) {
var module = angular.module('grafana.controllers'); var module = angular.module('grafana.controllers');
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) { module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) {
$scope.esVersion = $scope.datasource.esVersion;
$scope.init = function() { $scope.init = function() {
var target = $scope.target; var target = $scope.target;
if (!target) { return; } if (!target) { return; }
target.metrics = target.metrics || [{ type: 'count', id: '1' }]; $scope.queryUpdated();
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
target.timeField = $scope.datasource.timeField;
}; };
$scope.getFields = function(type) { $scope.getFields = function(type) {
@ -39,14 +38,6 @@ function (angular) {
return []; return [];
}; };
$scope.toggleQueryMode = function () {
if ($scope.target.rawQuery) {
delete $scope.target.rawQuery;
} else {
$scope.target.rawQuery = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true);
}
};
$scope.init(); $scope.init();
}); });

View File

@ -7,13 +7,15 @@ function (_) {
return { return {
metricAggTypes: [ metricAggTypes: [
{text: "Count", value: 'count', requiresField: false}, {text: "Count", value: 'count', requiresField: false},
{text: "Average", value: 'avg', requiresField: true}, {text: "Average", value: 'avg', requiresField: true, supportsInlineScript: true, supportsMissing: true},
{text: "Sum", value: 'sum', requiresField: true}, {text: "Sum", value: 'sum', requiresField: true, supportsInlineScript: true, supportsMissing: true},
{text: "Max", value: 'max', requiresField: true}, {text: "Max", value: 'max', requiresField: true, supportsInlineScript: true, supportsMissing: true},
{text: "Min", value: 'min', requiresField: true}, {text: "Min", value: 'min', requiresField: true, supportsInlineScript: true, supportsMissing: true},
{text: "Extended Stats", value: 'extended_stats', requiresField: true}, {text: "Extended Stats", value: 'extended_stats', requiresField: true, supportsMissing: true, supportsInlineScript: true},
{text: "Percentiles", value: 'percentiles', requiresField: true}, {text: "Percentiles", value: 'percentiles', requiresField: true, supportsMissing: true, supportsInlineScript: true},
{text: "Unique Count", value: "cardinality", requiresField: true}, {text: "Unique Count", value: "cardinality", requiresField: true, supportsMissing: true},
{text: "Moving Average", value: 'moving_avg', requiresField: false, isPipelineAgg: true, minVersion: 2},
{text: "Derivative", value: 'derivative', requiresField: false, isPipelineAgg: true, minVersion: 2 },
{text: "Raw Document", value: "raw_document", requiresField: false} {text: "Raw Document", value: "raw_document", requiresField: false}
], ],
@ -66,6 +68,53 @@ function (_) {
{text: '1d', value: '1d'}, {text: '1d', value: '1d'},
], ],
pipelineOptions: {
'moving_avg' : [
{text: 'window', default: 5},
{text: 'model', default: 'simple'}
],
'derivative': []
},
getMetricAggTypes: function(esVersion) {
return _.filter(this.metricAggTypes, function(f) {
if (f.minVersion) {
return f.minVersion <= esVersion;
} else {
return true;
}
});
},
getPipelineOptions: function(metric) {
if (!this.isPipelineAgg(metric.type)) {
return [];
}
return this.pipelineOptions[metric.type];
},
isPipelineAgg: function(metricType) {
if (metricType) {
var po = this.pipelineOptions[metricType];
return po !== null && po !== undefined;
}
return false;
},
getPipelineAggOptions: function(targets) {
var self = this;
var result = [];
_.each(targets.metrics, function(metric) {
if (!self.isPipelineAgg(metric.type)) {
result.push({text: self.describeMetric(metric), value: metric.id });
}
});
return result;
},
getOrderByOptions: function(target) { getOrderByOptions: function(target) {
var self = this; var self = this;
var metricRefs = []; var metricRefs = [];

View File

@ -22,14 +22,6 @@ describe('ElasticQueryBuilder', function() {
expect(query.aggs["1"].date_histogram.extended_bounds.min).to.be("$timeFrom"); expect(query.aggs["1"].date_histogram.extended_bounds.min).to.be("$timeFrom");
}); });
it('with raw query', function() {
var query = builder.build({
rawQuery: '{"query": "$lucene_query"}',
});
expect(query.query).to.be("$lucene_query");
});
it('with multiple bucket aggs', function() { it('with multiple bucket aggs', function() {
var query = builder.build({ var query = builder.build({
metrics: [{type: 'count', id: '1'}], metrics: [{type: 'count', id: '1'}],
@ -44,6 +36,39 @@ describe('ElasticQueryBuilder', function() {
expect(query.aggs["2"].aggs["3"].date_histogram.field).to.be("@timestamp"); expect(query.aggs["2"].aggs["3"].date_histogram.field).to.be("@timestamp");
}); });
it('with es1.x and es2.x date histogram queries check time format', function() {
var builder_2x = new ElasticQueryBuilder({
timeField: '@timestamp',
esVersion: 2
});
var query_params = {
metrics: [],
bucketAggs: [
{type: 'date_histogram', field: '@timestamp', id: '1'}
],
};
// format should not be specified in 1.x queries
expect("format" in builder.build(query_params)["aggs"]["1"]["date_histogram"]).to.be(false);
// 2.x query should specify format to be "epoch_millis"
expect(builder_2x.build(query_params)["aggs"]["1"]["date_histogram"]["format"]).to.be("epoch_millis");
});
it('with es1.x and es2.x range filter check time format', function() {
var builder_2x = new ElasticQueryBuilder({
timeField: '@timestamp',
esVersion: 2
});
// format should not be specified in 1.x queries
expect("format" in builder.getRangeFilter()["@timestamp"]).to.be(false);
// 2.x query should specify format to be "epoch_millis"
expect(builder_2x.getRangeFilter()["@timestamp"]["format"]).to.be("epoch_millis");
});
it('with select field', function() { it('with select field', function() {
var query = builder.build({ var query = builder.build({
metrics: [{type: 'avg', field: '@value', id: '1'}], metrics: [{type: 'avg', field: '@value', id: '1'}],
@ -130,4 +155,89 @@ describe('ElasticQueryBuilder', function() {
expect(query.size).to.be(500); expect(query.size).to.be(500);
}); });
it('with moving average', function() {
var query = builder.build({
metrics: [
{
id: '3',
type: 'sum',
field: '@value'
},
{
id: '2',
type: 'moving_avg',
field: '3',
pipelineAgg: '3'
}
],
bucketAggs: [
{type: 'date_histogram', field: '@timestamp', id: '3'}
],
});
var firstLevel = query.aggs["3"];
expect(firstLevel.aggs["2"]).not.to.be(undefined);
expect(firstLevel.aggs["2"].moving_avg).not.to.be(undefined);
expect(firstLevel.aggs["2"].moving_avg.buckets_path).to.be("3");
});
it('with broken moving average', function() {
var query = builder.build({
metrics: [
{
id: '3',
type: 'sum',
field: '@value'
},
{
id: '2',
type: 'moving_avg',
pipelineAgg: '3'
},
{
id: '4',
type: 'moving_avg',
pipelineAgg: 'Metric to apply moving average'
}
],
bucketAggs: [
{ type: 'date_histogram', field: '@timestamp', id: '3' }
],
});
var firstLevel = query.aggs["3"];
expect(firstLevel.aggs["2"]).not.to.be(undefined);
expect(firstLevel.aggs["2"].moving_avg).not.to.be(undefined);
expect(firstLevel.aggs["2"].moving_avg.buckets_path).to.be("3");
expect(firstLevel.aggs["4"]).to.be(undefined);
});
it('with derivative', function() {
var query = builder.build({
metrics: [
{
id: '3',
type: 'sum',
field: '@value'
},
{
id: '2',
type: 'derivative',
pipelineAgg: '3'
}
],
bucketAggs: [
{type: 'date_histogram', field: '@timestamp', id: '3'}
],
});
var firstLevel = query.aggs["3"];
expect(firstLevel.aggs["2"]).not.to.be(undefined);
expect(firstLevel.aggs["2"].derivative).not.to.be(undefined);
expect(firstLevel.aggs["2"].derivative.buckets_path).to.be("3");
});
}); });

View File

@ -0,0 +1,102 @@
///<amd-dependency path="../query_def" name="QueryDef" />
///<amd-dependency path="test/specs/helpers" name="helpers" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
declare var helpers: any;
declare var QueryDef: any;
describe('ElasticQueryDef', function() {
describe('getPipelineAggOptions', function() {
describe('with zero targets', function() {
var response = QueryDef.getPipelineAggOptions([]);
it('should return zero', function() {
expect(response.length).to.be(0);
});
});
describe('with count and sum targets', function() {
var targets = {
metrics: [
{ type: 'count', field: '@value' },
{ type: 'sum', field: '@value' }
]
};
var response = QueryDef.getPipelineAggOptions(targets);
it('should return zero', function() {
expect(response.length).to.be(2);
});
});
describe('with count and moving average targets', function() {
var targets = {
metrics: [
{ type: 'count', field: '@value' },
{ type: 'moving_avg', field: '@value' }
]
};
var response = QueryDef.getPipelineAggOptions(targets);
it('should return one', function() {
expect(response.length).to.be(1);
});
});
describe('with derivatives targets', function() {
var targets = {
metrics: [
{ type: 'derivative', field: '@value' }
]
};
var response = QueryDef.getPipelineAggOptions(targets);
it('should return zero', function() {
expect(response.length).to.be(0);
});
});
});
describe('isPipelineMetric', function() {
describe('moving_avg', function() {
var result = QueryDef.isPipelineAgg('moving_avg');
it('is pipe line metric', function() {
expect(result).to.be(true);
});
});
describe('count', function() {
var result = QueryDef.isPipelineAgg('count');
it('is not pipe line metric', function() {
expect(result).to.be(false);
});
});
});
describe('pipeline aggs depending on esverison', function() {
describe('using esversion undefined', function() {
it('should not get pipeline aggs', function() {
expect(QueryDef.getMetricAggTypes(undefined).length).to.be(9);
});
});
describe('using esversion 1', function() {
it('should not get pipeline aggs', function() {
expect(QueryDef.getMetricAggTypes(1).length).to.be(9);
});
});
describe('using esversion 2', function() {
it('should get pipeline aggs', function() {
expect(QueryDef.getMetricAggTypes(2).length).to.be(11);
});
});
});
});

View File

@ -53,6 +53,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
// replace templated variables // replace templated variables
allQueries = templateSrv.replace(allQueries, options.scopedVars); allQueries = templateSrv.replace(allQueries, options.scopedVars);
return this._seriesQuery(allQueries).then(function(data) { return this._seriesQuery(allQueries).then(function(data) {
if (!data || !data.results) { if (!data || !data.results) {
return []; return [];
@ -63,13 +64,26 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
var result = data.results[i]; var result = data.results[i];
if (!result || !result.series) { continue; } if (!result || !result.series) { continue; }
var alias = (queryTargets[i] || {}).alias; var target = queryTargets[i];
var alias = target.alias;
if (alias) { if (alias) {
alias = templateSrv.replace(alias, options.scopedVars); alias = templateSrv.replace(target.alias, options.scopedVars);
} }
var targetSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }).getTimeSeries();
for (y = 0; y < targetSeries.length; y++) { var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
seriesList.push(targetSeries[y]);
switch(target.resultFormat) {
case 'table': {
seriesList.push(influxSeries.getTable());
break;
}
default: {
var timeSeries = influxSeries.getTimeSeries();
for (y = 0; y < timeSeries.length; y++) {
seriesList.push(timeSeries[y]);
}
break;
}
} }
} }

View File

@ -12,6 +12,8 @@ class InfluxQuery {
constructor(target) { constructor(target) {
this.target = target; this.target = target;
target.dsType = 'influxdb';
target.resultFormat = target.resultFormat || 'time_series';
target.tags = target.tags || []; target.tags = target.tags || [];
target.groupBy = target.groupBy || [ target.groupBy = target.groupBy || [
{type: 'time', params: ['$interval']}, {type: 'time', params: ['$interval']},

View File

@ -1,7 +1,8 @@
define([ define([
'lodash', 'lodash',
'app/core/table_model',
], ],
function (_) { function (_, TableModel) {
'use strict'; 'use strict';
function InfluxSeries(options) { function InfluxSeries(options) {
@ -108,5 +109,44 @@ function (_) {
return list; return list;
}; };
p.getTable = function() {
var table = new TableModel();
var self = this;
var i, j;
if (self.series.length === 0) {
return table;
}
_.each(self.series, function(series, seriesIndex) {
if (seriesIndex === 0) {
table.columns.push({text: 'Time', type: 'time'});
_.each(_.keys(series.tags), function(key) {
table.columns.push({text: key});
});
for (j = 1; j < series.columns.length; j++) {
table.columns.push({text: series.columns[j]});
}
}
if (series.values) {
for (i = 0; i < series.values.length; i++) {
var values = series.values[i];
if (series.tags) {
for (var key in series.tags) {
if (series.tags.hasOwnProperty(key)) {
values.splice(1, 0, series.tags[key]);
}
}
}
table.rows.push(values);
}
}
});
return table;
};
return InfluxSeries; return InfluxSeries;
}); });

View File

@ -55,12 +55,12 @@
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment> <metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li> </li>
</ul> </ul>
<div class="clearfix"></div>
<div style="padding: 10px" ng-if="target.rawQuery"> <div class="tight-form-flex-wrapper" ng-show="target.rawQuery">
<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea> <input type="text" class="tight-form-clear-input" ng-model="target.query" spellcheck="false" style="width: 100%;" ng-blur="get_data()"></input>
</div> </div>
<div class="clearfix"></div>
</div> </div>
<div ng-hide="target.rawQuery"> <div ng-hide="target.rawQuery">
@ -82,7 +82,7 @@
<div class="tight-form"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;"> <li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">GROUP BY</span> <span>GROUP BY</span>
</li> </li>
<li ng-repeat="part in queryModel.groupByParts"> <li ng-repeat="part in queryModel.groupByParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="get_data();" get-options="getPartOptions(part)"></influx-query-part-editor> <influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="get_data();" get-options="getPartOptions(part)"></influx-query-part-editor>
@ -103,6 +103,12 @@
<li> <li>
<input type="text" class="tight-form-clear-input input-xlarge" ng-model="target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="get_data()"> <input type="text" class="tight-form-clear-input input-xlarge" ng-model="target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="get_data()">
</li> </li>
<li class="tight-form-item">
Format as
</li>
<li>
<select class="input-small tight-form-input" style="width: 104px" ng-model="target.resultFormat" ng-options="f.value as f.text for f in resultFormats" ng-change="get_data()"></select>
</li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>

View File

@ -20,6 +20,10 @@ function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
$scope.queryModel = new InfluxQuery($scope.target); $scope.queryModel = new InfluxQuery($scope.target);
$scope.queryBuilder = new InfluxQueryBuilder($scope.target); $scope.queryBuilder = new InfluxQueryBuilder($scope.target);
$scope.groupBySegment = uiSegmentSrv.newPlusButton(); $scope.groupBySegment = uiSegmentSrv.newPlusButton();
$scope.resultFormats = [
{text: 'Time series', value: 'time_series'},
{text: 'Table', value: 'table'},
];
if (!$scope.target.measurement) { if (!$scope.target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement(); $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();

View File

@ -227,7 +227,7 @@ QueryPartDef.register({
type: 'derivative', type: 'derivative',
addStrategy: addTransformationStrategy, addStrategy: addTransformationStrategy,
category: categories.Transformations, category: categories.Transformations,
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}],
defaultParams: ['10s'], defaultParams: ['10s'],
renderer: functionRenderer, renderer: functionRenderer,
}); });
@ -236,7 +236,7 @@ QueryPartDef.register({
type: 'non_negative_derivative', type: 'non_negative_derivative',
addStrategy: addTransformationStrategy, addStrategy: addTransformationStrategy,
category: categories.Transformations, category: categories.Transformations,
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}],
defaultParams: ['10s'], defaultParams: ['10s'],
renderer: functionRenderer, renderer: functionRenderer,
}); });

View File

@ -186,5 +186,28 @@ describe('when generating timeseries from influxdb response', function() {
}); });
}); });
describe('given table response', function() {
var options = {
alias: '',
series: [
{
name: 'app.prod.server1.count',
tags: {},
columns: ['time', 'datacenter', 'value'],
values: [[1431946625000, 'America', 10], [1431946626000, 'EU', 12]]
}
]
};
it('should return table', function() {
var series = new InfluxSeries(options);
var table = series.getTable();
expect(table.type).to.be('table');
expect(table.columns.length).to.be(3);
expect(table.rows[0]).to.eql([1431946625000, 'America', 10]);
});
});
}); });

View File

@ -23,6 +23,7 @@ function (angular, _, moment, dateMath) {
this.url = datasource.url; this.url = datasource.url;
this.directUrl = datasource.directUrl; this.directUrl = datasource.directUrl;
this.basicAuth = datasource.basicAuth; this.basicAuth = datasource.basicAuth;
this.withCredentials = datasource.withCredentials;
this.lastErrors = {}; this.lastErrors = {};
} }
@ -32,8 +33,10 @@ function (angular, _, moment, dateMath) {
method: method method: method
}; };
if (this.basicAuth) { if (this.basicAuth || this.withCredentials) {
options.withCredentials = true; options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = { options.headers = {
"Authorization": this.basicAuth "Authorization": this.basicAuth
}; };

View File

@ -151,7 +151,7 @@ function (angular, _, $) {
html += '</div>'; html += '</div>';
html += '<div class="graph-legend-alias">'; html += '<div class="graph-legend-alias">';
html += '<a>' + series.label + '</a>'; html += '<a>' + _.escape(series.label) + '</a>';
html += '</div>'; html += '</div>';
if (panel.legend.values) { if (panel.legend.values) {

View File

@ -5,7 +5,7 @@ import _ = require('lodash');
import moment = require('moment'); import moment = require('moment');
import PanelMeta = require('app/features/panel/panel_meta'); import PanelMeta = require('app/features/panel/panel_meta');
import {TableModel} from './table_model'; import {transformDataToTable} from './transformers';
export class TablePanelCtrl { export class TablePanelCtrl {
@ -26,7 +26,7 @@ export class TablePanelCtrl {
var panelDefaults = { var panelDefaults = {
targets: [{}], targets: [{}],
transform: 'timeseries_to_rows', transform: 'timeseries_to_columns',
pageSize: null, pageSize: null,
showHeader: true, showHeader: true,
styles: [ styles: [
@ -104,7 +104,23 @@ export class TablePanelCtrl {
}; };
$scope.render = function() { $scope.render = function() {
$scope.table = TableModel.transform($scope.dataRaw, $scope.panel); // automatically correct transform mode
// based on data
if ($scope.dataRaw && $scope.dataRaw.length) {
if ($scope.dataRaw[0].type === 'table') {
$scope.panel.transform = 'table';
} else {
if ($scope.dataRaw[0].type === 'docs') {
$scope.panel.transform = 'json';
} else {
if ($scope.panel.transform === 'table' || $scope.panel.transform === 'json') {
$scope.panel.transform = 'timeseries_to_rows';
}
}
}
}
$scope.table = transformDataToTable($scope.dataRaw, $scope.panel);
$scope.table.sort($scope.panel.sort); $scope.table.sort($scope.panel.sort);
panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw); panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
}; };

View File

@ -132,6 +132,9 @@
<spectrum-picker ng-model="style.colors[1]" ng-change="render()" ></spectrum-picker> <spectrum-picker ng-model="style.colors[1]" ng-change="render()" ></spectrum-picker>
<spectrum-picker ng-model="style.colors[2]" ng-change="render()" ></spectrum-picker> <spectrum-picker ng-model="style.colors[2]" ng-change="render()" ></spectrum-picker>
</li> </li>
<li class="tight-form-item last">
<a class="pointer" ng-click="invertColorOrder($index)">invert order</a>
</li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>

View File

@ -44,11 +44,17 @@ export class TablePanelEditorCtrl {
}; };
$scope.addColumn = function() { $scope.addColumn = function() {
$scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.value}); var columns = transformers[$scope.panel.transform].getColumns($scope.dataRaw);
$scope.render(); var column = _.findWhere(columns, {text: $scope.addColumnSegment.value});
if (column) {
$scope.panel.columns.push(column);
$scope.render();
}
var plusButton = uiSegmentSrv.newPlusButton(); var plusButton = uiSegmentSrv.newPlusButton();
$scope.addColumnSegment.html = plusButton.html; $scope.addColumnSegment.html = plusButton.html;
$scope.addColumnSegment.value = plusButton.value;
}; };
$scope.transformChanged = function() { $scope.transformChanged = function() {
@ -93,6 +99,15 @@ export class TablePanelEditorCtrl {
return col.text; return col.text;
}); });
}; };
$scope.invertColorOrder = function(index) {
var ref = $scope.panel.styles[index].colors;
var copy = ref[0];
ref[0] = ref[2];
ref[2] = copy;
$scope.render();
};
} }
} }

View File

@ -112,7 +112,7 @@ export class TableRenderer {
// this hack adds header content to cell (not visible) // this hack adds header content to cell (not visible)
var widthHack = ''; var widthHack = '';
if (addWidthHack) { if (addWidthHack) {
widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].text + '<div>'; widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].text + '</div>';
} }
return '<td' + style + '>' + value + widthHack + '</td>'; return '<td' + style + '>' + value + widthHack + '</td>';

View File

@ -1,6 +1,6 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model'; import TableModel = require('app/core/table_model');
import {TableRenderer} from '../renderer'; import {TableRenderer} from '../renderer';
describe('when rendering table', () => { describe('when rendering table', () => {

Some files were not shown because too many files have changed in this diff Show More