mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
commit
201f50b121
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage/
|
||||
.aws-config.json
|
||||
awsconfig
|
||||
|
21
CHANGELOG.md
21
CHANGELOG.md
@ -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
|
||||
* **table**: New powerful and flexible table panel, closes [#215](https://github.com/grafana/grafana/issues/215)
|
||||
@ -6,9 +20,9 @@
|
||||
### Enhancements
|
||||
* **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 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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
* **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)
|
||||
@ -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)
|
||||
* **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)
|
||||
|
||||
**New Feature: Mix data sources**
|
||||
|
40
Godeps/Godeps.json
generated
40
Godeps/Godeps.json
generated
@ -20,53 +20,53 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/endpoints",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/ec2query",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/signer/v4",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/private/waiter",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatch",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/ec2",
|
||||
"Comment": "v0.10.4-18-gce51895",
|
||||
"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/davecgh/go-spew/spew",
|
||||
|
26
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go
generated
vendored
26
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go
generated
vendored
@ -13,11 +13,11 @@ var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
|
||||
|
||||
// rValuesAtPath returns a slice of values found in value v. The values
|
||||
// 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, "||")
|
||||
if len(pathparts) > 1 {
|
||||
for _, pathpart := range pathparts {
|
||||
vals := rValuesAtPath(v, pathpart, create, caseSensitive)
|
||||
vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
|
||||
if len(vals) > 0 {
|
||||
return vals
|
||||
}
|
||||
@ -76,7 +76,16 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
|
||||
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 = value.Elem()
|
||||
} 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 !create && value.IsNil() {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
@ -116,7 +125,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
|
||||
// pull out index
|
||||
i := int(*index)
|
||||
if i >= value.Len() { // check out of bounds
|
||||
if create {
|
||||
if createPath {
|
||||
// TODO resize slice
|
||||
} else {
|
||||
continue
|
||||
@ -127,7 +136,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
|
||||
value = reflect.Indirect(value.Index(i))
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !create && value.IsNil() {
|
||||
if !createPath && value.IsNil() {
|
||||
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
|
||||
// of a structure.
|
||||
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 {
|
||||
if rval.Kind() == reflect.Ptr && rval.IsNil() {
|
||||
continue
|
||||
}
|
||||
setValue(rval, v)
|
||||
}
|
||||
}
|
||||
|
34
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go
generated
vendored
34
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go
generated
vendored
@ -105,4 +105,38 @@ func TestSetValueAtPathSuccess(t *testing.T) {
|
||||
assert.Equal(t, "test0", s2.B.B.C)
|
||||
awsutil.SetValueAtPath(&s2, "A", []Struct{{}})
|
||||
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)
|
||||
}
|
||||
|
17
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go
generated
vendored
17
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go
generated
vendored
@ -41,11 +41,20 @@ func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, op
|
||||
Handlers: handlers,
|
||||
}
|
||||
|
||||
maxRetries := aws.IntValue(cfg.MaxRetries)
|
||||
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
|
||||
maxRetries = 3
|
||||
switch retryer, ok := cfg.Retryer.(request.Retryer); {
|
||||
case ok:
|
||||
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()
|
||||
|
||||
|
22
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go
generated
vendored
22
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go
generated
vendored
@ -12,6 +12,9 @@ import (
|
||||
// is nil also.
|
||||
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,
|
||||
// all clients will use the {defaults.DefaultConfig} structure.
|
||||
type Config struct {
|
||||
@ -59,6 +62,21 @@ type Config struct {
|
||||
// configuration.
|
||||
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
|
||||
// required fields and/or other semantic request input errors.
|
||||
DisableParamValidation *bool
|
||||
@ -217,6 +235,10 @@ func mergeInConfig(dst *Config, other *Config) {
|
||||
dst.MaxRetries = other.MaxRetries
|
||||
}
|
||||
|
||||
if other.Retryer != nil {
|
||||
dst.Retryer = other.Retryer
|
||||
}
|
||||
|
||||
if other.DisableParamValidation != nil {
|
||||
dst.DisableParamValidation = other.DisableParamValidation
|
||||
}
|
||||
|
14
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go
generated
vendored
14
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go
generated
vendored
@ -44,12 +44,19 @@ func (r *Request) nextPageTokens() []interface{} {
|
||||
}
|
||||
|
||||
tokens := []interface{}{}
|
||||
tokenAdded := false
|
||||
for _, outToken := range r.Operation.OutputTokens {
|
||||
v, _ := awsutil.ValuesAtPath(r.Data, outToken)
|
||||
if len(v) > 0 {
|
||||
tokens = append(tokens, v[0])
|
||||
tokenAdded = true
|
||||
} else {
|
||||
tokens = append(tokens, nil)
|
||||
}
|
||||
}
|
||||
if !tokenAdded {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
@ -85,9 +92,10 @@ func (r *Request) NextPage() *Request {
|
||||
// return true to keep iterating or false to stop.
|
||||
func (r *Request) EachPage(fn func(data interface{}, isLastPage bool) (shouldContinue bool)) error {
|
||||
for page := r; page != nil; page = page.NextPage() {
|
||||
page.Send()
|
||||
shouldContinue := fn(page.Data, !page.HasNextPage())
|
||||
if page.Error != nil || !shouldContinue {
|
||||
if err := page.Send(); err != nil {
|
||||
return err
|
||||
}
|
||||
if getNextPage := fn(page.Data, !page.HasNextPage()); !getNextPage {
|
||||
return page.Error
|
||||
}
|
||||
}
|
||||
|
63
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go
generated
vendored
63
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go
generated
vendored
@ -9,6 +9,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/awstesting/unit"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go/service/route53"
|
||||
"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.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
|
||||
|
8
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go
generated
vendored
8
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go
generated
vendored
@ -3,6 +3,7 @@ package request
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
)
|
||||
|
||||
@ -15,6 +16,13 @@ type Retryer interface {
|
||||
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
|
||||
// without any further action.
|
||||
var retryableCodes = map[string]struct{}{
|
||||
|
2
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go
generated
vendored
2
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go
generated
vendored
@ -5,4 +5,4 @@ package aws
|
||||
const SDKName = "aws-sdk-go"
|
||||
|
||||
// SDKVersion is the version of this SDK
|
||||
const SDKVersion = "0.10.4"
|
||||
const SDKVersion = "1.0.0"
|
||||
|
95
Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go
generated
vendored
95
Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go
generated
vendored
@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
@ -47,52 +48,74 @@ func (w *Waiter) Wait() error {
|
||||
res := method.Call([]reflect.Value{in})
|
||||
req := res[0].Interface().(*request.Request)
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("Waiter"))
|
||||
if err := req.Send(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := req.Send()
|
||||
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
|
||||
var vals []interface{}
|
||||
switch a.Matcher {
|
||||
case "pathAll":
|
||||
if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil {
|
||||
result = true
|
||||
for _, val := range vals {
|
||||
if !awsutil.DeepEqual(val, a.Expected) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
case "pathAll", "path":
|
||||
// Require all matches to be equal for result to match
|
||||
vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
|
||||
result = true
|
||||
for _, val := range vals {
|
||||
if !awsutil.DeepEqual(val, a.Expected) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
case "pathAny":
|
||||
if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil {
|
||||
for _, val := range vals {
|
||||
if awsutil.DeepEqual(val, a.Expected) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
// Only a single match needs to equal for the result to match
|
||||
vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
|
||||
for _, val := range vals {
|
||||
if awsutil.DeepEqual(val, a.Expected) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
case "status":
|
||||
s := a.Expected.(int)
|
||||
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 {
|
||||
switch a.State {
|
||||
case "success":
|
||||
return nil // waiter completed
|
||||
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
|
||||
if !result {
|
||||
// If there was no matching result found there is nothing more to do
|
||||
// for this response, retry the request.
|
||||
continue
|
||||
}
|
||||
|
||||
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))
|
||||
@ -101,3 +124,13 @@ func (w *Waiter) Wait() error {
|
||||
return awserr.New("ResourceNotReady",
|
||||
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...))
|
||||
}
|
||||
}
|
||||
|
252
Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go
generated
vendored
252
Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go
generated
vendored
@ -1,6 +1,9 @@
|
||||
package waiter_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -41,22 +44,7 @@ func (c *mockClient) MockRequest(input *MockInput) (*request.Request, *MockOutpu
|
||||
return req, output
|
||||
}
|
||||
|
||||
var mockAcceptors = []waiter.WaitAcceptor{
|
||||
{
|
||||
State: "success",
|
||||
Matcher: "pathAll",
|
||||
Argument: "States[].State",
|
||||
Expected: "running",
|
||||
},
|
||||
{
|
||||
State: "failure",
|
||||
Matcher: "pathAny",
|
||||
Argument: "States[].State",
|
||||
Expected: "stopping",
|
||||
},
|
||||
}
|
||||
|
||||
func TestWaiter(t *testing.T) {
|
||||
func TestWaiterPathAll(t *testing.T) {
|
||||
svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
|
||||
Region: aws.String("mock-region"),
|
||||
})}
|
||||
@ -73,13 +61,13 @@ func TestWaiter(t *testing.T) {
|
||||
{State: aws.String("pending")},
|
||||
},
|
||||
},
|
||||
{ // Request 1
|
||||
{ // Request 2
|
||||
States: []*MockState{
|
||||
{State: aws.String("running")},
|
||||
{State: aws.String("pending")},
|
||||
},
|
||||
},
|
||||
{ // Request 1
|
||||
{ // Request 3
|
||||
States: []*MockState{
|
||||
{State: aws.String("running")},
|
||||
{State: aws.String("running")},
|
||||
@ -104,7 +92,83 @@ func TestWaiter(t *testing.T) {
|
||||
Operation: "Mock",
|
||||
Delay: 0,
|
||||
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{
|
||||
Client: svc,
|
||||
@ -135,13 +199,13 @@ func TestWaiterFailure(t *testing.T) {
|
||||
{State: aws.String("pending")},
|
||||
},
|
||||
},
|
||||
{ // Request 1
|
||||
{ // Request 2
|
||||
States: []*MockState{
|
||||
{State: aws.String("running")},
|
||||
{State: aws.String("pending")},
|
||||
},
|
||||
},
|
||||
{ // Request 1
|
||||
{ // Request 3
|
||||
States: []*MockState{
|
||||
{State: aws.String("running")},
|
||||
{State: aws.String("stopping")},
|
||||
@ -166,7 +230,20 @@ func TestWaiterFailure(t *testing.T) {
|
||||
Operation: "Mock",
|
||||
Delay: 0,
|
||||
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{
|
||||
Client: svc,
|
||||
@ -181,3 +258,134 @@ func TestWaiterFailure(t *testing.T) {
|
||||
assert.Equal(t, 3, numBuiltReq)
|
||||
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)
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ Replace X.Y.Z by actual version number.
|
||||
cd $GOPATH/src/github.com/grafana/grafana
|
||||
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 go run build.go build
|
||||
go run build.go build
|
||||
```
|
||||
|
||||
### Building frontend assets
|
||||
|
@ -5,7 +5,7 @@ os: Windows Server 2012 R2
|
||||
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
|
||||
environment:
|
||||
nodejs_version: "0.12.2"
|
||||
nodejs_version: "4"
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
|
19
build.go
19
build.go
@ -76,6 +76,14 @@ func main() {
|
||||
grunt("release")
|
||||
createLinuxPackages()
|
||||
|
||||
case "pkg-rpm":
|
||||
grunt("release")
|
||||
createRpmPackages()
|
||||
|
||||
case "pkg-deb":
|
||||
grunt("release")
|
||||
createDebPackages()
|
||||
|
||||
case "latest":
|
||||
makeLatestDistCopies()
|
||||
|
||||
@ -147,7 +155,7 @@ type linuxPackageOptions struct {
|
||||
depends []string
|
||||
}
|
||||
|
||||
func createLinuxPackages() {
|
||||
func createDebPackages() {
|
||||
createPackage(linuxPackageOptions{
|
||||
packageType: "deb",
|
||||
homeDir: "/usr/share/grafana",
|
||||
@ -167,7 +175,9 @@ func createLinuxPackages() {
|
||||
|
||||
depends: []string{"adduser", "libfontconfig"},
|
||||
})
|
||||
}
|
||||
|
||||
func createRpmPackages() {
|
||||
createPackage(linuxPackageOptions{
|
||||
packageType: "rpm",
|
||||
homeDir: "/usr/share/grafana",
|
||||
@ -189,6 +199,11 @@ func createLinuxPackages() {
|
||||
})
|
||||
}
|
||||
|
||||
func createLinuxPackages() {
|
||||
createDebPackages()
|
||||
createRpmPackages()
|
||||
}
|
||||
|
||||
func createPackage(options linuxPackageOptions) {
|
||||
packageRoot, _ := ioutil.TempDir("", "grafana-linux-pack")
|
||||
|
||||
@ -315,6 +330,8 @@ func build(pkg string, tags []string) {
|
||||
args = append(args, "-o", binary)
|
||||
args = append(args, pkg)
|
||||
setBuildEnv()
|
||||
|
||||
runPrint("go", "version")
|
||||
runPrint("go", args...)
|
||||
|
||||
// Create an md5 checksum of the binary, to be included in the archive for
|
||||
|
@ -142,6 +142,9 @@ auto_assign_org_role = Viewer
|
||||
# Require email validation before sign up completes
|
||||
verify_email_enabled = false
|
||||
|
||||
# Background text for the user field on the login page
|
||||
login_hint = email or username
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
@ -245,6 +248,18 @@ daily_rotate = true
|
||||
# Expired days of log file(delete after max days), default is 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 ##########################
|
||||
[event_publisher]
|
||||
enabled = false
|
||||
|
@ -134,6 +134,9 @@
|
||||
# Default role new users will be automatically assigned (if disabled above is set to true)
|
||||
;auto_assign_org_role = Viewer
|
||||
|
||||
# Background text for the user field on the login page
|
||||
;login_hint = email or username
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
|
@ -1 +1 @@
|
||||
2.5.0
|
||||
2.6.0
|
||||
|
@ -45,6 +45,7 @@ pages:
|
||||
|
||||
- ['guides/basic_concepts.md', 'User Guides', 'Basic Concepts']
|
||||
- ['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-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"]
|
||||
@ -52,13 +53,14 @@ pages:
|
||||
|
||||
- ['reference/graph.md', 'Reference', 'Graph Panel']
|
||||
- ['reference/singlestat.md', 'Reference', 'Singlestat Panel']
|
||||
- ['reference/table_panel.md', 'Reference', 'Table Panel']
|
||||
- ['reference/dashlist.md', 'Reference', 'Dashboard List Panel']
|
||||
- ['reference/sharing.md', 'Reference', 'Sharing']
|
||||
- ['reference/annotations.md', 'Reference', 'Annotations']
|
||||
- ['reference/timerange.md', 'Reference', 'Time Range Controls']
|
||||
- ['reference/search.md', 'Reference', 'Dashboard Search']
|
||||
- ['reference/templating.md', 'Reference', 'Templated Dashboards']
|
||||
- ['reference/scripting.md', 'Reference', 'Scripted Dashboards']
|
||||
- ['reference/timerange.md', 'Reference', 'Time Range']
|
||||
- ['reference/search.md', 'Reference', 'Search']
|
||||
- ['reference/templating.md', 'Reference', 'Templating']
|
||||
- ['reference/scripting.md', 'Reference', 'Scripting']
|
||||
- ['reference/playlist.md', 'Reference', 'Playlist']
|
||||
- ['reference/export_import.md', 'Reference', 'Import & Export']
|
||||
- ['reference/admin.md', 'Reference', 'Administration']
|
||||
|
@ -63,15 +63,10 @@ Name | Description
|
||||
`namespaces()` | Returns a list of namespaces CloudWatch support.
|
||||
`metrics(namespace)` | Returns a list of metrics 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).
|
||||
|
||||
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,...)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Cost
|
||||
|
@ -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
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
TODO
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
||||
## InfluxDB 0.9.x
|
||||
## Query Editor
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
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
|
||||
will automatically adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`).
|
||||
|
||||
### Editor group by
|
||||
To group by a tag click the plus icon after the `GROUP BY ($interval)` text. Pick a tag from the dropdown that appears.
|
||||
You can remove the group by by clicking on the tag and then select `--remove group by--` from the dropdown.
|
||||
### Field & Aggregation functions
|
||||
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.
|
||||
|
||||
### Editor RAW Query
|
||||
You can switch to raw query mode by pressing the pen icon.
|
||||
The editor tries simplify and unify this part of the query. For example:
|
||||

|
||||
|
||||
> 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
|
||||
> 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
|
||||
- You can also use [[tag_hostname]] pattern replacement syntax
|
||||
|
||||
### Templating
|
||||
### Table query / raw data
|
||||
|
||||

|
||||
|
||||
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 then use this variable in your InfluxDB metric queries.
|
||||
|
||||
@ -93,7 +119,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/
|
||||
|
||||

|
||||
|
||||
### Annotations
|
||||
## Annotations
|
||||
Annotations allows you to overlay rich event information on top of graphs.
|
||||
|
||||
An example query:
|
||||
@ -102,10 +128,4 @@ An example query:
|
||||
SELECT title, description from events WHERE $timeFilter order asc
|
||||
```
|
||||
|
||||
### InfluxDB 0.8.x
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
124
docs/sources/guides/whats-new-in-v2-6.md
Normal file
124
docs/sources/guides/whats-new-in-v2-6.md
Normal 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:
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
|
@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
|
||||
|
||||
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
|
||||
|
||||
$ 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 dpkg -i grafana_2.5.0_amd64.deb
|
||||
$ sudo dpkg -i grafana_2.6.0_amd64.deb
|
||||
|
||||
## APT Repository
|
||||
|
||||
|
@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
|
||||
|
||||
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
|
||||
|
||||
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`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
$ 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:
|
||||
|
||||
$ 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
|
||||
|
||||
|
86
docs/sources/reference/table_panel.md
Normal file
86
docs/sources/reference/table_panel.md
Normal 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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
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
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
@ -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.1'>Version v2.1</a></li>
|
||||
<li><a class='version' href='/v2.0'>Version v2.0</a></li>
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "2.6.0-pre1",
|
||||
"version": "3.0.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/torkelo/grafana.git"
|
||||
|
@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
version=2.5.0
|
||||
version=2.6.0
|
||||
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
@ -119,7 +120,15 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
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 {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
@ -160,7 +169,15 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
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 {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
|
@ -15,31 +15,47 @@ func init() {
|
||||
metricsMap = map[string][]string{
|
||||
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
|
||||
"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/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
||||
"AWS/ECS": {"CPUUtilization", "MemoryUtilization"},
|
||||
"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",
|
||||
"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/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/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/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/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", "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"},
|
||||
"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/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
|
||||
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
|
||||
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
||||
"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/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
|
||||
"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
|
||||
"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
|
||||
"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "TotalLoad",
|
||||
"BackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup",
|
||||
"IsIdle", "ContainerAllocated", "ContainerReserved", "ContainerPending", "AppsCompleted", "AppsFailed", "AppsKilled", "AppsPending", "AppsRunning", "AppsSubmitted",
|
||||
"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "MRTotalNodes", "MRActiveNodes", "MRLostNodes", "MRUnhealthyNodes", "MRDecommissionedNodes", "MRRebootedNodes",
|
||||
"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB",
|
||||
"HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"},
|
||||
"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/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/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{
|
||||
"AWS/AutoScaling": {"AutoScalingGroupName"},
|
||||
@ -47,13 +63,15 @@ func init() {
|
||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||
"AWS/CloudSearch": {},
|
||||
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation"},
|
||||
"AWS/ECS": {"ClusterName", "ServiceName"},
|
||||
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
|
||||
"AWS/EBS": {"VolumeId"},
|
||||
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
|
||||
"AWS/ECS": {"ClusterName", "ServiceName"},
|
||||
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
|
||||
"AWS/ElasticMapReduce": {"ClusterId", "JobId"},
|
||||
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
|
||||
"AWS/ES": {},
|
||||
"AWS/Kinesis": {"StreamName"},
|
||||
"AWS/Lambda": {"FunctionName"},
|
||||
"AWS/ML": {"MLModelId", "RequestMode"},
|
||||
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
||||
"AWS/Redshift": {"NodeID", "ClusterIdentifier"},
|
||||
@ -62,8 +80,9 @@ func init() {
|
||||
"AWS/SNS": {"Application", "Platform", "TopicName"},
|
||||
"AWS/SQS": {"QueueName"},
|
||||
"AWS/S3": {"BucketName", "StorageType"},
|
||||
"AWS/SWF": {"Domain", "ActivityTypeName", "ActivityTypeVersion"},
|
||||
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||
"AWS/WAF": {"Rule", "WebACL"},
|
||||
"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)
|
||||
return
|
||||
}
|
||||
sort.Sort(sort.StringSlice(namespaceMetrics))
|
||||
|
||||
result := []interface{}{}
|
||||
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)
|
||||
return
|
||||
}
|
||||
sort.Sort(sort.StringSlice(dimensionValues))
|
||||
|
||||
result := []interface{}{}
|
||||
for _, name := range dimensionValues {
|
||||
|
@ -65,6 +65,7 @@ func GetDataSourceById(c *middleware.Context) Response {
|
||||
BasicAuth: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
BasicAuthPassword: ds.BasicAuthPassword,
|
||||
WithCredentials: ds.WithCredentials,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: ds.JsonData,
|
||||
})
|
||||
|
@ -61,6 +61,7 @@ type DataSource struct {
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
BasicAuthUser string `json:"basicAuthUser"`
|
||||
BasicAuthPassword string `json:"basicAuthPassword"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
}
|
||||
|
@ -69,6 +69,9 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
if ds.BasicAuth {
|
||||
dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
|
||||
}
|
||||
if ds.WithCredentials {
|
||||
dsMap["withCredentials"] = ds.WithCredentials
|
||||
}
|
||||
|
||||
if ds.Type == m.DS_INFLUXDB_08 {
|
||||
dsMap["username"] = ds.User
|
||||
|
@ -28,6 +28,7 @@ func LoginView(c *middleware.Context) {
|
||||
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
|
||||
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||
viewData.Settings["loginHint"] = setting.LoginHint
|
||||
|
||||
if !tryLoginUsingRememberCookie(c) {
|
||||
c.HTML(200, VIEW_INDEX, viewData)
|
||||
|
95
pkg/log/syslog.go
Normal file
95
pkg/log/syslog.go
Normal 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)
|
||||
}
|
@ -32,7 +32,12 @@ func Logger() macaron.Handler {
|
||||
rw := res.(macaron.ResponseWriter)
|
||||
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() {
|
||||
case 200, 304:
|
||||
|
@ -40,6 +40,7 @@ type DataSource struct {
|
||||
BasicAuth bool
|
||||
BasicAuthUser string
|
||||
BasicAuthPassword string
|
||||
WithCredentials bool
|
||||
IsDefault bool
|
||||
JsonData map[string]interface{}
|
||||
|
||||
@ -83,6 +84,7 @@ type AddDataSourceCommand struct {
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
BasicAuthUser string `json:"basicAuthUser"`
|
||||
BasicAuthPassword string `json:"basicAuthPassword"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
|
||||
@ -103,6 +105,7 @@ type UpdateDataSourceCommand struct {
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
BasicAuthUser string `json:"basicAuthUser"`
|
||||
BasicAuthPassword string `json:"basicAuthPassword"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
|
||||
|
@ -114,12 +114,14 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
|
||||
BasicAuth: cmd.BasicAuth,
|
||||
BasicAuthUser: cmd.BasicAuthUser,
|
||||
BasicAuthPassword: cmd.BasicAuthPassword,
|
||||
WithCredentials: cmd.WithCredentials,
|
||||
JsonData: cmd.JsonData,
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
sess.UseBool("is_default")
|
||||
sess.UseBool("basic_auth")
|
||||
sess.UseBool("with_credentials")
|
||||
|
||||
_, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds)
|
||||
if err != nil {
|
||||
|
@ -96,4 +96,9 @@ func addDataSourceMigration(mg *Migrator) {
|
||||
}))
|
||||
|
||||
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",
|
||||
}))
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func (col *Column) StringNoPk(d Dialect) string {
|
||||
}
|
||||
|
||||
if col.Default != "" {
|
||||
sql += "DEFAULT " + col.Default + " "
|
||||
sql += "DEFAULT " + d.Default(col) + " "
|
||||
}
|
||||
|
||||
return sql
|
||||
|
@ -17,10 +17,11 @@ type Dialect interface {
|
||||
SqlType(col *Column) string
|
||||
SupportEngine() bool
|
||||
LikeStr() string
|
||||
Default(col *Column) string
|
||||
|
||||
CreateIndexSql(tableName string, index *Index) 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
|
||||
DropTable(tableName string) string
|
||||
DropIndexSql(tableName string, index *Index) string
|
||||
@ -71,6 +72,10 @@ func (b *BaseDialect) EqStr() string {
|
||||
return "="
|
||||
}
|
||||
|
||||
func (b *BaseDialect) Default(col *Column) string {
|
||||
return col.Default
|
||||
}
|
||||
|
||||
func (b *BaseDialect) CreateTableSql(table *Table) string {
|
||||
var sql string
|
||||
sql = "CREATE TABLE IF NOT EXISTS "
|
||||
|
@ -64,6 +64,10 @@ type AddColumnMigration struct {
|
||||
column *Column
|
||||
}
|
||||
|
||||
func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration {
|
||||
return &AddColumnMigration{tableName: table.Name, column: col}
|
||||
}
|
||||
|
||||
func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
|
||||
m.tableName = tableName
|
||||
return m
|
||||
|
@ -36,6 +36,17 @@ func (db *Postgres) AutoIncrStr() string {
|
||||
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 {
|
||||
var res string
|
||||
switch t := c.Type; t {
|
||||
|
@ -82,6 +82,7 @@ var (
|
||||
AutoAssignOrg bool
|
||||
AutoAssignOrgRole string
|
||||
VerifyEmailEnabled bool
|
||||
LoginHint string
|
||||
|
||||
// Http auth
|
||||
AdminUser string
|
||||
@ -174,6 +175,9 @@ func applyEnvVariableOverrides() {
|
||||
|
||||
if len(envValue) > 0 {
|
||||
key.SetValue(envValue)
|
||||
if strings.Contains(envKey, "PASSWORD") {
|
||||
envValue = "*********"
|
||||
}
|
||||
appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, envValue))
|
||||
}
|
||||
}
|
||||
@ -188,6 +192,9 @@ func applyCommandLineDefaultProperties(props map[string]string) {
|
||||
value, exists := props[keyString]
|
||||
if exists {
|
||||
key.SetValue(value)
|
||||
if strings.Contains(keyString, "password") {
|
||||
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)
|
||||
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"})
|
||||
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
||||
LoginHint = users.Key("login_hint").String()
|
||||
|
||||
// anonymous access
|
||||
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
|
||||
@ -565,6 +573,14 @@ func initLogging(args *CommandLineArgs) {
|
||||
"driver": sec.Key("driver").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])
|
||||
|
@ -18,6 +18,7 @@ function (angular, coreModule, config) {
|
||||
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
||||
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||
$scope.loginHint = config.loginHint;
|
||||
|
||||
$scope.loginMode = true;
|
||||
$scope.submitBtnText = 'Log in';
|
||||
|
@ -55,8 +55,8 @@ function (_, $, coreModule) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.switchToLink = function() {
|
||||
if (linkMode) { return; }
|
||||
$scope.switchToLink = function(fromClick) {
|
||||
if (linkMode && !fromClick) { return; }
|
||||
|
||||
clearTimeout(cancelBlur);
|
||||
cancelBlur = null;
|
||||
@ -69,7 +69,7 @@ function (_, $, coreModule) {
|
||||
$scope.inputBlur = function() {
|
||||
// happens long before the click event on the typeahead options
|
||||
// need to have long delay because the blur
|
||||
cancelBlur = setTimeout($scope.switchToLink, 100);
|
||||
cancelBlur = setTimeout($scope.switchToLink, 200);
|
||||
};
|
||||
|
||||
$scope.source = function(query, callback) {
|
||||
@ -100,7 +100,7 @@ function (_, $, coreModule) {
|
||||
}
|
||||
|
||||
$input.val(value);
|
||||
$scope.switchToLink();
|
||||
$scope.switchToLink(true);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
@ -156,7 +156,7 @@ function (angular, _, coreModule) {
|
||||
vm.selectionsChanged = function(commitChange) {
|
||||
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') {
|
||||
vm.selectedValues[0].selected = false;
|
||||
vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length);
|
||||
|
@ -141,6 +141,9 @@ define([
|
||||
controller: 'PluginEditCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/global-alerts', {
|
||||
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
|
||||
})
|
||||
.otherwise({
|
||||
templateUrl: 'app/partials/error.html',
|
||||
controller: 'ErrorCtrl'
|
||||
|
39
public/app/core/table_model.ts
Normal file
39
public/app/core/table_model.ts
Normal 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;
|
@ -17,9 +17,9 @@ var spans = {
|
||||
|
||||
var rangeOptions = [
|
||||
{ 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', 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/y', to: 'now/y', display: 'This year', section: 2 },
|
||||
|
||||
|
@ -4,33 +4,35 @@
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
<h2>
|
||||
Organizations
|
||||
</h2>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr>
|
||||
<th style="text-align:left">Id</th>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td>{{org.id}}</td>
|
||||
<td>{{org.name}}</td>
|
||||
<td style="width: 1%">
|
||||
<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="deleteOrg(org)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td>{{org.id}}</td>
|
||||
<td>{{org.name}}</td>
|
||||
<td class="text-right">
|
||||
<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="deleteOrg(org)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,38 +5,42 @@
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
<h2>
|
||||
Users
|
||||
</h2>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr>
|
||||
<th style="text-align:left">Id</th>
|
||||
<th>Name</th>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th style="white-space: nowrap">Grafana Admin</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{user.id}}</td>
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.isAdmin}}</td>
|
||||
<td style="width: 1%">
|
||||
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th style="white-space: nowrap">Grafana Admin</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{user.id}}</td>
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.isAdmin}}</td>
|
||||
<td class="text-right">
|
||||
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
282
public/app/features/dashboard/partials/globalAlerts.html
Normal file
282
public/app/features/dashboard/partials/globalAlerts.html
Normal 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 <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>
|
@ -47,8 +47,9 @@ define([
|
||||
if (value.length === 15) {
|
||||
return moment.utc(value, 'YYYYMMDDTHHmmss');
|
||||
}
|
||||
var epoch = parseInt(value);
|
||||
if (!_.isNaN(epoch)) {
|
||||
|
||||
if (!isNaN(value)) {
|
||||
var epoch = parseInt(value);
|
||||
return moment(epoch);
|
||||
}
|
||||
|
||||
|
@ -16,14 +16,12 @@
|
||||
|
||||
<span ng-show="ctrl.dashboard.refresh" class="text-warning">
|
||||
|
||||
|
||||
<i class="fa fa-refresh"></i>
|
||||
Refresh every {{ctrl.dashboard.refresh}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="grafana-menu-refresh" ng-show="!ctrl.dashboard.refresh">
|
||||
<li class="grafana-menu-refresh">
|
||||
<a ng-click="ctrl.timeSrv.refreshDashboard()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</a>
|
||||
|
@ -13,7 +13,7 @@ function (angular, _, config) {
|
||||
|
||||
$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 = [
|
||||
{name: 'No pattern', value: undefined},
|
||||
@ -24,6 +24,11 @@ function (angular, _, config) {
|
||||
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
|
||||
];
|
||||
|
||||
$scope.esVersions = [
|
||||
{name: '1.x', value: 1},
|
||||
{name: '2.x', value: 2},
|
||||
];
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.isNew = true;
|
||||
$scope.datasources = [];
|
||||
|
@ -1,44 +1,55 @@
|
||||
<br>
|
||||
<h5>Http settings</h5>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
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 class="tight-form-item">
|
||||
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>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
<div class="tight-form-container">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
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 class="tight-form-item">
|
||||
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>
|
||||
</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 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>
|
||||
|
||||
|
||||
|
@ -5,47 +5,52 @@
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
<h2>Data sources</h2>
|
||||
|
||||
<div ng-if="datasources.length === 0">
|
||||
<em>No datasources defined</em>
|
||||
</div>
|
||||
|
||||
<table class="grafana-options-table" ng-if="datasources.length > 0">
|
||||
<tr>
|
||||
<td><strong>Name</strong></td>
|
||||
<td><strong>Url</strong></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr ng-repeat="ds in datasources">
|
||||
<td style="width:1%">
|
||||
<i class="fa fa-database"></i>
|
||||
{{ds.name}}
|
||||
</td>
|
||||
<td style="width:90%">
|
||||
{{ds.url}}
|
||||
</td>
|
||||
<td style="width:2%" class="text-center">
|
||||
<span ng-if="ds.isDefault">
|
||||
<span class="label label-info">default</span>
|
||||
</span>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="remove(ds)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<table class="filter-table" ng-if="datasources.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Name</strong></th>
|
||||
<th><strong>Url</strong></th>
|
||||
<th style="width: 60px;"></th>
|
||||
<th style="width: 65px;"></th>
|
||||
<th style="width: 34px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="ds in datasources">
|
||||
<td>
|
||||
<a href="datasources/edit/{{ds.id}}">
|
||||
<i class="fa fa-database"></i> {{ds.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="ellipsis">{{ds.url}}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span ng-if="ds.isDefault">
|
||||
<span class="label label-info">default</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a ng-click="remove(ds)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
@ -4,9 +4,8 @@
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
<h2>
|
||||
API Keys
|
||||
</h2>
|
||||
@ -32,21 +31,25 @@
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<table class="grafana-options-table" style="width: 250px">
|
||||
<tr>
|
||||
<th style="text-align: left">Name</th>
|
||||
<th style="text-align: left">Role</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="t in tokens">
|
||||
<td>{{t.name}}</td>
|
||||
<td>{{t.role}}</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<table class="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th style="width: 34px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="t in tokens">
|
||||
<td>{{t.name}}</td>
|
||||
<td>{{t.role}}</td>
|
||||
<td>
|
||||
<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
|
||||
<h2>Organization users</h2>
|
||||
|
||||
@ -18,21 +18,23 @@
|
||||
|
||||
<tabset>
|
||||
<tab heading="Users ({{users.length}})">
|
||||
<table class="grafana-options-table form-inline">
|
||||
<tr>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th style="width: 34px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td><span class="ellipsis">{{user.email}}</span></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>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<td>
|
||||
<a ng-click="removeUser(user)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
@ -41,36 +43,46 @@
|
||||
</table>
|
||||
</tab>
|
||||
<tab heading="Pending Invitations ({{pendingInvites.length}})">
|
||||
<div class="grafana-list-item" ng-repeat="invite in pendingInvites" ng-click="invite.expanded = !invite.expanded">
|
||||
{{invite.email}}
|
||||
<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span>
|
||||
<span class="pull-right">
|
||||
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
|
||||
<i class="fa fa-clipboard"></i> Copy Invite
|
||||
</button>
|
||||
|
||||
<a class="pointer">
|
||||
<i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
|
||||
<i ng-show="invite.expanded" class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
</span>
|
||||
<div ng-show="invite.expanded">
|
||||
<a href="{{invite.url}}">{{invite.url}}</a><br>
|
||||
<button class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-envelope-o"></i> Resend invite
|
||||
</button>
|
||||
|
||||
<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>
|
||||
<div>
|
||||
</div>
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="invite in pendingInvites">
|
||||
<tr ng-click="invite.expanded = !invite.expanded" ng-class="{'expanded': invite.expanded}">
|
||||
<td>{{invite.email}}</td>
|
||||
<td>{{invite.name}}</td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
|
||||
<i class="fa fa-clipboard"></i> Copy Invite
|
||||
</button>
|
||||
|
||||
<button class="btn btn-inverse btn-mini">
|
||||
Details
|
||||
<i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
|
||||
<i ng-show="invite.expanded" class="fa fa-caret-down"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="invite.expanded">
|
||||
<td colspan="3">
|
||||
<a href="{{invite.url}}">{{invite.url}}</a><br><br>
|
||||
|
||||
<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>
|
||||
</tabset>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -5,8 +5,8 @@
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
|
||||
<h2>Profile</h2>
|
||||
|
||||
@ -62,19 +62,28 @@
|
||||
|
||||
<h3>Organizations</h3>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td style="width: 98%"><strong>Name: </strong> {{org.name}}</td>
|
||||
<td><strong>Role: </strong> {{org.role}}</td>
|
||||
<td class="nobg max-width-btns">
|
||||
<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>
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td>{{org.name}}</td>
|
||||
<td>{{org.role}}</td>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
@ -129,7 +129,7 @@
|
||||
<editor-checkbox text="Include auto interval" model="current.auto" change="runQuery()"></editor-checkbox>
|
||||
</li>
|
||||
<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>
|
||||
<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>
|
||||
|
@ -79,6 +79,7 @@ function (angular, _) {
|
||||
this.highlightVariablesAsHtml = function(str) {
|
||||
if (!str || !_.isString(str)) { return str; }
|
||||
|
||||
str = _.escape(str);
|
||||
this._regex.lastIndex = 0;
|
||||
return str.replace(this._regex, function(match, g1, g2) {
|
||||
if (self._values[g1 || g2]) {
|
||||
|
220
public/app/panels/graph/alerting.html
Normal file
220
public/app/panels/graph/alerting.html
Normal 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>
|
@ -24,7 +24,7 @@
|
||||
<strong>User</strong>
|
||||
</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>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
|
@ -25,7 +25,7 @@ function (angular, _) {
|
||||
var end = convertToCloudWatchTime(options.range.to);
|
||||
|
||||
var queries = [];
|
||||
options = _.clone(options);
|
||||
options = angular.copy(options);
|
||||
_.each(options.targets, _.bind(function(target) {
|
||||
if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
|
||||
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 = {
|
||||
region: templateSrv.replace(region),
|
||||
action: 'ListMetrics',
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace),
|
||||
metricName: templateSrv.replace(metricName),
|
||||
dimensions: convertDimensionFormat(dimensions, {}),
|
||||
dimensions: convertDimensionFormat(filterDimensions, {}),
|
||||
}
|
||||
};
|
||||
|
||||
return this.awsRequest(request).then(function(result) {
|
||||
return _.chain(result.Metrics).map(function(metric) {
|
||||
return _.pluck(metric.Dimensions, 'Value');
|
||||
}).flatten().uniq().sortBy(function(name) {
|
||||
return name;
|
||||
}).map(function(value) {
|
||||
return _.chain(result.Metrics)
|
||||
.pluck('Dimensions')
|
||||
.flatten()
|
||||
.filter(function(dimension) {
|
||||
return dimension !== null && dimension.Name === dimensionKey;
|
||||
})
|
||||
.pluck('Value')
|
||||
.uniq()
|
||||
.sortBy()
|
||||
.map(function(value) {
|
||||
return {value: value, text: value};
|
||||
}).value();
|
||||
});
|
||||
@ -174,25 +179,14 @@ function (angular, _) {
|
||||
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) {
|
||||
region = templateSrv.replace(dimensionValuesQuery[1]);
|
||||
namespace = templateSrv.replace(dimensionValuesQuery[2]);
|
||||
metricName = templateSrv.replace(dimensionValuesQuery[3]);
|
||||
var dimensionPart = templateSrv.replace(dimensionValuesQuery[5]);
|
||||
var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]);
|
||||
|
||||
var dimensions = {};
|
||||
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);
|
||||
return this.getDimensionValues(region, namespace, metricName, dimensionKey, {});
|
||||
}
|
||||
|
||||
var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
|
||||
@ -222,7 +216,7 @@ function (angular, _) {
|
||||
var metricName = 'EstimatedCharges';
|
||||
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' };
|
||||
});
|
||||
};
|
||||
|
@ -76,7 +76,7 @@ function (angular, _) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getDimSegments = function(segment) {
|
||||
$scope.getDimSegments = function(segment, $index) {
|
||||
if (segment.type === 'operator') { return $q.when([]); }
|
||||
|
||||
var target = $scope.target;
|
||||
@ -85,7 +85,8 @@ function (angular, _) {
|
||||
if (segment.type === 'key' || segment.type === 'plus-button') {
|
||||
query = $scope.datasource.getDimensionKeys($scope.target.namespace);
|
||||
} 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) {
|
||||
|
@ -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.requestResponse = {
|
||||
Metrics: [
|
||||
|
@ -92,8 +92,10 @@ function (angular, _, queryDef) {
|
||||
}
|
||||
case 'date_histogram': {
|
||||
settings.interval = settings.interval || 'auto';
|
||||
settings.min_doc_count = settings.min_doc_count || 0;
|
||||
$scope.agg.field = $scope.target.timeField;
|
||||
settingsLinkText = 'Interval: ' + settings.interval;
|
||||
settingsLinkText += ', Min Doc Count: ' + settings.min_doc_count;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,13 +19,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
function ElasticDatasource(datasource) {
|
||||
this.type = 'elasticsearch';
|
||||
this.basicAuth = datasource.basicAuth;
|
||||
this.withCredentials = datasource.withCredentials;
|
||||
this.url = datasource.url;
|
||||
this.name = datasource.name;
|
||||
this.index = datasource.index;
|
||||
this.timeField = datasource.jsonData.timeField;
|
||||
this.esVersion = datasource.jsonData.esVersion;
|
||||
this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval);
|
||||
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
|
||||
};
|
||||
|
||||
if (this.basicAuth) {
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
options.withCredentials = true;
|
||||
}
|
||||
if (this.basicAuth) {
|
||||
options.headers = {
|
||||
"Authorization": this.basicAuth
|
||||
};
|
||||
@ -94,7 +99,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
|
||||
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 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++) {
|
||||
fieldValue = fieldValue[fieldNames[i]];
|
||||
if (!fieldValue) {
|
||||
console.log('could not find field in annotatation: ', fieldName);
|
||||
console.log('could not find field in annotation: ', fieldName);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -183,12 +188,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
sentTargets.push(target);
|
||||
}
|
||||
|
||||
if (sentTargets.length === 0) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
payload = payload.replace(/\$interval/g, options.interval);
|
||||
payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf());
|
||||
payload = payload.replace(/\$timeTo/g, options.range.to.valueOf());
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ function (angular) {
|
||||
|
||||
module.directive('elasticMetricAgg', function() {
|
||||
return {
|
||||
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metricAgg.html',
|
||||
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
|
||||
controller: 'ElasticMetricAggCtrl',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -30,13 +30,14 @@ function (angular) {
|
||||
index: "=",
|
||||
onChange: "&",
|
||||
getFields: "&",
|
||||
esVersion: '='
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('elasticBucketAgg', function() {
|
||||
return {
|
||||
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucketAgg.html',
|
||||
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
|
||||
controller: 'ElasticBucketAggCtrl',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
|
@ -15,6 +15,9 @@ function (_, queryDef) {
|
||||
|
||||
for (y = 0; y < target.metrics.length; y++) {
|
||||
metric = target.metrics[y];
|
||||
if (metric.hide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch(metric.type) {
|
||||
case 'count': {
|
||||
@ -76,8 +79,12 @@ function (_, queryDef) {
|
||||
newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props};
|
||||
for (i = 0; i < esAgg.buckets.length; 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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -11,16 +11,23 @@ function (angular, _, queryDef) {
|
||||
module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
|
||||
var metricAggs = $scope.target.metrics;
|
||||
|
||||
$scope.metricAggTypes = queryDef.metricAggTypes;
|
||||
$scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion);
|
||||
$scope.extendedStats = queryDef.extendedStats;
|
||||
$scope.pipelineAggOptions = [];
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.agg = metricAggs[$scope.index];
|
||||
$scope.validateModel();
|
||||
$scope.updatePipelineAggOptions();
|
||||
};
|
||||
|
||||
$scope.updatePipelineAggOptions = function() {
|
||||
$scope.pipelineAggOptions = queryDef.getPipelineAggOptions($scope.target);
|
||||
};
|
||||
|
||||
$rootScope.onAppEvent('elastic-query-updated', function() {
|
||||
$scope.index = _.indexOf(metricAggs, $scope.agg);
|
||||
$scope.updatePipelineAggOptions();
|
||||
$scope.validateModel();
|
||||
}, $scope);
|
||||
|
||||
@ -30,17 +37,33 @@ function (angular, _, queryDef) {
|
||||
$scope.settingsLinkText = '';
|
||||
$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';
|
||||
}
|
||||
|
||||
switch($scope.agg.type) {
|
||||
case 'percentiles': {
|
||||
$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;
|
||||
}
|
||||
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) {
|
||||
if (val) {
|
||||
var def = _.findWhere($scope.extendedStats, {value: key});
|
||||
@ -48,29 +71,47 @@ function (angular, _, queryDef) {
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
|
||||
|
||||
if (stats.length === 0) {
|
||||
$scope.agg.meta.std_deviation_bounds_lower = true;
|
||||
$scope.agg.meta.std_deviation_bounds_upper = true;
|
||||
}
|
||||
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
|
||||
break;
|
||||
}
|
||||
case 'raw_document': {
|
||||
$scope.target.metrics = [$scope.agg];
|
||||
$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.showOptions = !$scope.showOptions;
|
||||
$scope.updatePipelineAggOptions();
|
||||
};
|
||||
|
||||
$scope.onChangeInternal = function() {
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.onTypeChange = function() {
|
||||
$scope.agg.settings = {};
|
||||
$scope.agg.meta = {};
|
||||
$scope.showOptions = false;
|
||||
$scope.updatePipelineAggOptions();
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
@ -94,6 +135,14 @@ function (angular, _, queryDef) {
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.toggleShowMetric = function() {
|
||||
$scope.agg.hide = !$scope.agg.hide;
|
||||
if (!$scope.agg.hide) {
|
||||
delete $scope.agg.hide;
|
||||
}
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
|
@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" class="span6" ng-model='currentAnnotation.query' placeholder="tags:deploy"></input>
|
||||
</div>
|
||||
|
@ -35,9 +35,9 @@
|
||||
|
||||
<div class="tight-form" ng-if="showOptions">
|
||||
<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">
|
||||
<li class="tight-form-item" style="width: 60px">
|
||||
<li class="tight-form-item" style="width: 94px">
|
||||
Interval
|
||||
</li>
|
||||
<li>
|
||||
@ -46,6 +46,17 @@
|
||||
</ul>
|
||||
<div class="clearfix"></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 class="tight-form-inner-box" ng-if="agg.type === 'terms'">
|
||||
<div class="tight-form">
|
@ -20,7 +20,7 @@
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 144px">
|
||||
Time field name
|
||||
@ -31,3 +31,14 @@
|
||||
</ul>
|
||||
<div class="clearfix"></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>
|
||||
|
@ -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>
|
@ -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
|
||||
|
||||
<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>
|
@ -14,7 +14,6 @@
|
||||
<i class="fa fa-bars"></i>
|
||||
</a>
|
||||
<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="moveDataQuery($index, $index-1)">Move up</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
|
||||
target="target" index="$index"
|
||||
get-fields="getFields($fieldType)"
|
||||
on-change="queryUpdated()">
|
||||
on-change="queryUpdated()"
|
||||
es-version="esVersion">
|
||||
</elastic-metric-agg>
|
||||
</div>
|
||||
|
||||
|
@ -1,16 +1,22 @@
|
||||
define([
|
||||
"angular"
|
||||
'./query_def'
|
||||
],
|
||||
function (angular) {
|
||||
function (queryDef) {
|
||||
'use strict';
|
||||
|
||||
function ElasticQueryBuilder(options) {
|
||||
this.timeField = options.timeField;
|
||||
this.esVersion = options.esVersion;
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.getRangeFilter = function() {
|
||||
var filter = {};
|
||||
filter[this.timeField] = {"gte": "$timeFrom", "lte": "$timeTo"};
|
||||
|
||||
if (this.esVersion >= 2) {
|
||||
filter[this.timeField]["format"] = "epoch_millis";
|
||||
}
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
@ -45,12 +51,23 @@ function (angular) {
|
||||
return queryNode;
|
||||
};
|
||||
|
||||
ElasticQueryBuilder.prototype.getInterval = function(agg) {
|
||||
if (agg.settings && agg.settings.interval !== 'auto') {
|
||||
return agg.settings.interval;
|
||||
} else {
|
||||
return '$interval';
|
||||
ElasticQueryBuilder.prototype.getDateHistogramAgg = function(aggDef) {
|
||||
var esAgg = {};
|
||||
var settings = aggDef.settings || {};
|
||||
esAgg.interval = settings.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) {
|
||||
@ -82,9 +99,11 @@ function (angular) {
|
||||
};
|
||||
|
||||
ElasticQueryBuilder.prototype.build = function(target) {
|
||||
if (target.rawQuery) {
|
||||
return angular.fromJson(target.rawQuery);
|
||||
}
|
||||
// make sure query has defaults;
|
||||
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 query = {
|
||||
@ -123,12 +142,7 @@ function (angular) {
|
||||
|
||||
switch(aggDef.type) {
|
||||
case 'date_histogram': {
|
||||
esAgg["date_histogram"] = {
|
||||
"interval": this.getInterval(aggDef),
|
||||
"field": this.timeField,
|
||||
"min_doc_count": 0,
|
||||
"extended_bounds": { "min": "$timeFrom", "max": "$timeTo" }
|
||||
};
|
||||
esAgg["date_histogram"] = this.getDateHistogramAgg(aggDef);
|
||||
break;
|
||||
}
|
||||
case 'filters': {
|
||||
@ -154,14 +168,25 @@ function (angular) {
|
||||
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) {
|
||||
if (metric.settings.hasOwnProperty(prop) && metric.settings[prop] !== null) {
|
||||
metricAgg[prop] = metric.settings[prop];
|
||||
}
|
||||
}
|
||||
|
||||
var aggField = {};
|
||||
aggField[metric.type] = metricAgg;
|
||||
nestedAggs.aggs[metric.id] = aggField;
|
||||
}
|
||||
@ -204,5 +229,4 @@ function (angular) {
|
||||
};
|
||||
|
||||
return ElasticQueryBuilder;
|
||||
|
||||
});
|
||||
|
@ -7,14 +7,13 @@ function (angular) {
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) {
|
||||
$scope.esVersion = $scope.datasource.esVersion;
|
||||
|
||||
$scope.init = function() {
|
||||
var target = $scope.target;
|
||||
if (!target) { return; }
|
||||
|
||||
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
|
||||
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
|
||||
target.timeField = $scope.datasource.timeField;
|
||||
$scope.queryUpdated();
|
||||
};
|
||||
|
||||
$scope.getFields = function(type) {
|
||||
@ -39,14 +38,6 @@ function (angular) {
|
||||
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();
|
||||
|
||||
});
|
||||
|
@ -7,13 +7,15 @@ function (_) {
|
||||
return {
|
||||
metricAggTypes: [
|
||||
{text: "Count", value: 'count', requiresField: false},
|
||||
{text: "Average", value: 'avg', requiresField: true},
|
||||
{text: "Sum", value: 'sum', requiresField: true},
|
||||
{text: "Max", value: 'max', requiresField: true},
|
||||
{text: "Min", value: 'min', requiresField: true},
|
||||
{text: "Extended Stats", value: 'extended_stats', requiresField: true},
|
||||
{text: "Percentiles", value: 'percentiles', requiresField: true},
|
||||
{text: "Unique Count", value: "cardinality", requiresField: true},
|
||||
{text: "Average", value: 'avg', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Sum", value: 'sum', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Max", value: 'max', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Min", value: 'min', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Extended Stats", value: 'extended_stats', requiresField: true, supportsMissing: true, supportsInlineScript: true},
|
||||
{text: "Percentiles", value: 'percentiles', requiresField: true, supportsMissing: true, supportsInlineScript: 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}
|
||||
],
|
||||
|
||||
@ -66,6 +68,53 @@ function (_) {
|
||||
{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) {
|
||||
var self = this;
|
||||
var metricRefs = [];
|
||||
|
@ -22,14 +22,6 @@ describe('ElasticQueryBuilder', function() {
|
||||
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() {
|
||||
var query = builder.build({
|
||||
metrics: [{type: 'count', id: '1'}],
|
||||
@ -44,6 +36,39 @@ describe('ElasticQueryBuilder', function() {
|
||||
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() {
|
||||
var query = builder.build({
|
||||
metrics: [{type: 'avg', field: '@value', id: '1'}],
|
||||
@ -130,4 +155,89 @@ describe('ElasticQueryBuilder', function() {
|
||||
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");
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -53,6 +53,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
|
||||
|
||||
// replace templated variables
|
||||
allQueries = templateSrv.replace(allQueries, options.scopedVars);
|
||||
|
||||
return this._seriesQuery(allQueries).then(function(data) {
|
||||
if (!data || !data.results) {
|
||||
return [];
|
||||
@ -63,13 +64,26 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
|
||||
var result = data.results[i];
|
||||
if (!result || !result.series) { continue; }
|
||||
|
||||
var alias = (queryTargets[i] || {}).alias;
|
||||
var target = queryTargets[i];
|
||||
var alias = target.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++) {
|
||||
seriesList.push(targetSeries[y]);
|
||||
|
||||
var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ class InfluxQuery {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
|
||||
target.dsType = 'influxdb';
|
||||
target.resultFormat = target.resultFormat || 'time_series';
|
||||
target.tags = target.tags || [];
|
||||
target.groupBy = target.groupBy || [
|
||||
{type: 'time', params: ['$interval']},
|
||||
|
@ -1,7 +1,8 @@
|
||||
define([
|
||||
'lodash',
|
||||
'app/core/table_model',
|
||||
],
|
||||
function (_) {
|
||||
function (_, TableModel) {
|
||||
'use strict';
|
||||
|
||||
function InfluxSeries(options) {
|
||||
@ -108,5 +109,44 @@ function (_) {
|
||||
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;
|
||||
});
|
||||
|
@ -55,12 +55,12 @@
|
||||
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="padding: 10px" ng-if="target.rawQuery">
|
||||
<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
|
||||
<div class="tight-form-flex-wrapper" ng-show="target.rawQuery">
|
||||
<input type="text" class="tight-form-clear-input" ng-model="target.query" spellcheck="false" style="width: 100%;" ng-blur="get_data()"></input>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="target.rawQuery">
|
||||
@ -82,7 +82,7 @@
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<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 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>
|
||||
@ -103,6 +103,12 @@
|
||||
<li>
|
||||
<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 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>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -20,6 +20,10 @@ function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
|
||||
$scope.queryModel = new InfluxQuery($scope.target);
|
||||
$scope.queryBuilder = new InfluxQueryBuilder($scope.target);
|
||||
$scope.groupBySegment = uiSegmentSrv.newPlusButton();
|
||||
$scope.resultFormats = [
|
||||
{text: 'Time series', value: 'time_series'},
|
||||
{text: 'Table', value: 'table'},
|
||||
];
|
||||
|
||||
if (!$scope.target.measurement) {
|
||||
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
|
||||
|
@ -227,7 +227,7 @@ QueryPartDef.register({
|
||||
type: 'derivative',
|
||||
addStrategy: addTransformationStrategy,
|
||||
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'],
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
@ -236,7 +236,7 @@ QueryPartDef.register({
|
||||
type: 'non_negative_derivative',
|
||||
addStrategy: addTransformationStrategy,
|
||||
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'],
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
@ -23,6 +23,7 @@ function (angular, _, moment, dateMath) {
|
||||
this.url = datasource.url;
|
||||
this.directUrl = datasource.directUrl;
|
||||
this.basicAuth = datasource.basicAuth;
|
||||
this.withCredentials = datasource.withCredentials;
|
||||
this.lastErrors = {};
|
||||
}
|
||||
|
||||
@ -32,8 +33,10 @@ function (angular, _, moment, dateMath) {
|
||||
method: method
|
||||
};
|
||||
|
||||
if (this.basicAuth) {
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
options.withCredentials = true;
|
||||
}
|
||||
if (this.basicAuth) {
|
||||
options.headers = {
|
||||
"Authorization": this.basicAuth
|
||||
};
|
||||
|
@ -151,7 +151,7 @@ function (angular, _, $) {
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="graph-legend-alias">';
|
||||
html += '<a>' + series.label + '</a>';
|
||||
html += '<a>' + _.escape(series.label) + '</a>';
|
||||
html += '</div>';
|
||||
|
||||
if (panel.legend.values) {
|
||||
|
@ -5,7 +5,7 @@ import _ = require('lodash');
|
||||
import moment = require('moment');
|
||||
import PanelMeta = require('app/features/panel/panel_meta');
|
||||
|
||||
import {TableModel} from './table_model';
|
||||
import {transformDataToTable} from './transformers';
|
||||
|
||||
export class TablePanelCtrl {
|
||||
|
||||
@ -26,7 +26,7 @@ export class TablePanelCtrl {
|
||||
|
||||
var panelDefaults = {
|
||||
targets: [{}],
|
||||
transform: 'timeseries_to_rows',
|
||||
transform: 'timeseries_to_columns',
|
||||
pageSize: null,
|
||||
showHeader: true,
|
||||
styles: [
|
||||
@ -104,7 +104,23 @@ export class TablePanelCtrl {
|
||||
};
|
||||
|
||||
$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);
|
||||
panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
|
||||
};
|
||||
|
@ -132,6 +132,9 @@
|
||||
<spectrum-picker ng-model="style.colors[1]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="style.colors[2]" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<a class="pointer" ng-click="invertColorOrder($index)">invert order</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -44,11 +44,17 @@ export class TablePanelEditorCtrl {
|
||||
};
|
||||
|
||||
$scope.addColumn = function() {
|
||||
$scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.value});
|
||||
$scope.render();
|
||||
var columns = transformers[$scope.panel.transform].getColumns($scope.dataRaw);
|
||||
var column = _.findWhere(columns, {text: $scope.addColumnSegment.value});
|
||||
|
||||
if (column) {
|
||||
$scope.panel.columns.push(column);
|
||||
$scope.render();
|
||||
}
|
||||
|
||||
var plusButton = uiSegmentSrv.newPlusButton();
|
||||
$scope.addColumnSegment.html = plusButton.html;
|
||||
$scope.addColumnSegment.value = plusButton.value;
|
||||
};
|
||||
|
||||
$scope.transformChanged = function() {
|
||||
@ -93,6 +99,15 @@ export class TablePanelEditorCtrl {
|
||||
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();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ export class TableRenderer {
|
||||
// this hack adds header content to cell (not visible)
|
||||
var widthHack = '';
|
||||
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>';
|
||||
|
@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
describe('when rendering table', () => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user