mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
Merged with master, resolved conflicts
This commit is contained in:
commit
c433167950
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,7 +1,28 @@
|
||||
# 2.6.0 (2015-12-04)
|
||||
# 3.0.0 (unrelased master branch)
|
||||
|
||||
|
||||
### Breaking changes
|
||||
**InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
|
||||
**KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
|
||||
|
||||
# 2.6.1 (unrelased, 2.6.x branch)
|
||||
|
||||
### New Features
|
||||
* **Elasticsearch**: Support for derivative unit option, closes [#3512](https://github.com/grafana/grafana/issues/3512)
|
||||
|
||||
# 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)
|
||||
|
||||
|
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)
|
||||
}
|
||||
|
17
build.go
17
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")
|
||||
|
||||
|
@ -15,6 +15,10 @@ data = data
|
||||
# Directory where grafana can store logs
|
||||
#
|
||||
logs = data/log
|
||||
#
|
||||
# Directory where grafana will automatically scan and look for plugins
|
||||
#
|
||||
plugins = data/plugins
|
||||
|
||||
#################################### Server ####################################
|
||||
[server]
|
||||
@ -125,6 +129,12 @@ disable_gravatar = false
|
||||
# data source proxy whitelist (ip_or_domain:port seperated by spaces)
|
||||
data_source_proxy_whitelist =
|
||||
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
external_enabled = true
|
||||
external_snapshot_url = https://snapshots-origin.raintank.io
|
||||
external_snapshot_name = Publish to snapshot.raintank.io
|
||||
|
||||
#################################### Users ####################################
|
||||
[users]
|
||||
# disable user signup / registration
|
||||
@ -142,6 +152,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 +258,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
|
||||
|
@ -15,7 +15,12 @@
|
||||
# Directory where grafana can store logs
|
||||
#
|
||||
;logs = /var/log/grafana
|
||||
#
|
||||
# Directory where grafana will automatically scan and look for plugins
|
||||
#
|
||||
;plugins = /var/lib/grafana/plugins
|
||||
|
||||
#
|
||||
#################################### Server ####################################
|
||||
[server]
|
||||
# Protocol (http or https)
|
||||
@ -120,6 +125,12 @@
|
||||
# data source proxy whitelist (ip_or_domain:port seperated by spaces)
|
||||
;data_source_proxy_whitelist =
|
||||
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
;external_enabled = true
|
||||
;external_snapshot_url = https://snapshots-origin.raintank.io
|
||||
;external_snapshot_name = Publish to snapshot.raintank.io
|
||||
|
||||
#################################### Users ####################################
|
||||
[users]
|
||||
# disable user signup / registration
|
||||
@ -134,6 +145,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']
|
||||
|
@ -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.
|
||||
|
||||
![](/img/elasticsearch/pipeline_metrics_editor.png)
|
||||
|
||||
## Templating
|
||||
|
||||
The Elasticsearch datasource supports two types of queries you can use to fill template variables with values.
|
||||
|
||||
### Possible values for a field
|
||||
|
||||
```json
|
||||
{"find": "terms", "field": "@hostname"}
|
||||
```
|
||||
|
||||
### Fields filtered by type
|
||||
```json
|
||||
{"find": "fields", "type": "string"}
|
||||
```
|
||||
|
||||
### Multi format / All format
|
||||
Use lucene format.
|
||||
|
||||
|
||||
|
||||
## Annotations
|
||||
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
|
||||
|
||||
![](/img/influxdb/InfluxDB_09_editor.png)
|
||||
![](/img/influxdb/editor_v3.png)
|
||||
|
||||
You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
|
||||
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:
|
||||
![](/img/influxdb/select_editor.png)
|
||||
|
||||
> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` clause and ends with `order by asc`.
|
||||
The above will generate the following InfluxDB `SELECT` clause:
|
||||
|
||||
```sql
|
||||
SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM ....
|
||||
```
|
||||
|
||||
#### Select multiple fields
|
||||
Use the plus button and select Field > field to add another SELECT clause. You can also
|
||||
specify an asterix `*` to select all fields.
|
||||
|
||||
### Group By
|
||||
To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears.
|
||||
You can remove the group by by clicking on the `tag` and then click on the x icon.
|
||||
|
||||
### Text Editor Mode (RAW)
|
||||
You can switch to raw query mode by clicking hamburger icon and then `Switch editor mode`.
|
||||
|
||||
> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter`
|
||||
> Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands
|
||||
> 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
|
||||
|
||||
![](/img/influxdb/raw_data.png)
|
||||
|
||||
You can remove the group by time by clicking on the `time` part and then the `x` icon. You can
|
||||
change the option `Format As` to `Table` if you want to show raw data in the `Table` panel.
|
||||
|
||||
|
||||
## Templating
|
||||
You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query.
|
||||
You can then use this variable in your InfluxDB metric queries.
|
||||
|
||||
@ -93,7 +119,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/
|
||||
|
||||
![](/img/influxdb/templating_simple_ex1.png)
|
||||
|
||||
### 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
|
||||
|
||||
![](/img/v1/influxdb_editor.png)
|
||||
|
||||
|
||||
|
||||
|
||||
|
123
docs/sources/guides/whats-new-in-v2-6.md
Normal file
123
docs/sources/guides/whats-new-in-v2-6.md
Normal file
@ -0,0 +1,123 @@
|
||||
---
|
||||
page_title: What's New in Grafana v2.6
|
||||
page_description: What's new in Grafana v2.6
|
||||
page_keywords: grafana, new, changes, features, documentation, table
|
||||
---
|
||||
|
||||
# What's new in Grafana v2.6
|
||||
|
||||
## Release highlights
|
||||
The release includes a new Table panel, a new InfluxDB query editor, support for Elasticsearch Pipeline Metrics and
|
||||
support for multiple Cloudwatch credentials.
|
||||
|
||||
## Table Panel
|
||||
<img src="/img/v2/table-panel.png">
|
||||
|
||||
The new table panel is very flexible, supporting both multiple modes for time series as well as for
|
||||
table, annotation and raw JSON data. It also provides date formating and value formating and coloring options.
|
||||
|
||||
### Time series to rows
|
||||
|
||||
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column.
|
||||
Where `Metric` is the name of the time series.
|
||||
|
||||
<img src="/img/v2.6/table_ts_to_rows.png">
|
||||
|
||||
### Table Transform
|
||||
Above you see the options tab for the **Table Panel**. The most important option is the `To Table Transform`.
|
||||
This option controls how the result of the metric/data query is turned into a table.
|
||||
|
||||
### Column Styles
|
||||
The column styles allow you control how dates and numbers are formatted.
|
||||
|
||||
### Time series to columns
|
||||
This transform allows you to take multiple time series and group them by time. Which will result in a `Time` column
|
||||
and a column for each time series.
|
||||
|
||||
<img src="/img/v2.6/table_ts_to_columns.png">
|
||||
|
||||
In the screenshot above you can see how the same time series query as in the previous example can be transformed into
|
||||
a different table by changing the `To Table Transform` to `Time series to columns`.
|
||||
|
||||
### Time series to aggregations
|
||||
This transform works very similar to the legend values in the Graph panel. Each series gets its own row. In the Options
|
||||
tab you can select which aggregations you want using the plus button the Columns section.
|
||||
|
||||
<img src="/img/v2.6/table_ts_to_aggregations.png">
|
||||
|
||||
You have to think about how accurate the aggregations will be. It depends on what aggregation is used in the time series query,
|
||||
how many data points are fetched, etc. The time series aggregations are calculated by Grafana after aggregation is performed
|
||||
by the time series database.
|
||||
|
||||
### Raw logs queries
|
||||
|
||||
If you want to show documents from Elasticsearch pick `Raw Document` as the first metric.
|
||||
|
||||
<img src="/img/v2.6/elastic_raw_doc.png">
|
||||
|
||||
This in combination with the `JSON Data` table transform will allow you to pick which fields in the document
|
||||
you want to show in the table.
|
||||
|
||||
<img src="/img/v2.6/table_json_data.png">
|
||||
|
||||
### Elasticsearch aggregations
|
||||
|
||||
You can also make Elasticsearch aggregation queries without a `Date Histogram`. This allows you to
|
||||
use Elasticsearch metric aggregations to get accurate aggregations for the selected time range.
|
||||
|
||||
<img src="/img/v2.6/elastic_aggregations.png">
|
||||
|
||||
### Annotations
|
||||
|
||||
The table can also show any annotations you have enabled in the dashboard.
|
||||
|
||||
<img src="/img/v2.6/table_annotations.png">
|
||||
|
||||
## The New InfluxDB Editor
|
||||
The new InfluxDB editor is a lot more flexible and powerful. It supports nested functions, like `derivative`.
|
||||
It also uses the same technique as the Graphite query editor in that it presents nested functions as chain of function
|
||||
transformations. It tries to simplify and unify the complicated nature of InfluxDB's query language.
|
||||
|
||||
<img src="/img/v2.6/influxdb_editor_v3.gif">
|
||||
|
||||
In the `SELECT` row you can specify what fields and functions you want to use. If you have a
|
||||
group by time you need an aggregation function. Some functions like derivative require an aggregation function.
|
||||
|
||||
The editor tries simplify and unify this part of the query. For example:
|
||||
![](/img/influxdb/select_editor.png)
|
||||
|
||||
The above will generate the following InfluxDB `SELECT` clause:
|
||||
|
||||
```sql
|
||||
SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM ....
|
||||
```
|
||||
|
||||
### Select multiple fields
|
||||
Use the plus button and select Field > field to add another SELECT clause. You can also
|
||||
specify an asterix `*` to select all fields.
|
||||
|
||||
### Group By
|
||||
To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears.
|
||||
You can remove the group by by clicking on the `tag` and then click on the x icon.
|
||||
|
||||
The new editor also allows you to remove group by time and select `raw` table data. Which is very useful
|
||||
in combination with the new Table panel to show raw log data stored in InfluxDB.
|
||||
|
||||
<img src="/img/v2.6/table_influxdb_logs.png">
|
||||
|
||||
## Pipeline metrics
|
||||
|
||||
If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like
|
||||
**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric
|
||||
to hide metrics from appearing in the graph.
|
||||
|
||||
![](/img/elasticsearch/pipeline_metrics_editor.png)
|
||||
|
||||
## Changelog
|
||||
For a detailed list and link to github issues for everything included in the 2.6 release please
|
||||
view the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file.
|
||||
|
||||
- - -
|
||||
|
||||
<a href="http://grafana.org/download">Download Grafana 2.6 now</a>
|
||||
|
@ -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 formatting and value formatting 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 formatting and display options.
|
||||
|
||||
## Data to Table
|
||||
|
||||
<img class="no-shadow" src="/img/v2/table-data-options.png">
|
||||
|
||||
The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric
|
||||
query should be transformed into a table format. The **Columns (2)** option allows you to select what columns
|
||||
you want in the table. Only applicable for some transforms.
|
||||
|
||||
### Time series to rows
|
||||
|
||||
<img src="/img/v2/table_ts_to_rows.png">
|
||||
|
||||
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
|
||||
|
||||
### Time series to columns
|
||||
|
||||
![](/img/v2/table_ts_to_columns.png)
|
||||
|
||||
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
|
||||
|
||||
### Time series aggregations
|
||||
|
||||
![](/img/v2/table_ts_to_aggregations.png)
|
||||
This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
|
||||
|
||||
### Annotations
|
||||
![](/img/v2/table_annotations.png)
|
||||
|
||||
If you have annotations enabled in the dashboard you can have the table show them. If you configure this
|
||||
mode then any queries you have in the metrics tab will be ignored.
|
||||
|
||||
### JSON Data
|
||||
![](/img/v2/table_json_data.png)
|
||||
|
||||
If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this
|
||||
transform mode and pick the columns using the **Columns** section.
|
||||
|
||||
![](/img/v2/elastic_raw_doc.png)
|
||||
|
||||
## Table Display
|
||||
|
||||
<img class="no-shadow" src="/img/v2/table-display.png">
|
||||
|
||||
1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages.
|
||||
2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
|
||||
3. `Font Size`: The `font size` field allows you to increase or decrease the size for the panel, relative to the default font size.
|
||||
|
||||
|
||||
## Column Styles
|
||||
|
||||
The column styles allow you control how dates and numbers are formatted.
|
||||
|
||||
<img class="no-shadow" src="/img/v2/Column-Options.png">
|
||||
|
||||
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
|
||||
2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
|
||||
3. `Format`: Specify date format. Only available when `Type` is set to `Date`.
|
||||
4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
|
||||
5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
|
||||
6. `Add column style rule`: Add new column rule.
|
||||
|
@ -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-beta1",
|
||||
"version": "3.0.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/torkelo/grafana.git"
|
||||
|
@ -15,3 +15,5 @@ CONF_DIR=/etc/grafana
|
||||
CONF_FILE=/etc/grafana/grafana.ini
|
||||
|
||||
RESTART_ON_UPGRADE=false
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
@ -30,12 +30,14 @@ GRAFANA_HOME=/usr/share/grafana
|
||||
CONF_DIR=/etc/grafana
|
||||
WORK_DIR=$GRAFANA_HOME
|
||||
DATA_DIR=/var/lib/grafana
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
LOG_DIR=/var/log/grafana
|
||||
CONF_FILE=$CONF_DIR/grafana.ini
|
||||
MAX_OPEN_FILES=10000
|
||||
PID_FILE=/var/run/$NAME.pid
|
||||
DAEMON=/usr/sbin/$NAME
|
||||
|
||||
|
||||
umask 0027
|
||||
|
||||
if [ `id -u` -ne 0 ]; then
|
||||
@ -59,7 +61,7 @@ if [ -f "$DEFAULT" ]; then
|
||||
. "$DEFAULT"
|
||||
fi
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
|
@ -14,7 +14,8 @@ ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE} \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR}
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
UMask=0027
|
||||
|
@ -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
|
||||
|
||||
|
@ -29,6 +29,7 @@ GRAFANA_HOME=/usr/share/grafana
|
||||
CONF_DIR=/etc/grafana
|
||||
WORK_DIR=$GRAFANA_HOME
|
||||
DATA_DIR=/var/lib/grafana
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
LOG_DIR=/var/log/grafana
|
||||
CONF_FILE=$CONF_DIR/grafana.ini
|
||||
MAX_OPEN_FILES=10000
|
||||
@ -63,7 +64,7 @@ fi
|
||||
# overwrite settings from default file
|
||||
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function isRunning() {
|
||||
status -p $PID_FILE $NAME > /dev/null 2>&1
|
||||
|
@ -15,3 +15,5 @@ CONF_DIR=/etc/grafana
|
||||
CONF_FILE=/etc/grafana/grafana.ini
|
||||
|
||||
RESTART_ON_UPGRADE=false
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
@ -14,7 +14,8 @@ ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE} \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR}
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
|
||||
|
@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
|
||||
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
|
||||
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
||||
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
||||
regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
|
||||
reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
|
||||
quota := middleware.Quota
|
||||
bind := binding.Bind
|
||||
|
||||
@ -41,6 +41,9 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
|
||||
|
||||
r.Get("/plugins", reqSignedIn, Index)
|
||||
r.Get("/plugins/edit/*", reqSignedIn, Index)
|
||||
|
||||
r.Get("/dashboard/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||
|
||||
@ -65,6 +68,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||
r.Get("/dashboard/snapshot/*", Index)
|
||||
|
||||
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
|
||||
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
|
||||
r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
|
||||
|
||||
@ -113,7 +117,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/invites", wrap(GetPendingOrgInvites))
|
||||
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
|
||||
r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
|
||||
}, regOrgAdmin)
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// create new org
|
||||
r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
|
||||
@ -140,7 +144,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/", wrap(GetApiKeys))
|
||||
r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
|
||||
r.Delete("/:id", wrap(DeleteApiKey))
|
||||
}, regOrgAdmin)
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// Data sources
|
||||
r.Group("/datasources", func() {
|
||||
@ -150,7 +154,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Delete("/:id", DeleteDataSource)
|
||||
r.Get("/:id", wrap(GetDataSourceById))
|
||||
r.Get("/plugins", GetDataSourcePlugins)
|
||||
}, regOrgAdmin)
|
||||
}, reqOrgAdmin)
|
||||
|
||||
r.Get("/frontend/settings/", GetFrontendSettings)
|
||||
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
|
||||
|
@ -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"
|
||||
@ -30,13 +31,15 @@ type cwRequest struct {
|
||||
|
||||
func init() {
|
||||
actionHandlers = map[string]actionHandler{
|
||||
"GetMetricStatistics": handleGetMetricStatistics,
|
||||
"ListMetrics": handleListMetrics,
|
||||
"DescribeInstances": handleDescribeInstances,
|
||||
"__GetRegions": handleGetRegions,
|
||||
"__GetNamespaces": handleGetNamespaces,
|
||||
"__GetMetrics": handleGetMetrics,
|
||||
"__GetDimensions": handleGetDimensions,
|
||||
"GetMetricStatistics": handleGetMetricStatistics,
|
||||
"ListMetrics": handleListMetrics,
|
||||
"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
|
||||
"DescribeAlarmHistory": handleDescribeAlarmHistory,
|
||||
"DescribeInstances": handleDescribeInstances,
|
||||
"__GetRegions": handleGetRegions,
|
||||
"__GetNamespaces": handleGetNamespaces,
|
||||
"__GetMetrics": handleGetMetrics,
|
||||
"__GetDimensions": handleGetDimensions,
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,7 +122,107 @@ 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
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
sess := session.New()
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
})
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(req.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Namespace string `json:"namespace"`
|
||||
MetricName string `json:"metricName"`
|
||||
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
|
||||
Statistic string `json:"statistic"`
|
||||
Period int64 `json:"period"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.DescribeAlarmsForMetricInput{
|
||||
Namespace: aws.String(reqParam.Parameters.Namespace),
|
||||
MetricName: aws.String(reqParam.Parameters.MetricName),
|
||||
Period: aws.Int64(reqParam.Parameters.Period),
|
||||
}
|
||||
if len(reqParam.Parameters.Dimensions) != 0 {
|
||||
params.Dimensions = reqParam.Parameters.Dimensions
|
||||
}
|
||||
if reqParam.Parameters.Statistic != "" {
|
||||
params.Statistic = aws.String(reqParam.Parameters.Statistic)
|
||||
}
|
||||
|
||||
resp, err := svc.DescribeAlarmsForMetric(params)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
sess := session.New()
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
})
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(req.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
AlarmName string `json:"alarmName"`
|
||||
HistoryItemType string `json:"historyItemType"`
|
||||
StartDate int64 `json:"startDate"`
|
||||
EndDate int64 `json:"endDate"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.DescribeAlarmHistoryInput{
|
||||
AlarmName: aws.String(reqParam.Parameters.AlarmName),
|
||||
StartDate: aws.Time(time.Unix(reqParam.Parameters.StartDate, 0)),
|
||||
EndDate: aws.Time(time.Unix(reqParam.Parameters.EndDate, 0)),
|
||||
}
|
||||
if reqParam.Parameters.HistoryItemType != "" {
|
||||
params.HistoryItemType = aws.String(reqParam.Parameters.HistoryItemType)
|
||||
}
|
||||
|
||||
resp, err := svc.DescribeAlarmHistory(params)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
@ -160,7 +263,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 {
|
||||
|
@ -12,6 +12,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func GetSharingOptions(c *middleware.Context) {
|
||||
c.JSON(200, util.DynMap{
|
||||
"externalSnapshotURL": setting.ExternalSnapshotUrl,
|
||||
"externalSnapshotName": setting.ExternalSnapshotName,
|
||||
"externalEnabled": setting.ExternalEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
|
||||
if cmd.External {
|
||||
// external snapshot ref requires key and delete key
|
||||
|
@ -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,
|
||||
})
|
||||
@ -117,7 +118,7 @@ func GetDataSourcePlugins(c *middleware.Context) {
|
||||
dsList := make(map[string]interface{})
|
||||
|
||||
for key, value := range plugins.DataSources {
|
||||
if value.(map[string]interface{})["builtIn"] == nil {
|
||||
if !value.BuiltIn {
|
||||
dsList[key] = value
|
||||
}
|
||||
}
|
||||
|
25
pkg/api/dtos/index.go
Normal file
25
pkg/api/dtos/index.go
Normal file
@ -0,0 +1,25 @@
|
||||
package dtos
|
||||
|
||||
type IndexViewData struct {
|
||||
User *CurrentUser
|
||||
Settings map[string]interface{}
|
||||
AppUrl string
|
||||
AppSubUrl string
|
||||
GoogleAnalyticsId string
|
||||
GoogleTagManagerId string
|
||||
|
||||
PluginCss []*PluginCss
|
||||
PluginJs []string
|
||||
MainNavLinks []*NavLink
|
||||
}
|
||||
|
||||
type PluginCss struct {
|
||||
Light string `json:"light"`
|
||||
Dark string `json:"dark"`
|
||||
}
|
||||
|
||||
type NavLink struct {
|
||||
Text string `json:"text"`
|
||||
Icon string `json:"icon"`
|
||||
Href string `json:"href"`
|
||||
}
|
8
pkg/api/dtos/plugin_bundle.go
Normal file
8
pkg/api/dtos/plugin_bundle.go
Normal file
@ -0,0 +1,8 @@
|
||||
package dtos
|
||||
|
||||
type PluginBundle struct {
|
||||
Type string `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Module string `json:"module"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
}
|
@ -62,6 +62,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
|
||||
@ -106,11 +109,21 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
defaultDatasource = "-- Grafana --"
|
||||
}
|
||||
|
||||
panels := map[string]interface{}{}
|
||||
for _, panel := range plugins.Panels {
|
||||
panels[panel.Type] = map[string]interface{}{
|
||||
"module": panel.Module,
|
||||
"name": panel.Name,
|
||||
}
|
||||
}
|
||||
|
||||
jsonObj := map[string]interface{}{
|
||||
"defaultDatasource": defaultDatasource,
|
||||
"datasources": datasources,
|
||||
"panels": panels,
|
||||
"appSubUrl": setting.AppSubUrl,
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"buildInfo": map[string]interface{}{
|
||||
"version": setting.BuildVersion,
|
||||
"commit": setting.BuildCommit,
|
||||
|
@ -3,65 +3,74 @@ package api
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func setIndexViewData(c *middleware.Context) error {
|
||||
func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
settings, err := getFrontendSettingsMap(c)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentUser := &dtos.CurrentUser{
|
||||
Id: c.UserId,
|
||||
IsSignedIn: c.IsSignedIn,
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
LightTheme: c.Theme == "light",
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
var data = dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserId,
|
||||
IsSignedIn: c.IsSignedIn,
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
LightTheme: c.Theme == "light",
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
},
|
||||
Settings: settings,
|
||||
AppUrl: setting.AppUrl,
|
||||
AppSubUrl: setting.AppSubUrl,
|
||||
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
||||
GoogleTagManagerId: setting.GoogleTagManagerId,
|
||||
}
|
||||
|
||||
if setting.DisableGravatar {
|
||||
currentUser.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
|
||||
data.User.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
|
||||
}
|
||||
|
||||
if len(currentUser.Name) == 0 {
|
||||
currentUser.Name = currentUser.Login
|
||||
if len(data.User.Name) == 0 {
|
||||
data.User.Name = data.User.Login
|
||||
}
|
||||
|
||||
themeUrlParam := c.Query("theme")
|
||||
if themeUrlParam == "light" {
|
||||
currentUser.LightTheme = true
|
||||
data.User.LightTheme = true
|
||||
}
|
||||
|
||||
c.Data["User"] = currentUser
|
||||
c.Data["Settings"] = settings
|
||||
c.Data["AppUrl"] = setting.AppUrl
|
||||
c.Data["AppSubUrl"] = setting.AppSubUrl
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Dashboards",
|
||||
Icon: "fa fa-fw fa-th-large",
|
||||
Href: "/",
|
||||
})
|
||||
|
||||
if setting.GoogleAnalyticsId != "" {
|
||||
c.Data["GoogleAnalyticsId"] = setting.GoogleAnalyticsId
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Data Sources",
|
||||
Icon: "fa fa-fw fa-database",
|
||||
Href: "/datasources",
|
||||
})
|
||||
}
|
||||
|
||||
if setting.GoogleTagManagerId != "" {
|
||||
c.Data["GoogleTagManagerId"] = setting.GoogleTagManagerId
|
||||
}
|
||||
|
||||
return nil
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func Index(c *middleware.Context) {
|
||||
if err := setIndexViewData(c); err != nil {
|
||||
if data, err := setIndexViewData(c); err != nil {
|
||||
c.Handle(500, "Failed to get settings", err)
|
||||
return
|
||||
} else {
|
||||
c.HTML(200, "index", data)
|
||||
}
|
||||
|
||||
c.HTML(200, "index")
|
||||
}
|
||||
|
||||
func NotFoundHandler(c *middleware.Context) {
|
||||
@ -70,10 +79,10 @@ func NotFoundHandler(c *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := setIndexViewData(c); err != nil {
|
||||
if data, err := setIndexViewData(c); err != nil {
|
||||
c.Handle(500, "Failed to get settings", err)
|
||||
return
|
||||
} else {
|
||||
c.HTML(404, "index", data)
|
||||
}
|
||||
|
||||
c.HTML(404, "index")
|
||||
}
|
||||
|
@ -19,18 +19,19 @@ const (
|
||||
)
|
||||
|
||||
func LoginView(c *middleware.Context) {
|
||||
if err := setIndexViewData(c); err != nil {
|
||||
viewData, err := setIndexViewData(c)
|
||||
if err != nil {
|
||||
c.Handle(500, "Failed to get settings", err)
|
||||
return
|
||||
}
|
||||
|
||||
settings := c.Data["Settings"].(map[string]interface{})
|
||||
settings["googleAuthEnabled"] = setting.OAuthService.Google
|
||||
settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||
settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||
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)
|
||||
c.HTML(200, VIEW_INDEX, viewData)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/static"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -28,12 +29,18 @@ func newMacaron() *macaron.Macaron {
|
||||
m.Use(middleware.Gziper())
|
||||
}
|
||||
|
||||
mapStatic(m, "", "public")
|
||||
mapStatic(m, "app", "app")
|
||||
mapStatic(m, "css", "css")
|
||||
mapStatic(m, "img", "img")
|
||||
mapStatic(m, "fonts", "fonts")
|
||||
mapStatic(m, "robots.txt", "robots.txt")
|
||||
for _, route := range plugins.StaticRoutes {
|
||||
pluginRoute := path.Join("/public/plugins/", route.Url)
|
||||
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path)
|
||||
mapStatic(m, route.Path, "", pluginRoute)
|
||||
}
|
||||
|
||||
mapStatic(m, setting.StaticRootPath, "", "public")
|
||||
mapStatic(m, setting.StaticRootPath, "app", "app")
|
||||
mapStatic(m, setting.StaticRootPath, "css", "css")
|
||||
mapStatic(m, setting.StaticRootPath, "img", "img")
|
||||
mapStatic(m, setting.StaticRootPath, "fonts", "fonts")
|
||||
mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
|
||||
|
||||
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: path.Join(setting.StaticRootPath, "views"),
|
||||
@ -51,7 +58,7 @@ func newMacaron() *macaron.Macaron {
|
||||
return m
|
||||
}
|
||||
|
||||
func mapStatic(m *macaron.Macaron, dir string, prefix string) {
|
||||
func mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
|
||||
headers := func(c *macaron.Context) {
|
||||
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
}
|
||||
@ -63,7 +70,7 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) {
|
||||
}
|
||||
|
||||
m.Use(httpstatic.Static(
|
||||
path.Join(setting.StaticRootPath, dir),
|
||||
path.Join(rootDir, dir),
|
||||
httpstatic.StaticOptions{
|
||||
SkipLogging: true,
|
||||
Prefix: prefix,
|
||||
|
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)
|
||||
}
|
@ -131,8 +131,8 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
|
||||
}
|
||||
|
||||
return userQuery.Result, nil
|
||||
}
|
||||
|
||||
}
|
||||
func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) {
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: ldapUser.Username,
|
||||
|
@ -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"`
|
||||
|
||||
|
34
pkg/models/plugin_bundle.go
Normal file
34
pkg/models/plugin_bundle.go
Normal file
@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type PluginBundle struct {
|
||||
Id int64
|
||||
Type string
|
||||
OrgId int64
|
||||
Enabled bool
|
||||
JsonData map[string]interface{}
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// COMMANDS
|
||||
|
||||
// Also acts as api DTO
|
||||
type UpdatePluginBundleCmd struct {
|
||||
Type string `json:"type" binding:"Required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
|
||||
Id int64 `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
type GetPluginBundlesQuery struct {
|
||||
OrgId int64
|
||||
Result []*PluginBundle
|
||||
}
|
26
pkg/plugins/models.go
Normal file
26
pkg/plugins/models.go
Normal file
@ -0,0 +1,26 @@
|
||||
package plugins
|
||||
|
||||
type DataSourcePlugin struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
Module string `json:"module"`
|
||||
Partials map[string]interface{} `json:"partials"`
|
||||
DefaultMatchFormat string `json:"defaultMatchFormat"`
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
StaticRootConfig *StaticRootConfig `json:"staticRoot"`
|
||||
}
|
||||
|
||||
type PanelPlugin struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Module string `json:"module"`
|
||||
StaticRootConfig *StaticRootConfig `json:"staticRoot"`
|
||||
}
|
||||
|
||||
type StaticRootConfig struct {
|
||||
Url string `json:"url"`
|
||||
Path string `json:"path"`
|
||||
}
|
@ -6,18 +6,17 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type PluginMeta struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var (
|
||||
DataSources map[string]interface{}
|
||||
DataSources map[string]DataSourcePlugin
|
||||
Panels map[string]PanelPlugin
|
||||
StaticRoutes []*StaticRootConfig
|
||||
)
|
||||
|
||||
type PluginScanner struct {
|
||||
@ -25,18 +24,37 @@ type PluginScanner struct {
|
||||
errors []error
|
||||
}
|
||||
|
||||
func Init() {
|
||||
func Init() error {
|
||||
DataSources = make(map[string]DataSourcePlugin)
|
||||
StaticRoutes = make([]*StaticRootConfig, 0)
|
||||
Panels = make(map[string]PanelPlugin)
|
||||
|
||||
scan(path.Join(setting.StaticRootPath, "app/plugins"))
|
||||
scan(path.Join(setting.PluginsPath))
|
||||
checkExternalPluginPaths()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkExternalPluginPaths() error {
|
||||
for _, section := range setting.Cfg.Sections() {
|
||||
if strings.HasPrefix(section.Name(), "plugin.") {
|
||||
path := section.Key("path").String()
|
||||
if path != "" {
|
||||
log.Info("Plugin: Scaning dir %s", path)
|
||||
scan(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scan(pluginDir string) error {
|
||||
DataSources = make(map[string]interface{})
|
||||
|
||||
scanner := &PluginScanner{
|
||||
pluginPath: pluginDir,
|
||||
}
|
||||
|
||||
if err := filepath.Walk(pluginDir, scanner.walker); err != nil {
|
||||
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -47,7 +65,7 @@ func scan(pluginDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) error {
|
||||
func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -57,17 +75,25 @@ func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) erro
|
||||
}
|
||||
|
||||
if f.Name() == "plugin.json" {
|
||||
err := scanner.loadPluginJson(path)
|
||||
err := scanner.loadPluginJson(currentPath)
|
||||
if err != nil {
|
||||
log.Error(3, "Failed to load plugin json file: %v, err: %v", path, err)
|
||||
log.Error(3, "Failed to load plugin json file: %v, err: %v", currentPath, err)
|
||||
scanner.errors = append(scanner.errors, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (scanner *PluginScanner) loadPluginJson(path string) error {
|
||||
reader, err := os.Open(path)
|
||||
func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
|
||||
if staticRootConfig != nil {
|
||||
staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path)
|
||||
StaticRoutes = append(StaticRoutes, staticRootConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
currentDir := filepath.Dir(pluginJsonFilePath)
|
||||
reader, err := os.Open(pluginJsonFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -87,11 +113,33 @@ func (scanner *PluginScanner) loadPluginJson(path string) error {
|
||||
}
|
||||
|
||||
if pluginType == "datasource" {
|
||||
datasourceType, exists := pluginJson["type"]
|
||||
if !exists {
|
||||
p := DataSourcePlugin{}
|
||||
reader.Seek(0, 0)
|
||||
if err := jsonParser.Decode(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Type == "" {
|
||||
return errors.New("Did not find type property in plugin.json")
|
||||
}
|
||||
DataSources[datasourceType.(string)] = pluginJson
|
||||
|
||||
DataSources[p.Type] = p
|
||||
addStaticRoot(p.StaticRootConfig, currentDir)
|
||||
}
|
||||
|
||||
if pluginType == "panel" {
|
||||
p := PanelPlugin{}
|
||||
reader.Seek(0, 0)
|
||||
if err := jsonParser.Decode(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Type == "" {
|
||||
return errors.New("Did not find type property in plugin.json")
|
||||
}
|
||||
|
||||
Panels[p.Type] = p
|
||||
addStaticRoot(p.StaticRootConfig, currentDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -4,14 +4,17 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestPluginScans(t *testing.T) {
|
||||
|
||||
Convey("When scaning for plugins", t, func() {
|
||||
path, _ := filepath.Abs("../../public/app/plugins")
|
||||
err := scan(path)
|
||||
setting.StaticRootPath, _ = filepath.Abs("../../public/")
|
||||
setting.Cfg = ini.Empty()
|
||||
err := Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(DataSources), ShouldBeGreaterThan, 1)
|
||||
|
@ -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",
|
||||
}))
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addApiKeyMigrations(mg)
|
||||
addDashboardSnapshotMigrations(mg)
|
||||
addQuotaMigration(mg)
|
||||
addPluginBundleMigration(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
26
pkg/services/sqlstore/migrations/plugin_bundle.go
Normal file
26
pkg/services/sqlstore/migrations/plugin_bundle.go
Normal file
@ -0,0 +1,26 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addPluginBundleMigration(mg *Migrator) {
|
||||
|
||||
var pluginBundleV1 = Table{
|
||||
Name: "plugin_bundle",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "enabled", Type: DB_Bool, Nullable: false},
|
||||
{Name: "json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"org_id", "type"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1))
|
||||
|
||||
//------- indexes ------------------
|
||||
addTableIndicesMigrations(mg, "v1", pluginBundleV1)
|
||||
}
|
@ -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 {
|
||||
|
46
pkg/services/sqlstore/plugin_bundle.go
Normal file
46
pkg/services/sqlstore/plugin_bundle.go
Normal file
@ -0,0 +1,46 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetPluginBundles)
|
||||
bus.AddHandler("sql", UpdatePluginBundle)
|
||||
}
|
||||
|
||||
func GetPluginBundles(query *m.GetPluginBundlesQuery) error {
|
||||
sess := x.Where("org_id=?", query.OrgId)
|
||||
|
||||
query.Result = make([]*m.PluginBundle, 0)
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func UpdatePluginBundle(cmd *m.UpdatePluginBundleCmd) error {
|
||||
return inTransaction2(func(sess *session) error {
|
||||
var bundle m.PluginBundle
|
||||
|
||||
exists, err := sess.Where("org_id=? and type=?", cmd.OrgId, cmd.Type).Get(&bundle)
|
||||
sess.UseBool("enabled")
|
||||
if !exists {
|
||||
bundle = m.PluginBundle{
|
||||
Type: cmd.Type,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
JsonData: cmd.JsonData,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
_, err = sess.Insert(&bundle)
|
||||
return err
|
||||
} else {
|
||||
bundle.Enabled = cmd.Enabled
|
||||
bundle.JsonData = cmd.JsonData
|
||||
_, err = sess.Id(bundle.Id).Update(&bundle)
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
@ -48,9 +48,10 @@ var (
|
||||
BuildStamp int64
|
||||
|
||||
// Paths
|
||||
LogsPath string
|
||||
HomePath string
|
||||
DataPath string
|
||||
LogsPath string
|
||||
HomePath string
|
||||
DataPath string
|
||||
PluginsPath string
|
||||
|
||||
// Log settings.
|
||||
LogModes []string
|
||||
@ -76,12 +77,18 @@ var (
|
||||
EmailCodeValidMinutes int
|
||||
DataProxyWhiteList map[string]bool
|
||||
|
||||
// Snapshots
|
||||
ExternalSnapshotUrl string
|
||||
ExternalSnapshotName string
|
||||
ExternalEnabled bool
|
||||
|
||||
// User settings
|
||||
AllowUserSignUp bool
|
||||
AllowUserOrgCreate bool
|
||||
AutoAssignOrg bool
|
||||
AutoAssignOrgRole string
|
||||
VerifyEmailEnabled bool
|
||||
LoginHint string
|
||||
|
||||
// Http auth
|
||||
AdminUser string
|
||||
@ -281,13 +288,11 @@ func loadSpecifedConfigFile(configFile string) {
|
||||
|
||||
defaultSec, err := Cfg.GetSection(section.Name())
|
||||
if err != nil {
|
||||
log.Error(3, "Unknown config section %s defined in %s", section.Name(), configFile)
|
||||
continue
|
||||
defaultSec, _ = Cfg.NewSection(section.Name())
|
||||
}
|
||||
defaultKey, err := defaultSec.GetKey(key.Name())
|
||||
if err != nil {
|
||||
log.Error(3, "Unknown config key %s defined in section %s, in file %s", key.Name(), section.Name(), configFile)
|
||||
continue
|
||||
defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value())
|
||||
}
|
||||
defaultKey.SetValue(key.Value())
|
||||
}
|
||||
@ -389,6 +394,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
loadConfiguration(args)
|
||||
|
||||
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
||||
PluginsPath = Cfg.Section("paths").Key("plugins").String()
|
||||
|
||||
server := Cfg.Section("server")
|
||||
AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
|
||||
@ -420,6 +426,12 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
CookieRememberName = security.Key("cookie_remember_name").String()
|
||||
DisableGravatar = security.Key("disable_gravatar").MustBool(true)
|
||||
|
||||
// read snapshots settings
|
||||
snapshots := Cfg.Section("snapshots")
|
||||
ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String()
|
||||
ExternalSnapshotName = snapshots.Key("external_snapshot_name").String()
|
||||
ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
|
||||
|
||||
// read data source proxy white list
|
||||
DataProxyWhiteList = make(map[string]bool)
|
||||
for _, hostAndIp := range security.Key("data_source_proxy_whitelist").Strings(" ") {
|
||||
@ -436,6 +448,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)
|
||||
@ -573,6 +586,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])
|
||||
@ -607,6 +628,7 @@ func LogConfigurationInfo() {
|
||||
text.WriteString(fmt.Sprintf(" home: %s\n", HomePath))
|
||||
text.WriteString(fmt.Sprintf(" data: %s\n", DataPath))
|
||||
text.WriteString(fmt.Sprintf(" logs: %s\n", LogsPath))
|
||||
text.WriteString(fmt.Sprintf(" plugins: %s\n", PluginsPath))
|
||||
|
||||
log.Info(text.String())
|
||||
}
|
||||
|
98
pkg/util/filepath.go
Normal file
98
pkg/util/filepath.go
Normal file
@ -0,0 +1,98 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
//WalkSkipDir is the Error returned when we want to skip descending into a directory
|
||||
var WalkSkipDir = errors.New("skip this directory")
|
||||
|
||||
//WalkFunc is a callback function called for each path as a directory is walked
|
||||
//If resolvedPath != "", then we are following symbolic links.
|
||||
type WalkFunc func(resolvedPath string, info os.FileInfo, err error) error
|
||||
|
||||
//Walk walks a path, optionally following symbolic links, and for each path,
|
||||
//it calls the walkFn passed.
|
||||
//
|
||||
//It is similar to filepath.Walk, except that it supports symbolic links and
|
||||
//can detect infinite loops while following sym links.
|
||||
//It solves the issue where your WalkFunc needs a path relative to the symbolic link
|
||||
//(resolving links within walkfunc loses the path to the symbolic link for each traversal).
|
||||
func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walkFn WalkFunc) error {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var symlinkPathsFollowed map[string]bool
|
||||
var resolvedPath string
|
||||
if followSymlinks {
|
||||
resolvedPath = path
|
||||
if detectSymlinkInfiniteLoop {
|
||||
symlinkPathsFollowed = make(map[string]bool, 8)
|
||||
}
|
||||
}
|
||||
return walk(path, info, resolvedPath, symlinkPathsFollowed, walkFn)
|
||||
}
|
||||
|
||||
//walk walks the path. It is a helper/sibling function to Walk.
|
||||
//It takes a resolvedPath into consideration. This way, paths being walked are
|
||||
//always relative to the path argument, even if symbolic links were resolved).
|
||||
//
|
||||
//If resolvedPath is "", then we are not following symbolic links.
|
||||
//If symlinkPathsFollowed is not nil, then we need to detect infinite loop.
|
||||
func walk(path string, info os.FileInfo, resolvedPath string,
|
||||
symlinkPathsFollowed map[string]bool, walkFn WalkFunc) error {
|
||||
if info == nil {
|
||||
return errors.New("Walk: Nil FileInfo passed")
|
||||
}
|
||||
err := walkFn(resolvedPath, info, nil)
|
||||
if err != nil {
|
||||
if info.IsDir() && err == WalkSkipDir {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if resolvedPath != "" && info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
path2, err := os.Readlink(resolvedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//vout("SymLink Path: %v, links to: %v", resolvedPath, path2)
|
||||
if symlinkPathsFollowed != nil {
|
||||
if _, ok := symlinkPathsFollowed[path2]; ok {
|
||||
errMsg := "Potential SymLink Infinite Loop. Path: %v, Link To: %v"
|
||||
return fmt.Errorf(errMsg, resolvedPath, path2)
|
||||
} else {
|
||||
symlinkPathsFollowed[path2] = true
|
||||
}
|
||||
}
|
||||
info2, err := os.Lstat(path2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return walk(path, info2, path2, symlinkPathsFollowed, walkFn)
|
||||
}
|
||||
if info.IsDir() {
|
||||
list, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return walkFn(resolvedPath, info, err)
|
||||
}
|
||||
for _, fileInfo := range list {
|
||||
path2 := filepath.Join(path, fileInfo.Name())
|
||||
var resolvedPath2 string
|
||||
if resolvedPath != "" {
|
||||
resolvedPath2 = filepath.Join(resolvedPath, fileInfo.Name())
|
||||
}
|
||||
err = walk(path2, fileInfo, resolvedPath2, symlinkPathsFollowed, walkFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
@ -2,6 +2,7 @@ define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'lodash',
|
||||
'app/core/config',
|
||||
'require',
|
||||
'bootstrap',
|
||||
'angular-route',
|
||||
@ -12,7 +13,7 @@ define([
|
||||
'bindonce',
|
||||
'app/core/core',
|
||||
],
|
||||
function (angular, $, _, appLevelRequire) {
|
||||
function (angular, $, _, config, appLevelRequire) {
|
||||
"use strict";
|
||||
|
||||
var app = angular.module('grafana', []);
|
||||
@ -35,6 +36,8 @@ function (angular, $, _, appLevelRequire) {
|
||||
} else {
|
||||
_.extend(module, register_fns);
|
||||
}
|
||||
// push it into the apps dependencies
|
||||
apps_deps.push(module.name);
|
||||
return module;
|
||||
};
|
||||
|
||||
@ -64,13 +67,15 @@ function (angular, $, _, appLevelRequire) {
|
||||
var module_name = 'grafana.'+type;
|
||||
// create the module
|
||||
app.useModule(angular.module(module_name, []));
|
||||
// push it into the apps dependencies
|
||||
apps_deps.push(module_name);
|
||||
});
|
||||
|
||||
var preBootRequires = [
|
||||
'app/features/all',
|
||||
];
|
||||
var preBootRequires = ['app/features/all'];
|
||||
var pluginModules = config.bootData.pluginModules || [];
|
||||
|
||||
// add plugin modules
|
||||
for (var i = 0; i < pluginModules.length; i++) {
|
||||
preBootRequires.push(pluginModules[i]);
|
||||
}
|
||||
|
||||
app.boot = function() {
|
||||
require(preBootRequires, function () {
|
||||
|
@ -6,6 +6,7 @@ function (Settings) {
|
||||
|
||||
var bootData = window.grafanaBootData || { settings: {} };
|
||||
var options = bootData.settings;
|
||||
options.bootData = bootData;
|
||||
|
||||
return new Settings(options);
|
||||
|
||||
|
@ -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';
|
||||
|
@ -15,19 +15,13 @@ function (angular, _, $, coreModule, config) {
|
||||
};
|
||||
|
||||
$scope.setupMainNav = function() {
|
||||
$scope.mainLinks.push({
|
||||
text: "Dashboards",
|
||||
icon: "fa fa-fw fa-th-large",
|
||||
href: $scope.getUrl("/"),
|
||||
});
|
||||
|
||||
if (contextSrv.hasRole('Admin')) {
|
||||
_.each(config.bootData.mainNavLinks, function(item) {
|
||||
$scope.mainLinks.push({
|
||||
text: "Data Sources",
|
||||
icon: "fa fa-fw fa-database",
|
||||
href: $scope.getUrl("/datasources"),
|
||||
text: item.text,
|
||||
icon: item.icon,
|
||||
href: $scope.getUrl(item.href)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loadOrgs = function() {
|
||||
@ -120,6 +114,7 @@ function (angular, _, $, coreModule, config) {
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.showSignout = contextSrv.isSignedIn && !config['authProxyEnabled'];
|
||||
$scope.updateMenu();
|
||||
$scope.$on('$routeChangeSuccess', $scope.updateMenu);
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -131,6 +131,19 @@ define([
|
||||
templateUrl: 'app/partials/reset_password.html',
|
||||
controller : 'ResetPasswordCtrl',
|
||||
})
|
||||
.when('/plugins', {
|
||||
templateUrl: 'app/features/org/partials/plugins.html',
|
||||
controller: 'PluginsCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/plugins/edit/:type', {
|
||||
templateUrl: 'app/features/org/partials/pluginEdit.html',
|
||||
controller: 'PluginEditCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/global-alerts', {
|
||||
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
|
||||
})
|
||||
.otherwise({
|
||||
templateUrl: 'app/partials/error.html',
|
||||
controller: 'ErrorCtrl'
|
||||
|
@ -12,8 +12,8 @@ function (angular, _, coreModule, store, config) {
|
||||
var self = this;
|
||||
|
||||
function User() {
|
||||
if (window.grafanaBootData.user) {
|
||||
_.extend(this, window.grafanaBootData.user);
|
||||
if (config.bootData.user) {
|
||||
_.extend(this, config.bootData.user);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,15 +8,8 @@ function (_) {
|
||||
var defaults = {
|
||||
datasources : {},
|
||||
window_title_prefix : 'Grafana - ',
|
||||
panels : {
|
||||
'graph': { path: 'app/panels/graph', name: 'Graph' },
|
||||
'table': { path: 'app/panels/table', name: 'Table' },
|
||||
'singlestat': { path: 'app/panels/singlestat', name: 'Single stat' },
|
||||
'text': { path: 'app/panels/text', name: 'Text' },
|
||||
'dashlist': { path: 'app/panels/dashlist', name: 'Dashboard list' },
|
||||
},
|
||||
panels : {},
|
||||
new_panel_title: 'Panel Title',
|
||||
plugins: {},
|
||||
playlist_timespan: "1m",
|
||||
unsaved_changes_warning: true,
|
||||
appSubUrl: ""
|
||||
|
@ -399,6 +399,7 @@ function($, _) {
|
||||
// Volume
|
||||
kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L');
|
||||
kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
|
||||
kbn.valueFormats.m3 = kbn.formatBuilders.decimalSIPrefix('m3');
|
||||
|
||||
// Time
|
||||
kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
|
||||
@ -626,8 +627,9 @@ function($, _) {
|
||||
{
|
||||
text: 'volume',
|
||||
submenu: [
|
||||
{text: 'millilitre', value: 'mlitre'},
|
||||
{text: 'litre', value: 'litre' },
|
||||
{text: 'millilitre', value: 'mlitre'},
|
||||
{text: 'litre', value: 'litre' },
|
||||
{text: 'cubic metre', value: 'm3' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -2,8 +2,9 @@ define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'app/core/config',
|
||||
'moment',
|
||||
],
|
||||
function (angular, $, config) {
|
||||
function (angular, $, config, moment) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
@ -149,6 +150,10 @@ function (angular, $, config) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.formatDate = function(date) {
|
||||
return moment(date).format('MMM Do YYYY, h:mm:ss a');
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -49,6 +49,21 @@ function (angular, _) {
|
||||
$scope.appEvent('hide-dash-search');
|
||||
};
|
||||
|
||||
$scope.makeEditable = function() {
|
||||
$scope.dashboard.editable = true;
|
||||
|
||||
var clone = $scope.dashboard.getSaveModelClone();
|
||||
|
||||
backendSrv.saveDashboard(clone, {overwrite: false}).then(function(data) {
|
||||
$scope.dashboard.version = data.version;
|
||||
$scope.appEvent('dashboard-saved', $scope.dashboard);
|
||||
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
|
||||
|
||||
//force refresh whole page
|
||||
window.location.href = window.location.href;
|
||||
}, $scope.handleSaveDashError);
|
||||
};
|
||||
|
||||
$scope.saveDashboard = function(options) {
|
||||
if ($scope.dashboardMeta.canSave === false) {
|
||||
return;
|
||||
|
@ -214,10 +214,7 @@ function (angular, $, _, moment) {
|
||||
};
|
||||
|
||||
p.formatDate = function(date, format) {
|
||||
if (!moment.isMoment(date)) {
|
||||
date = moment(date);
|
||||
}
|
||||
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
return this.timezone === 'browser' ?
|
||||
@ -225,6 +222,14 @@ function (angular, $, _, moment) {
|
||||
moment.utc(date).format(format);
|
||||
};
|
||||
|
||||
p.getRelativeTime = function(date) {
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
|
||||
return this.timezone === 'browser' ?
|
||||
moment(date).fromNow() :
|
||||
moment.utc(date).fromNow();
|
||||
};
|
||||
|
||||
p._updateSchema = function(old) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
|
@ -37,6 +37,7 @@
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
|
||||
<li><a class="pointer" ng-click="exportDashboard();">Export</a></li>
|
||||
<li><a class="pointer" ng-click="editJson();">View JSON</a></li>
|
||||
<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
|
||||
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
|
||||
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
|
||||
</ul>
|
||||
|
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>
|
@ -107,7 +107,7 @@
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareSnapshot.html">
|
||||
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl">
|
||||
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
|
||||
<div class="share-modal-big-icon">
|
||||
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i ng-if="!loading" class="gf-icon gf-icon-snap-multi"></i>
|
||||
@ -175,10 +175,9 @@
|
||||
<i class="fa fa-save"></i>
|
||||
Local Snapshot
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-large" ng-click="createSnapshot(true)" ng-disabled="loading">
|
||||
<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
Publish to snapshot.raintank.io
|
||||
{{sharingButtonText}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -42,12 +42,21 @@ function (angular, _, config) {
|
||||
};
|
||||
|
||||
$scope.deleteRow = function() {
|
||||
function delete_row() {
|
||||
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
|
||||
}
|
||||
|
||||
if (!$scope.row.panels.length) {
|
||||
delete_row();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Are you sure you want to delete this row?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: function() {
|
||||
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
|
||||
delete_row();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -29,7 +29,14 @@ function (angular, _) {
|
||||
{text: 'Public on the web', value: 3},
|
||||
];
|
||||
|
||||
$scope.externalUrl = '//snapshots-origin.raintank.io';
|
||||
$scope.init = function() {
|
||||
backendSrv.get('/api/snapshot/shared-options').then(function(options) {
|
||||
$scope.externalUrl = options['externalSnapshotURL'];
|
||||
$scope.sharingButtonText = options['externalSnapshotName'];
|
||||
$scope.externalEnabled = options['externalEnabled'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.apiUrl = '/api/snapshots';
|
||||
|
||||
$scope.createSnapshot = function(external) {
|
||||
|
@ -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>
|
||||
|
@ -6,4 +6,8 @@ define([
|
||||
'./userInviteCtrl',
|
||||
'./orgApiKeysCtrl',
|
||||
'./orgDetailsCtrl',
|
||||
'./pluginsCtrl',
|
||||
'./pluginEditCtrl',
|
||||
'./plugin_srv',
|
||||
'./plugin_directive',
|
||||
], function () {});
|
||||
|
@ -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>
|
||||
|
||||
|
3
public/app/features/org/partials/pluginConfigCore.html
Normal file
3
public/app/features/org/partials/pluginConfigCore.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{current.type}} plugin does not have any additional config.
|
||||
</div>
|
42
public/app/features/org/partials/pluginEdit.html
Normal file
42
public/app/features/org/partials/pluginEdit.html
Normal file
@ -0,0 +1,42 @@
|
||||
<topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true">
|
||||
<ul class="nav">
|
||||
<li ><a href="plugins">Overview</a></li>
|
||||
<li class="active" ><a href="plugins/edit/{{current.type}}">Edit</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>Edit Plugin</h2>
|
||||
|
||||
|
||||
<form name="editForm">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Type
|
||||
</li>
|
||||
<li>
|
||||
<li>
|
||||
<input type="text" disabled="disabled" class="input-xlarge tight-form-input" ng-model="current.type">
|
||||
</li>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Default
|
||||
<input class="cr1" id="current.enabled" type="checkbox" ng-model="current.enabled" ng-checked="current.enabled">
|
||||
<label for="current.enabled" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<br>
|
||||
<plugin-config-loader plugin="current"></plugin-config-loader>
|
||||
<div class="pull-right" style="margin-top: 35px">
|
||||
<button type="submit" class="btn btn-success" ng-click="update()">Save</button>
|
||||
<a class="btn btn-inverse" href="plugins">Cancel</a>
|
||||
</div>
|
||||
<br>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
41
public/app/features/org/partials/plugins.html
Normal file
41
public/app/features/org/partials/plugins.html
Normal file
@ -0,0 +1,41 @@
|
||||
<topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active" ><a href="plugins">Overview</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>Plugins</h2>
|
||||
|
||||
<div ng-if="!plugins">
|
||||
<em>No plugins defined</em>
|
||||
</div>
|
||||
|
||||
<table class="grafana-options-table" ng-if="plugins">
|
||||
<tr>
|
||||
<td><strong>Type</strong></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr ng-repeat="(type, p) in plugins">
|
||||
<td style="width:1%">
|
||||
<i class="fa fa-cubes"></i>
|
||||
{{p.type}}
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a href="plugins/edit/{{p.type}}" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
Enabled
|
||||
<input id="p.enabled" type="checkbox" ng-model="p.enabled" ng-checked="p.enabled" ng-change="update(p)">
|
||||
<label for="p.enabled"></label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
35
public/app/features/org/pluginEditCtrl.js
Normal file
35
public/app/features/org/pluginEditCtrl.js
Normal file
@ -0,0 +1,35 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PluginEditCtrl', function($scope, pluginSrv, $routeParams) {
|
||||
$scope.init = function() {
|
||||
$scope.current = {};
|
||||
$scope.getPlugins();
|
||||
};
|
||||
|
||||
$scope.getPlugins = function() {
|
||||
pluginSrv.get($routeParams.type).then(function(result) {
|
||||
$scope.current = _.clone(result);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
$scope._update();
|
||||
};
|
||||
|
||||
$scope._update = function() {
|
||||
pluginSrv.update($scope.current).then(function() {
|
||||
window.location.href = config.appSubUrl + "plugins";
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
47
public/app/features/org/plugin_directive.js
Normal file
47
public/app/features/org/plugin_directive.js
Normal file
@ -0,0 +1,47 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
module.directive('pluginConfigLoader', function($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, elem) {
|
||||
var directive = 'grafana-plugin-core';
|
||||
//wait for the parent scope to be applied.
|
||||
scope.$watch("current", function(newVal) {
|
||||
if (newVal) {
|
||||
if (newVal.module) {
|
||||
directive = 'grafana-plugin-'+newVal.type;
|
||||
}
|
||||
scope.require([newVal.module], function () {
|
||||
var panelEl = angular.element(document.createElement(directive));
|
||||
elem.append(panelEl);
|
||||
$compile(panelEl)(scope);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('grafanaPluginCore', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/features/org/partials/pluginConfigCore.html',
|
||||
transclude: true,
|
||||
link: function(scope) {
|
||||
scope.update = function() {
|
||||
//Perform custom save events to the plugins own backend if needed.
|
||||
|
||||
// call parent update to commit the change to the plugin object.
|
||||
// this will cause the page to reload.
|
||||
scope._update();
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
58
public/app/features/org/plugin_srv.js
Normal file
58
public/app/features/org/plugin_srv.js
Normal file
@ -0,0 +1,58 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('pluginSrv', function($rootScope, $timeout, $q, backendSrv) {
|
||||
var self = this;
|
||||
this.init = function() {
|
||||
console.log("pluginSrv init");
|
||||
this.plugins = {};
|
||||
};
|
||||
|
||||
this.get = function(type) {
|
||||
return $q(function(resolve) {
|
||||
if (type in self.plugins) {
|
||||
return resolve(self.plugins[type]);
|
||||
}
|
||||
backendSrv.get('/api/plugins').then(function(results) {
|
||||
_.forEach(results, function(p) {
|
||||
self.plugins[p.type] = p;
|
||||
});
|
||||
return resolve(self.plugins[type]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.getAll = function() {
|
||||
return $q(function(resolve) {
|
||||
if (!_.isEmpty(self.plugins)) {
|
||||
return resolve(self.plugins);
|
||||
}
|
||||
backendSrv.get('api/plugins').then(function(results) {
|
||||
_.forEach(results, function(p) {
|
||||
self.plugins[p.type] = p;
|
||||
});
|
||||
return resolve(self.plugins);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.update = function(plugin) {
|
||||
return $q(function(resolve, reject) {
|
||||
backendSrv.post('/api/plugins', plugin).then(function(resp) {
|
||||
self.plugins[plugin.type] = plugin;
|
||||
resolve(resp);
|
||||
}, function(resp) {
|
||||
reject(resp);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.init();
|
||||
});
|
||||
});
|
33
public/app/features/org/pluginsCtrl.js
Normal file
33
public/app/features/org/pluginsCtrl.js
Normal file
@ -0,0 +1,33 @@
|
||||
define([
|
||||
'angular',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PluginsCtrl', function($scope, $location, pluginSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.plugins = {};
|
||||
$scope.getPlugins();
|
||||
};
|
||||
|
||||
$scope.getPlugins = function() {
|
||||
pluginSrv.getAll().then(function(result) {
|
||||
console.log(result);
|
||||
$scope.plugins = result;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function(plugin) {
|
||||
pluginSrv.update(plugin).then(function() {
|
||||
window.location.href = config.appSubUrl + $location.path();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
@ -13,9 +13,9 @@ function (angular, $, config) {
|
||||
restrict: 'E',
|
||||
link: function(scope, elem, attr) {
|
||||
var getter = $parse(attr.type), panelType = getter(scope);
|
||||
var panelPath = config.panels[panelType].path;
|
||||
var module = config.panels[panelType].module;
|
||||
|
||||
scope.require([panelPath + "/module"], function () {
|
||||
scope.require([module], function () {
|
||||
var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
|
||||
elem.append(panelEl);
|
||||
$compile(panelEl)(scope);
|
||||
|
@ -11,7 +11,7 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('panelHelper', function(timeSrv, $rootScope) {
|
||||
module.service('panelHelper', function(timeSrv, $rootScope, $q) {
|
||||
var self = this;
|
||||
|
||||
this.setTimeQueryStart = function(scope) {
|
||||
@ -59,7 +59,9 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
|
||||
scope.resolution = Math.ceil($(window).width() * (scope.panel.span / 12));
|
||||
}
|
||||
|
||||
scope.interval = kbn.calculateInterval(scope.range, scope.resolution, scope.panel.interval);
|
||||
var panelInterval = scope.panel.interval;
|
||||
var datasourceInterval = (scope.datasource || {}).interval;
|
||||
scope.interval = kbn.calculateInterval(scope.range, scope.resolution, panelInterval || datasourceInterval);
|
||||
};
|
||||
|
||||
this.applyPanelTimeOverrides = function(scope) {
|
||||
@ -103,6 +105,10 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
|
||||
};
|
||||
|
||||
this.issueMetricQuery = function(scope, datasource) {
|
||||
if (!scope.panel.targets || scope.panel.targets.length === 0) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
var metricsQuery = {
|
||||
range: scope.range,
|
||||
rangeRaw: scope.rangeRaw,
|
||||
|
@ -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]) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user