mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into readonly_dashboards
This commit is contained in:
commit
6bbeb723ca
18
CHANGELOG.md
18
CHANGELOG.md
@ -10,8 +10,11 @@
|
|||||||
* **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942)
|
* **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942)
|
||||||
* **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz)
|
* **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz)
|
||||||
* **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
|
* **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
|
||||||
|
* **Prometheus**: Show template variable candidate in query editor [#9210](https://github.com/grafana/grafana/issues/9210), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
|
* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
|
* **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
|
||||||
|
* **Table**: Table plugin value mappings [#7119](https://github.com/grafana/grafana/issues/7119), thx [infernix](https://github.com/infernix)
|
||||||
|
* **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
|
||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
|
* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
|
||||||
@ -21,9 +24,20 @@
|
|||||||
* **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps)
|
* **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps)
|
||||||
* **Dashboards**: Version cleanup fails on old databases with many entries [#11278](https://github.com/grafana/grafana/issues/11278)
|
* **Dashboards**: Version cleanup fails on old databases with many entries [#11278](https://github.com/grafana/grafana/issues/11278)
|
||||||
* **Server**: Adjust permissions of unix socket [#11343](https://github.com/grafana/grafana/pull/11343), thx [@corny](https://github.com/corny)
|
* **Server**: Adjust permissions of unix socket [#11343](https://github.com/grafana/grafana/pull/11343), thx [@corny](https://github.com/corny)
|
||||||
|
* **Shortcuts**: Add shortcut for duplicate panel [#11102](https://github.com/grafana/grafana/issues/11102)
|
||||||
|
* **AuthProxy**: Support IPv6 in Auth proxy white list [#11330](https://github.com/grafana/grafana/pull/11330), thx [@corny](https://github.com/corny)
|
||||||
|
* **SMTP**: Don't connect to STMP server using TLS unless configured. [#7189](https://github.com/grafana/grafana/issues/7189)
|
||||||
|
* **Prometheus**: Escape backslash in labels correctly. [#10555](https://github.com/grafana/grafana/issues/10555), thx [@roidelapluie](https://github.com/roidelapluie)
|
||||||
|
* **Variables** Case-insensitive sorting for template values [#11128](https://github.com/grafana/grafana/issues/11128) thx [@cross](https://github.com/cross)
|
||||||
|
|
||||||
# 5.0.4 (unreleased)
|
# 5.0.4 (2018-03-28)
|
||||||
* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086)
|
|
||||||
|
* **Docker** Can't start Grafana on Kubernetes 1.7.14, 1.8.9, or 1.9.4 [#140 in grafana-docker repo](https://github.com/grafana/grafana-docker/issues/140) thx [@suquant](https://github.com/suquant)
|
||||||
|
* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086) & [#11296](https://github.com/grafana/grafana/issues/11296)
|
||||||
|
* **Dashboard** Provisioning dashboard with alert rules should create alerts [#11247](https://github.com/grafana/grafana/issues/11247)
|
||||||
|
* **Snapshots** For snapshots, the Graph panel renders the legend incorrectly on right hand side [#11318](https://github.com/grafana/grafana/issues/11318)
|
||||||
|
* **Alerting** Link back to Grafana returns wrong URL if root_path contains sub-path components [#11403](https://github.com/grafana/grafana/issues/11403)
|
||||||
|
* **Alerting** Incorrect default value for upload images setting for alert notifiers [#11413](https://github.com/grafana/grafana/pull/11413)
|
||||||
|
|
||||||
# 5.0.3 (2018-03-16)
|
# 5.0.3 (2018-03-16)
|
||||||
* **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155)
|
* **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155)
|
||||||
|
217
Gopkg.lock
generated
217
Gopkg.lock
generated
@ -27,37 +27,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/aws/aws-sdk-go"
|
name = "github.com/aws/aws-sdk-go"
|
||||||
packages = [
|
packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/ec2query","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","service/cloudwatch","service/ec2","service/ec2/ec2iface","service/s3","service/sts"]
|
||||||
"aws",
|
|
||||||
"aws/awserr",
|
|
||||||
"aws/awsutil",
|
|
||||||
"aws/client",
|
|
||||||
"aws/client/metadata",
|
|
||||||
"aws/corehandlers",
|
|
||||||
"aws/credentials",
|
|
||||||
"aws/credentials/ec2rolecreds",
|
|
||||||
"aws/credentials/endpointcreds",
|
|
||||||
"aws/credentials/stscreds",
|
|
||||||
"aws/defaults",
|
|
||||||
"aws/ec2metadata",
|
|
||||||
"aws/endpoints",
|
|
||||||
"aws/request",
|
|
||||||
"aws/session",
|
|
||||||
"aws/signer/v4",
|
|
||||||
"internal/shareddefaults",
|
|
||||||
"private/protocol",
|
|
||||||
"private/protocol/ec2query",
|
|
||||||
"private/protocol/query",
|
|
||||||
"private/protocol/query/queryutil",
|
|
||||||
"private/protocol/rest",
|
|
||||||
"private/protocol/restxml",
|
|
||||||
"private/protocol/xml/xmlutil",
|
|
||||||
"service/cloudwatch",
|
|
||||||
"service/ec2",
|
|
||||||
"service/ec2/ec2iface",
|
|
||||||
"service/s3",
|
|
||||||
"service/sts"
|
|
||||||
]
|
|
||||||
revision = "decd990ddc5dcdf2f73309cbcab90d06b996ca28"
|
revision = "decd990ddc5dcdf2f73309cbcab90d06b996ca28"
|
||||||
version = "v1.12.67"
|
version = "v1.12.67"
|
||||||
|
|
||||||
@ -105,10 +75,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/denisenkom/go-mssqldb"
|
name = "github.com/denisenkom/go-mssqldb"
|
||||||
packages = [
|
packages = [".","internal/cp"]
|
||||||
".",
|
|
||||||
"internal/cp"
|
|
||||||
]
|
|
||||||
revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
|
revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
@ -150,12 +117,7 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/go-macaron/session"
|
name = "github.com/go-macaron/session"
|
||||||
packages = [
|
packages = [".","memcache","postgres","redis"]
|
||||||
".",
|
|
||||||
"memcache",
|
|
||||||
"postgres",
|
|
||||||
"redis"
|
|
||||||
]
|
|
||||||
revision = "b8e286a0dba8f4999042d6b258daf51b31d08938"
|
revision = "b8e286a0dba8f4999042d6b258daf51b31d08938"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
@ -190,13 +152,7 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/golang/protobuf"
|
name = "github.com/golang/protobuf"
|
||||||
packages = [
|
packages = ["proto","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"]
|
||||||
"proto",
|
|
||||||
"ptypes",
|
|
||||||
"ptypes/any",
|
|
||||||
"ptypes/duration",
|
|
||||||
"ptypes/timestamp"
|
|
||||||
]
|
|
||||||
revision = "c65a0412e71e8b9b3bfd22925720d23c0f054237"
|
revision = "c65a0412e71e8b9b3bfd22925720d23c0f054237"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
@ -265,10 +221,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/klauspost/compress"
|
name = "github.com/klauspost/compress"
|
||||||
packages = [
|
packages = ["flate","gzip"]
|
||||||
"flate",
|
|
||||||
"gzip"
|
|
||||||
]
|
|
||||||
revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
|
revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
|
||||||
version = "v1.2.1"
|
version = "v1.2.1"
|
||||||
|
|
||||||
@ -299,10 +252,7 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/lib/pq"
|
name = "github.com/lib/pq"
|
||||||
packages = [
|
packages = [".","oid"]
|
||||||
".",
|
|
||||||
"oid"
|
|
||||||
]
|
|
||||||
revision = "61fe37aa2ee24fabcdbe5c4ac1d4ac566f88f345"
|
revision = "61fe37aa2ee24fabcdbe5c4ac1d4ac566f88f345"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
@ -337,11 +287,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/opentracing/opentracing-go"
|
name = "github.com/opentracing/opentracing-go"
|
||||||
packages = [
|
packages = [".","ext","log"]
|
||||||
".",
|
|
||||||
"ext",
|
|
||||||
"log"
|
|
||||||
]
|
|
||||||
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
||||||
version = "v1.0.2"
|
version = "v1.0.2"
|
||||||
|
|
||||||
@ -353,12 +299,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/prometheus/client_golang"
|
name = "github.com/prometheus/client_golang"
|
||||||
packages = [
|
packages = ["api","api/prometheus/v1","prometheus","prometheus/promhttp"]
|
||||||
"api",
|
|
||||||
"api/prometheus/v1",
|
|
||||||
"prometheus",
|
|
||||||
"prometheus/promhttp"
|
|
||||||
]
|
|
||||||
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
||||||
version = "v0.9.0-pre1"
|
version = "v0.9.0-pre1"
|
||||||
|
|
||||||
@ -371,22 +312,13 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/prometheus/common"
|
name = "github.com/prometheus/common"
|
||||||
packages = [
|
packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"]
|
||||||
"expfmt",
|
|
||||||
"internal/bitbucket.org/ww/goautoneg",
|
|
||||||
"model"
|
|
||||||
]
|
|
||||||
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
|
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/prometheus/procfs"
|
name = "github.com/prometheus/procfs"
|
||||||
packages = [
|
packages = [".","internal/util","nfsd","xfs"]
|
||||||
".",
|
|
||||||
"internal/util",
|
|
||||||
"nfsd",
|
|
||||||
"xfs"
|
|
||||||
]
|
|
||||||
revision = "85fadb6e89903ef7cca6f6a804474cd5ea85b6e1"
|
revision = "85fadb6e89903ef7cca6f6a804474cd5ea85b6e1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
@ -403,21 +335,13 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/smartystreets/assertions"
|
name = "github.com/smartystreets/assertions"
|
||||||
packages = [
|
packages = [".","internal/go-render/render","internal/oglematchers"]
|
||||||
".",
|
|
||||||
"internal/go-render/render",
|
|
||||||
"internal/oglematchers"
|
|
||||||
]
|
|
||||||
revision = "0b37b35ec7434b77e77a4bb29b79677cced992ea"
|
revision = "0b37b35ec7434b77e77a4bb29b79677cced992ea"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/smartystreets/goconvey"
|
name = "github.com/smartystreets/goconvey"
|
||||||
packages = [
|
packages = ["convey","convey/gotest","convey/reporting"]
|
||||||
"convey",
|
|
||||||
"convey/gotest",
|
|
||||||
"convey/reporting"
|
|
||||||
]
|
|
||||||
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
|
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
|
||||||
version = "1.6.3"
|
version = "1.6.3"
|
||||||
|
|
||||||
@ -429,21 +353,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/uber/jaeger-client-go"
|
name = "github.com/uber/jaeger-client-go"
|
||||||
packages = [
|
packages = [".","config","internal/baggage","internal/baggage/remote","internal/spanlog","log","rpcmetrics","thrift-gen/agent","thrift-gen/baggage","thrift-gen/jaeger","thrift-gen/sampling","thrift-gen/zipkincore","utils"]
|
||||||
".",
|
|
||||||
"config",
|
|
||||||
"internal/baggage",
|
|
||||||
"internal/baggage/remote",
|
|
||||||
"internal/spanlog",
|
|
||||||
"log",
|
|
||||||
"rpcmetrics",
|
|
||||||
"thrift-gen/agent",
|
|
||||||
"thrift-gen/baggage",
|
|
||||||
"thrift-gen/jaeger",
|
|
||||||
"thrift-gen/sampling",
|
|
||||||
"thrift-gen/zipkincore",
|
|
||||||
"utils"
|
|
||||||
]
|
|
||||||
revision = "3ac96c6e679cb60a74589b0d0aa7c70a906183f7"
|
revision = "3ac96c6e679cb60a74589b0d0aa7c70a906183f7"
|
||||||
version = "v2.11.2"
|
version = "v2.11.2"
|
||||||
|
|
||||||
@ -455,10 +365,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/yudai/gojsondiff"
|
name = "github.com/yudai/gojsondiff"
|
||||||
packages = [
|
packages = [".","formatter"]
|
||||||
".",
|
|
||||||
"formatter"
|
|
||||||
]
|
|
||||||
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
|
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
@ -471,37 +378,19 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
packages = [
|
packages = ["md4","pbkdf2"]
|
||||||
"md4",
|
|
||||||
"pbkdf2"
|
|
||||||
]
|
|
||||||
revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b"
|
revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/net"
|
name = "golang.org/x/net"
|
||||||
packages = [
|
packages = ["context","context/ctxhttp","http2","http2/hpack","idna","internal/timeseries","lex/httplex","trace"]
|
||||||
"context",
|
|
||||||
"context/ctxhttp",
|
|
||||||
"http2",
|
|
||||||
"http2/hpack",
|
|
||||||
"idna",
|
|
||||||
"internal/timeseries",
|
|
||||||
"lex/httplex",
|
|
||||||
"trace"
|
|
||||||
]
|
|
||||||
revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec"
|
revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/oauth2"
|
name = "golang.org/x/oauth2"
|
||||||
packages = [
|
packages = [".","google","internal","jws","jwt"]
|
||||||
".",
|
|
||||||
"google",
|
|
||||||
"internal",
|
|
||||||
"jws",
|
|
||||||
"jwt"
|
|
||||||
]
|
|
||||||
revision = "b28fcf2b08a19742b43084fb40ab78ac6c3d8067"
|
revision = "b28fcf2b08a19742b43084fb40ab78ac6c3d8067"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
@ -519,39 +408,12 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/text"
|
name = "golang.org/x/text"
|
||||||
packages = [
|
packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
|
||||||
"collate",
|
|
||||||
"collate/build",
|
|
||||||
"internal/colltab",
|
|
||||||
"internal/gen",
|
|
||||||
"internal/tag",
|
|
||||||
"internal/triegen",
|
|
||||||
"internal/ucd",
|
|
||||||
"language",
|
|
||||||
"secure/bidirule",
|
|
||||||
"transform",
|
|
||||||
"unicode/bidi",
|
|
||||||
"unicode/cldr",
|
|
||||||
"unicode/norm",
|
|
||||||
"unicode/rangetable"
|
|
||||||
]
|
|
||||||
revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3"
|
revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "google.golang.org/appengine"
|
name = "google.golang.org/appengine"
|
||||||
packages = [
|
packages = [".","cloudsql","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||||
".",
|
|
||||||
"cloudsql",
|
|
||||||
"internal",
|
|
||||||
"internal/app_identity",
|
|
||||||
"internal/base",
|
|
||||||
"internal/datastore",
|
|
||||||
"internal/log",
|
|
||||||
"internal/modules",
|
|
||||||
"internal/remote_api",
|
|
||||||
"internal/urlfetch",
|
|
||||||
"urlfetch"
|
|
||||||
]
|
|
||||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
@ -563,32 +425,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "google.golang.org/grpc"
|
name = "google.golang.org/grpc"
|
||||||
packages = [
|
packages = [".","balancer","balancer/base","balancer/roundrobin","codes","connectivity","credentials","encoding","grpclb/grpc_lb_v1/messages","grpclog","health","health/grpc_health_v1","internal","keepalive","metadata","naming","peer","resolver","resolver/dns","resolver/passthrough","stats","status","tap","transport"]
|
||||||
".",
|
|
||||||
"balancer",
|
|
||||||
"balancer/base",
|
|
||||||
"balancer/roundrobin",
|
|
||||||
"codes",
|
|
||||||
"connectivity",
|
|
||||||
"credentials",
|
|
||||||
"encoding",
|
|
||||||
"grpclb/grpc_lb_v1/messages",
|
|
||||||
"grpclog",
|
|
||||||
"health",
|
|
||||||
"health/grpc_health_v1",
|
|
||||||
"internal",
|
|
||||||
"keepalive",
|
|
||||||
"metadata",
|
|
||||||
"naming",
|
|
||||||
"peer",
|
|
||||||
"resolver",
|
|
||||||
"resolver/dns",
|
|
||||||
"resolver/passthrough",
|
|
||||||
"stats",
|
|
||||||
"status",
|
|
||||||
"tap",
|
|
||||||
"transport"
|
|
||||||
]
|
|
||||||
revision = "6b51017f791ae1cfbec89c52efdf444b13b550ef"
|
revision = "6b51017f791ae1cfbec89c52efdf444b13b550ef"
|
||||||
version = "v1.9.2"
|
version = "v1.9.2"
|
||||||
|
|
||||||
@ -610,12 +447,6 @@
|
|||||||
revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
|
revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
|
||||||
version = "v1"
|
version = "v1"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "gopkg.in/gomail.v2"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "81ebce5c23dfd25c6c67194b37d3dd3f338c98b1"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "gopkg.in/ini.v1"
|
name = "gopkg.in/ini.v1"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
@ -628,6 +459,12 @@
|
|||||||
revision = "75f2e9b42e99652f0d82b28ccb73648f44615faa"
|
revision = "75f2e9b42e99652f0d82b28ccb73648f44615faa"
|
||||||
version = "v1.2.4"
|
version = "v1.2.4"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "v2"
|
||||||
|
name = "gopkg.in/mail.v2"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "gopkg.in/redis.v2"
|
name = "gopkg.in/redis.v2"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
@ -643,6 +480,6 @@
|
|||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "8a9e651fb8ea49dfd3c6ddc99bd3242b39e453ea9edd11321da79bd2c865e9d1"
|
inputs-digest = "ad3c71fd3244369c313978e9e7464c7116faee764386439a17de0707a08103aa"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
@ -172,7 +172,7 @@ ignored = [
|
|||||||
name = "golang.org/x/sync"
|
name = "golang.org/x/sync"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "gopkg.in/gomail.v2"
|
name = "gopkg.in/mail.v2"
|
||||||
branch = "v2"
|
branch = "v2"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
|
@ -9,6 +9,7 @@ upgrading Grafana please check here before creating an issue.
|
|||||||
- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
|
- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
|
||||||
- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
|
- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
|
||||||
- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
|
- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
|
||||||
|
- [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack)
|
||||||
|
|
||||||
## Changes in v4.6
|
## Changes in v4.6
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ EXPOSE 389
|
|||||||
VOLUME ["/etc/ldap", "/var/lib/ldap"]
|
VOLUME ["/etc/ldap", "/var/lib/ldap"]
|
||||||
|
|
||||||
COPY modules/ /etc/ldap.dist/modules
|
COPY modules/ /etc/ldap.dist/modules
|
||||||
|
COPY prepopulate/ /etc/ldap.dist/prepopulate
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$SLAPD_ADDITIONAL_SCHEMAS" ]]; then
|
if [[ -n "$SLAPD_ADDITIONAL_SCHEMAS" ]]; then
|
||||||
IFS=","; declare -a schemas=($SLAPD_ADDITIONAL_SCHEMAS)
|
IFS=","; declare -a schemas=($SLAPD_ADDITIONAL_SCHEMAS); unset IFS
|
||||||
|
|
||||||
for schema in "${schemas[@]}"; do
|
for schema in "${schemas[@]}"; do
|
||||||
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/schema/${schema}.ldif" >/dev/null 2>&1
|
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/schema/${schema}.ldif" >/dev/null 2>&1
|
||||||
@ -73,14 +73,18 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$SLAPD_ADDITIONAL_MODULES" ]]; then
|
if [[ -n "$SLAPD_ADDITIONAL_MODULES" ]]; then
|
||||||
IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES)
|
IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES); unset IFS
|
||||||
|
|
||||||
for module in "${modules[@]}"; do
|
for module in "${modules[@]}"; do
|
||||||
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
|
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R openldap:openldap /etc/ldap/slapd.d/
|
for file in `ls /etc/ldap/prepopulate/*.ldif`; do
|
||||||
|
slapadd -F /etc/ldap/slapd.d -l "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
chown -R openldap:openldap /etc/ldap/slapd.d/ /var/lib/ldap/ /var/run/slapd/
|
||||||
else
|
else
|
||||||
slapd_configs_in_env=`env | grep 'SLAPD_'`
|
slapd_configs_in_env=`env | grep 'SLAPD_'`
|
||||||
|
|
||||||
|
13
docker/blocks/openldap/notes.md
Normal file
13
docker/blocks/openldap/notes.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Notes on OpenLdap Docker Block
|
||||||
|
|
||||||
|
Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database.
|
||||||
|
|
||||||
|
The ldif files add three users, `ldapviewer`, `ldapeditor` and `ldapadmin`. Two groups, `admins` and `users`, are added that correspond with the group mappings in the default conf/ldap.toml. `ldapadmin` is a member of `admins` and `ldapeditor` is a member of `users`.
|
||||||
|
|
||||||
|
Note that users that are added here need to specify a `memberOf` attribute manually as well as the `member` attribute for the group. The `memberOf` module usually does this automatically (if you add a group in Apache Directory Studio for example) but this does not work in the entrypoint script as it uses the `slapadd` command to add entries before the server has started and before the `memberOf` module is loaded.
|
||||||
|
|
||||||
|
After adding ldif files to `prepopulate`:
|
||||||
|
|
||||||
|
1. Remove your current docker image: `docker rm docker_openldap_1`
|
||||||
|
2. Build: `docker-compose build`
|
||||||
|
3. `docker-compose up`
|
10
docker/blocks/openldap/prepopulate/admin.ldif
Normal file
10
docker/blocks/openldap/prepopulate/admin.ldif
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
dn: cn=ldapadmin,dc=grafana,dc=org
|
||||||
|
mail: ldapadmin@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
sn: ldapadmin
|
||||||
|
cn: ldapadmin
|
||||||
|
memberOf: cn=admins,dc=grafana,dc=org
|
5
docker/blocks/openldap/prepopulate/adminsgroup.ldif
Normal file
5
docker/blocks/openldap/prepopulate/adminsgroup.ldif
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dn: cn=admins,dc=grafana,dc=org
|
||||||
|
cn: admins
|
||||||
|
member: cn=ldapadmin,dc=grafana,dc=org
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
10
docker/blocks/openldap/prepopulate/editor.ldif
Normal file
10
docker/blocks/openldap/prepopulate/editor.ldif
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
dn: cn=ldapeditor,dc=grafana,dc=org
|
||||||
|
mail: ldapeditor@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
sn: ldapeditor
|
||||||
|
cn: ldapeditor
|
||||||
|
memberOf: cn=users,dc=grafana,dc=org
|
5
docker/blocks/openldap/prepopulate/usersgroup.ldif
Normal file
5
docker/blocks/openldap/prepopulate/usersgroup.ldif
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dn: cn=users,dc=grafana,dc=org
|
||||||
|
cn: users
|
||||||
|
member: cn=ldapeditor,dc=grafana,dc=org
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
9
docker/blocks/openldap/prepopulate/viewer.ldif
Normal file
9
docker/blocks/openldap/prepopulate/viewer.ldif
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
dn: cn=ldapviewer,dc=grafana,dc=org
|
||||||
|
mail: ldapviewer@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
sn: ldapviewer
|
||||||
|
cn: ldapviewer
|
@ -41,6 +41,8 @@ Grafana ships with the following set of notification types:
|
|||||||
To enable email notifications you have to setup [SMTP settings](/installation/configuration/#smtp)
|
To enable email notifications you have to setup [SMTP settings](/installation/configuration/#smtp)
|
||||||
in the Grafana config. Email notifications will upload an image of the alert graph to an
|
in the Grafana config. Email notifications will upload an image of the alert graph to an
|
||||||
external image destination if available or fallback to attaching the image to the email.
|
external image destination if available or fallback to attaching the image to the email.
|
||||||
|
Be aware that if you use the `local` image storage email servers and clients might not be
|
||||||
|
able to access the image.
|
||||||
|
|
||||||
### Slack
|
### Slack
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ Hit `?` on your keyboard to open the shortcuts help modal.
|
|||||||
- `e` Toggle panel edit view
|
- `e` Toggle panel edit view
|
||||||
- `v` Toggle panel fullscreen view
|
- `v` Toggle panel fullscreen view
|
||||||
- `p` `s` Open Panel Share Modal
|
- `p` `s` Open Panel Share Modal
|
||||||
|
- `p` `d` Duplicate Panel
|
||||||
- `p` `r` Remove Panel
|
- `p` `r` Remove Panel
|
||||||
|
|
||||||
### Time Range
|
### Time Range
|
||||||
|
@ -15,7 +15,7 @@ weight = 1
|
|||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Stable for Debian-based Linux | [grafana_5.0.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb)
|
Stable for Debian-based Linux | [grafana_5.0.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb)
|
||||||
|
|
||||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||||
installation.
|
installation.
|
||||||
@ -24,9 +24,9 @@ installation.
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb
|
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb
|
||||||
sudo apt-get install -y adduser libfontconfig
|
sudo apt-get install -y adduser libfontconfig
|
||||||
sudo dpkg -i grafana_5.0.3_amd64.deb
|
sudo dpkg -i grafana_5.0.4_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
## APT Repository
|
## APT Repository
|
||||||
|
@ -15,7 +15,7 @@ weight = 2
|
|||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm)
|
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm)
|
||||||
|
|
||||||
|
|
||||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||||
@ -26,7 +26,7 @@ installation.
|
|||||||
You can install Grafana using Yum directly.
|
You can install Grafana using Yum directly.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm
|
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install manually using `rpm`.
|
Or install manually using `rpm`.
|
||||||
@ -34,15 +34,15 @@ Or install manually using `rpm`.
|
|||||||
#### On CentOS / Fedora / Redhat:
|
#### On CentOS / Fedora / Redhat:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm
|
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm
|
||||||
$ sudo yum install initscripts fontconfig
|
$ sudo yum install initscripts fontconfig
|
||||||
$ sudo rpm -Uvh grafana-5.0.3-1.x86_64.rpm
|
$ sudo rpm -Uvh grafana-5.0.4-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
#### On OpenSuse:
|
#### On OpenSuse:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo rpm -i --nodeps grafana-5.0.3-1.x86_64.rpm
|
$ sudo rpm -i --nodeps grafana-5.0.4-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install via YUM Repository
|
## Install via YUM Repository
|
||||||
@ -52,7 +52,7 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
|
|||||||
```bash
|
```bash
|
||||||
[grafana]
|
[grafana]
|
||||||
name=grafana
|
name=grafana
|
||||||
baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch
|
baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
|
||||||
repo_gpgcheck=1
|
repo_gpgcheck=1
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=1
|
gpgcheck=1
|
||||||
@ -64,7 +64,7 @@ sslcacert=/etc/pki/tls/certs/ca-bundle.crt
|
|||||||
There is also a testing repository if you want beta or release candidates.
|
There is also a testing repository if you want beta or release candidates.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch
|
baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch
|
||||||
```
|
```
|
||||||
|
|
||||||
Then install Grafana via the `yum` command.
|
Then install Grafana via the `yum` command.
|
||||||
|
@ -23,7 +23,7 @@ Before upgrading it can be a good idea to backup your Grafana database. This wil
|
|||||||
|
|
||||||
#### sqlite
|
#### sqlite
|
||||||
|
|
||||||
If you use sqlite you only need to make a backup of you `grafana.db` file. This is usually located at `/var/lib/grafana/grafana.db` on unix system.
|
If you use sqlite you only need to make a backup of your `grafana.db` file. This is usually located at `/var/lib/grafana/grafana.db` on unix system.
|
||||||
If you are unsure what database you use and where it is stored check you grafana configuration file. If you
|
If you are unsure what database you use and where it is stored check you grafana configuration file. If you
|
||||||
installed grafana to custom location using a binary tar/zip it is usally in `<grafana_install_dir>/data`.
|
installed grafana to custom location using a binary tar/zip it is usally in `<grafana_install_dir>/data`.
|
||||||
|
|
||||||
|
@ -8,12 +8,11 @@ parent = "installation"
|
|||||||
weight = 3
|
weight = 3
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
|
||||||
# Installing on Windows
|
# Installing on Windows
|
||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Latest stable package for Windows | [grafana-5.0.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3.windows-x64.zip)
|
Latest stable package for Windows | [grafana-5.0.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4.windows-x64.zip)
|
||||||
|
|
||||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||||
installation.
|
installation.
|
||||||
|
@ -71,13 +71,13 @@ Each field in the dashboard JSON is explained below with its usage:
|
|||||||
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
||||||
| **templating** | templating metadata, see [templating section](#templating) for details |
|
| **templating** | templating metadata, see [templating section](#templating) for details |
|
||||||
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
||||||
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema |
|
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
|
||||||
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
|
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
|
||||||
| **panels** | panels array, see below for detail. |
|
| **panels** | panels array, see below for detail. |
|
||||||
|
|
||||||
## Panels
|
## Panels
|
||||||
|
|
||||||
Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON of a text panel.
|
Panels are the building blocks of a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depend on the panel type. Following is an example of panel JSON of a text panel.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"panels": [
|
"panels": [
|
||||||
@ -105,7 +105,7 @@ The gridPos property describes the panel size and position in grid coordinates.
|
|||||||
- `x` The x position, in same unit as `w`.
|
- `x` The x position, in same unit as `w`.
|
||||||
- `y` The y position, in same unit as `h`.
|
- `y` The y position, in same unit as `h`.
|
||||||
|
|
||||||
The grid has a negative gravity that moves panels up if there i empty space above a panel.
|
The grid has a negative gravity that moves panels up if there is empty space above a panel.
|
||||||
|
|
||||||
### timepicker
|
### timepicker
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ Usage of the fields is explained below:
|
|||||||
|
|
||||||
### templating
|
### templating
|
||||||
|
|
||||||
`templating` fields contains array of template variables with their saved values along with some other metadata, for example:
|
The `templating` field contains an array of template variables with their saved values along with some other metadata, for example:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"templating": {
|
"templating": {
|
||||||
@ -236,7 +236,7 @@ Usage of the above mentioned fields in the templating section is explained below
|
|||||||
| Name | Usage |
|
| Name | Usage |
|
||||||
| ---- | ----- |
|
| ---- | ----- |
|
||||||
| **enable** | whether templating is enabled or not |
|
| **enable** | whether templating is enabled or not |
|
||||||
| **list** | an array of objects representing, each representing one template variable |
|
| **list** | an array of objects each representing one template variable |
|
||||||
| **allFormat** | format to use while fetching all values from datasource, eg: `wildcard`, `glob`, `regex`, `pipe`, etc. |
|
| **allFormat** | format to use while fetching all values from datasource, eg: `wildcard`, `glob`, `regex`, `pipe`, etc. |
|
||||||
| **current** | shows current selected variable text/value on the dashboard |
|
| **current** | shows current selected variable text/value on the dashboard |
|
||||||
| **datasource** | shows datasource for the variables |
|
| **datasource** | shows datasource for the variables |
|
||||||
|
@ -174,6 +174,8 @@ Interpolating a variable with multiple values selected is tricky as it is not st
|
|||||||
is valid in the given context where the variable is used. Grafana tries to solve this by allowing each data source plugin to
|
is valid in the given context where the variable is used. Grafana tries to solve this by allowing each data source plugin to
|
||||||
inform the templating interpolation engine what format to use for multiple values.
|
inform the templating interpolation engine what format to use for multiple values.
|
||||||
|
|
||||||
|
Note that the *Custom all value* option on the variable will have to be left blank for Grafana to format all values into a single string.
|
||||||
|
|
||||||
**Graphite**, for example, uses glob expressions. A variable with multiple values would, in this case, be interpolated as `{host1,host2,host3}` if
|
**Graphite**, for example, uses glob expressions. A variable with multiple values would, in this case, be interpolated as `{host1,host2,host3}` if
|
||||||
the current variable value was *host1*, *host2* and *host3*.
|
the current variable value was *host1*, *host2* and *host3*.
|
||||||
|
|
||||||
|
@ -104,10 +104,10 @@
|
|||||||
"test": "grunt test",
|
"test": "grunt test",
|
||||||
"test:coverage": "grunt test --coverage=true",
|
"test:coverage": "grunt test --coverage=true",
|
||||||
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
||||||
"karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
|
"karma": "grunt karma:dev",
|
||||||
"jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
|
"jest": "jest --notify --watch",
|
||||||
"api-tests": "node ./node_modules/jest-cli/bin/jest.js --notify --watch --config=tests/api/jest.js",
|
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||||
"precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
|
"precommit": "lint-staged && grunt precommit"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx}": [
|
"*.{ts,tsx}": [
|
||||||
|
@ -50,6 +50,10 @@ type UserStars struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetGravatarUrl(text string) string {
|
func GetGravatarUrl(text string) string {
|
||||||
|
if setting.DisableGravatar {
|
||||||
|
return "/public/img/user_profile.png"
|
||||||
|
}
|
||||||
|
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ func (g *GrafanaServerImpl) initLogging() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Error(err.Error())
|
fmt.Fprintf(os.Stderr, "Failed to start grafana. error: %s\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if auth proxy ip(s) defined, check if request comes from one of those
|
// if auth proxy ip(s) defined, check if request comes from one of those
|
||||||
if err := checkAuthenticationProxy(ctx, proxyHeaderValue); err != nil {
|
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
|
||||||
ctx.Handle(407, "Proxy authentication required", err)
|
ctx.Handle(407, "Proxy authentication required", err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -123,30 +123,26 @@ var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUs
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
|
func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
|
||||||
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
|
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
|
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
|
||||||
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
|
sourceIP, _, err := net.SplitHostPort(remoteAddr)
|
||||||
sourceIP := remoteAddrSplit[0]
|
if err != nil {
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, proxyIP := range proxies {
|
|
||||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
|
|
||||||
err := errors.New(msg)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare allowed IP addresses to actual address
|
||||||
|
for _, proxyIP := range proxies {
|
||||||
|
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
|
||||||
|
}
|
||||||
|
|
||||||
func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
|
func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
|
||||||
query := m.GetSignedInUserQuery{}
|
query := m.GetSignedInUserQuery{}
|
||||||
|
@ -226,11 +226,11 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
middlewareScenario("When auth_proxy is enabled and request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
||||||
setting.AuthProxyEnabled = true
|
setting.AuthProxyEnabled = true
|
||||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
setting.AuthProxyHeaderProperty = "username"
|
setting.AuthProxyHeaderProperty = "username"
|
||||||
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
|
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
|
||||||
|
|
||||||
sc.fakeReq("GET", "/")
|
sc.fakeReq("GET", "/")
|
||||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||||
@ -239,6 +239,24 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
|
|
||||||
Convey("should return 407 status code", func() {
|
Convey("should return 407 status code", func() {
|
||||||
So(sc.resp.Code, ShouldEqual, 407)
|
So(sc.resp.Code, ShouldEqual, 407)
|
||||||
|
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.3.1 is not from the authentication proxy")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
||||||
|
setting.AuthProxyEnabled = true
|
||||||
|
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
|
setting.AuthProxyHeaderProperty = "username"
|
||||||
|
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
|
||||||
|
|
||||||
|
sc.fakeReq("GET", "/")
|
||||||
|
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||||
|
sc.req.RemoteAddr = "[2001:23]:12345"
|
||||||
|
sc.exec()
|
||||||
|
|
||||||
|
Convey("should return 407 status code", func() {
|
||||||
|
So(sc.resp.Code, ShouldEqual, 407)
|
||||||
|
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 2001:23 is not from the authentication proxy")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -246,7 +264,7 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.AuthProxyEnabled = true
|
setting.AuthProxyEnabled = true
|
||||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
setting.AuthProxyHeaderProperty = "username"
|
setting.AuthProxyHeaderProperty = "username"
|
||||||
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
|
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||||
@ -255,7 +273,7 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
|
|
||||||
sc.fakeReq("GET", "/")
|
sc.fakeReq("GET", "/")
|
||||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||||
sc.req.RemoteAddr = "192.168.2.1:12345"
|
sc.req.RemoteAddr = "[2001::23]:12345"
|
||||||
sc.exec()
|
sc.exec()
|
||||||
|
|
||||||
Convey("Should init context with user info", func() {
|
Convey("Should init context with user info", func() {
|
||||||
|
@ -211,14 +211,14 @@ func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string {
|
|||||||
return GetDashboardUrl(uid, slug)
|
return GetDashboardUrl(uid, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the html url for a dashboard
|
// GetDashboardUrl return the html url for a dashboard
|
||||||
func GetDashboardUrl(uid string, slug string) string {
|
func GetDashboardUrl(uid string, slug string) string {
|
||||||
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
|
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the full url for a dashboard
|
// GetFullDashboardUrl return the full url for a dashboard
|
||||||
func GetFullDashboardUrl(uid string, slug string) string {
|
func GetFullDashboardUrl(uid string, slug string) string {
|
||||||
return fmt.Sprintf("%s%s", setting.AppUrl, GetDashboardUrl(uid, slug))
|
return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFolderUrl return the html url for a folder
|
// GetFolderUrl return the html url for a folder
|
||||||
|
@ -4,11 +4,24 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDashboardModel(t *testing.T) {
|
func TestDashboardModel(t *testing.T) {
|
||||||
|
|
||||||
|
Convey("Generate full dashboard url", t, func() {
|
||||||
|
setting.AppUrl = "http://grafana.local/"
|
||||||
|
fullUrl := GetFullDashboardUrl("uid", "my-dashboard")
|
||||||
|
So(fullUrl, ShouldEqual, "http://grafana.local/d/uid/my-dashboard")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Generate relative dashboard url", t, func() {
|
||||||
|
setting.AppUrl = ""
|
||||||
|
fullUrl := GetDashboardUrl("uid", "my-dashboard")
|
||||||
|
So(fullUrl, ShouldEqual, "/d/uid/my-dashboard")
|
||||||
|
})
|
||||||
|
|
||||||
Convey("When generating slug", t, func() {
|
Convey("When generating slug", t, func() {
|
||||||
dashboard := NewDashboard("Grafana Play Home")
|
dashboard := NewDashboard("Grafana Play Home")
|
||||||
dashboard.UpdateSlug()
|
dashboard.UpdateSlug()
|
||||||
|
@ -13,11 +13,7 @@ func init() {
|
|||||||
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||||
|
|
||||||
if _, err := extractor.GetAlerts(); err != nil {
|
return extractor.ValidateAlerts()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||||
@ -29,15 +25,12 @@ func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
|||||||
|
|
||||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||||
|
|
||||||
if alerts, err := extractor.GetAlerts(); err != nil {
|
alerts, err := extractor.GetAlerts()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
}
|
||||||
|
|
||||||
saveAlerts.Alerts = alerts
|
saveAlerts.Alerts = alerts
|
||||||
}
|
|
||||||
|
|
||||||
if err := bus.Dispatch(&saveAlerts); err != nil {
|
return bus.Dispatch(&saveAlerts)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -11,76 +11,78 @@ import (
|
|||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DashAlertExtractor extracts alerts from the dashboard json
|
||||||
type DashAlertExtractor struct {
|
type DashAlertExtractor struct {
|
||||||
Dash *m.Dashboard
|
Dash *m.Dashboard
|
||||||
OrgId int64
|
OrgID int64
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
|
// NewDashAlertExtractor returns a new DashAlertExtractor
|
||||||
|
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
|
||||||
return &DashAlertExtractor{
|
return &DashAlertExtractor{
|
||||||
Dash: dash,
|
Dash: dash,
|
||||||
OrgId: orgId,
|
OrgID: orgID,
|
||||||
log: log.New("alerting.extractor"),
|
log: log.New("alerting.extractor"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) {
|
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
|
||||||
if dsName == "" {
|
if dsName == "" {
|
||||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
|
query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
|
||||||
if err := bus.Dispatch(query); err != nil {
|
if err := bus.Dispatch(query); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
}
|
||||||
|
|
||||||
for _, ds := range query.Result {
|
for _, ds := range query.Result {
|
||||||
if ds.IsDefault {
|
if ds.IsDefault {
|
||||||
return ds, nil
|
return ds, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
|
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
|
||||||
if err := bus.Dispatch(query); err != nil {
|
if err := bus.Dispatch(query); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
|
||||||
return query.Result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return query.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("Could not find datasource id for " + dsName)
|
return nil, errors.New("Could not find datasource id for " + dsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json {
|
func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
|
||||||
for _, targetsObj := range panel.Get("targets").MustArray() {
|
for _, targetsObj := range panel.Get("targets").MustArray() {
|
||||||
target := simplejson.NewFromAny(targetsObj)
|
target := simplejson.NewFromAny(targetsObj)
|
||||||
|
|
||||||
if target.Get("refId").MustString() == refId {
|
if target.Get("refId").MustString() == refID {
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
|
func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
|
||||||
rawJson, err := in.MarshalJSON()
|
rawJSON, err := in.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return simplejson.NewJson(rawJson)
|
return simplejson.NewJson(rawJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
|
func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
|
||||||
alerts := make([]*m.Alert, 0)
|
alerts := make([]*m.Alert, 0)
|
||||||
|
|
||||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||||
panel := simplejson.NewFromAny(panelObj)
|
panel := simplejson.NewFromAny(panelObj)
|
||||||
|
|
||||||
collapsedJson, collapsed := panel.CheckGet("collapsed")
|
collapsedJSON, collapsed := panel.CheckGet("collapsed")
|
||||||
// check if the panel is collapsed
|
// check if the panel is collapsed
|
||||||
if collapsed && collapsedJson.MustBool() {
|
if collapsed && collapsedJSON.MustBool() {
|
||||||
|
|
||||||
// extract alerts from sub panels for collapsed panels
|
// extract alerts from sub panels for collapsed panels
|
||||||
als, err := e.GetAlertFromPanels(panel)
|
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -95,7 +97,7 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
panelId, err := panel.Get("id").Int64()
|
panelID, err := panel.Get("id").Int64()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("panel id is required. err %v", err)
|
return nil, fmt.Errorf("panel id is required. err %v", err)
|
||||||
}
|
}
|
||||||
@ -113,8 +115,8 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
|
|
||||||
alert := &m.Alert{
|
alert := &m.Alert{
|
||||||
DashboardId: e.Dash.Id,
|
DashboardId: e.Dash.Id,
|
||||||
OrgId: e.OrgId,
|
OrgId: e.OrgID,
|
||||||
PanelId: panelId,
|
PanelId: panelID,
|
||||||
Id: jsonAlert.Get("id").MustInt64(),
|
Id: jsonAlert.Get("id").MustInt64(),
|
||||||
Name: jsonAlert.Get("name").MustString(),
|
Name: jsonAlert.Get("name").MustString(),
|
||||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||||
@ -126,11 +128,11 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
jsonCondition := simplejson.NewFromAny(condition)
|
jsonCondition := simplejson.NewFromAny(condition)
|
||||||
|
|
||||||
jsonQuery := jsonCondition.Get("query")
|
jsonQuery := jsonCondition.Get("query")
|
||||||
queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
|
queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
|
||||||
panelQuery := findPanelQueryByRefId(panel, queryRefId)
|
panelQuery := findPanelQueryByRefID(panel, queryRefID)
|
||||||
|
|
||||||
if panelQuery == nil {
|
if panelQuery == nil {
|
||||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
|
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
|
||||||
return nil, ValidationError{Reason: reason}
|
return nil, ValidationError{Reason: reason}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,12 +143,13 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
dsName = panel.Get("datasource").MustString()
|
dsName = panel.Get("datasource").MustString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if datasource, err := e.lookupDatasourceId(dsName); err != nil {
|
datasource, err := e.lookupDatasourceID(dsName)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
|
||||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||||
|
|
||||||
if interval, err := panel.Get("interval").String(); err == nil {
|
if interval, err := panel.Get("interval").String(); err == nil {
|
||||||
panelQuery.Set("interval", interval)
|
panelQuery.Set("interval", interval)
|
||||||
}
|
}
|
||||||
@ -162,21 +165,28 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if alert.ValidToSave() {
|
if !validateAlertFunc(alert) {
|
||||||
alerts = append(alerts, alert)
|
|
||||||
} else {
|
|
||||||
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
||||||
return nil, m.ErrDashboardContainsInvalidAlertData
|
return nil, m.ErrDashboardContainsInvalidAlertData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alerts = append(alerts, alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
return alerts, nil
|
return alerts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
func validateAlertRule(alert *m.Alert) bool {
|
||||||
e.log.Debug("GetAlerts")
|
return alert.ValidToSave()
|
||||||
|
}
|
||||||
|
|
||||||
dashboardJson, err := copyJson(e.Dash.Data)
|
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data
|
||||||
|
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||||
|
return e.extractAlerts(validateAlertRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *m.Alert) bool) ([]*m.Alert, error) {
|
||||||
|
dashboardJSON, err := copyJSON(e.Dash.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -185,11 +195,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
|||||||
|
|
||||||
// We extract alerts from rows to be backwards compatible
|
// We extract alerts from rows to be backwards compatible
|
||||||
// with the old dashboard json model.
|
// with the old dashboard json model.
|
||||||
rows := dashboardJson.Get("rows").MustArray()
|
rows := dashboardJSON.Get("rows").MustArray()
|
||||||
if len(rows) > 0 {
|
if len(rows) > 0 {
|
||||||
for _, rowObj := range rows {
|
for _, rowObj := range rows {
|
||||||
row := simplejson.NewFromAny(rowObj)
|
row := simplejson.NewFromAny(rowObj)
|
||||||
a, err := e.GetAlertFromPanels(row)
|
a, err := e.getAlertFromPanels(row, validateFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -197,7 +207,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
|||||||
alerts = append(alerts, a...)
|
alerts = append(alerts, a...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
a, err := e.GetAlertFromPanels(dashboardJson)
|
a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -208,3 +218,10 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
|||||||
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
||||||
return alerts, nil
|
return alerts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
|
||||||
|
// in the first validation pass
|
||||||
|
func (e *DashAlertExtractor) ValidateAlerts() error {
|
||||||
|
_, err := e.extractAlerts(func(alert *m.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 })
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -240,5 +240,26 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
So(len(alerts), ShouldEqual, 4)
|
So(len(alerts), ShouldEqual, 4)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||||
|
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dashJSON, err := simplejson.NewJson(json)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
dash := m.NewDashboardFromJson(dashJSON)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1)
|
||||||
|
|
||||||
|
err = extractor.ValidateAlerts()
|
||||||
|
|
||||||
|
Convey("Should validate without error", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should fail on save", func() {
|
||||||
|
_, err := extractor.GetAlerts()
|
||||||
|
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,11 @@ type NotifierBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||||
uploadImage := model.Get("uploadImage").MustBool(false)
|
uploadImage := true
|
||||||
|
value, exist := model.CheckGet("uploadImage")
|
||||||
|
if exist {
|
||||||
|
uploadImage = value.MustBool()
|
||||||
|
}
|
||||||
|
|
||||||
return NotifierBase{
|
return NotifierBase{
|
||||||
Id: id,
|
Id: id,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
@ -11,6 +12,29 @@ import (
|
|||||||
|
|
||||||
func TestBaseNotifier(t *testing.T) {
|
func TestBaseNotifier(t *testing.T) {
|
||||||
Convey("Base notifier tests", t, func() {
|
Convey("Base notifier tests", t, func() {
|
||||||
|
Convey("default constructor for notifiers", func() {
|
||||||
|
bJson := simplejson.New()
|
||||||
|
|
||||||
|
Convey("can parse false value", func() {
|
||||||
|
bJson.Set("uploadImage", false)
|
||||||
|
|
||||||
|
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||||
|
So(base.UploadImage, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("can parse true value", func() {
|
||||||
|
bJson.Set("uploadImage", true)
|
||||||
|
|
||||||
|
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||||
|
So(base.UploadImage, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("default value should be true for backwards compatibility", func() {
|
||||||
|
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||||
|
So(base.UploadImage, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("should notify", func() {
|
Convey("should notify", func() {
|
||||||
Convey("pending -> ok", func() {
|
Convey("pending -> ok", func() {
|
||||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||||
|
281
pkg/services/alerting/test-data/dash-without-id.json
Normal file
281
pkg/services/alerting/test-data/dash-without-id.json
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
{
|
||||||
|
"title": "Influxdb",
|
||||||
|
"tags": [
|
||||||
|
"apa"
|
||||||
|
],
|
||||||
|
"style": "dark",
|
||||||
|
"timezone": "browser",
|
||||||
|
"editable": true,
|
||||||
|
"hideControls": false,
|
||||||
|
"sharedCrosshair": false,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"height": "450px",
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
10
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"B",
|
||||||
|
"5m",
|
||||||
|
"now"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frequency": "3s",
|
||||||
|
"handler": 1,
|
||||||
|
"name": "Influxdb",
|
||||||
|
"noDataState": "no_data",
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"id": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"alerting": {},
|
||||||
|
"aliasColors": {
|
||||||
|
"logins.count.count": "#890F02"
|
||||||
|
},
|
||||||
|
"bars": false,
|
||||||
|
"datasource": "InfluxDB",
|
||||||
|
"editable": true,
|
||||||
|
"error": false,
|
||||||
|
"fill": 1,
|
||||||
|
"grid": {},
|
||||||
|
"id": 1,
|
||||||
|
"interval": ">10s",
|
||||||
|
"isNew": true,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 2,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "connected",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"span": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"$interval"
|
||||||
|
],
|
||||||
|
"type": "time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"datacenter"
|
||||||
|
],
|
||||||
|
"type": "tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"none"
|
||||||
|
],
|
||||||
|
"type": "fill"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hide": false,
|
||||||
|
"measurement": "logins.count",
|
||||||
|
"policy": "default",
|
||||||
|
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
|
||||||
|
"rawQuery": true,
|
||||||
|
"refId": "B",
|
||||||
|
"resultFormat": "time_series",
|
||||||
|
"select": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": [],
|
||||||
|
"type": "count"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"$interval"
|
||||||
|
],
|
||||||
|
"type": "time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"type": "fill"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hide": true,
|
||||||
|
"measurement": "cpu",
|
||||||
|
"policy": "default",
|
||||||
|
"refId": "A",
|
||||||
|
"resultFormat": "time_series",
|
||||||
|
"select": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": [],
|
||||||
|
"type": "mean"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"params": [
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": [],
|
||||||
|
"type": "sum"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"colorMode": "critical",
|
||||||
|
"fill": true,
|
||||||
|
"line": true,
|
||||||
|
"op": "gt",
|
||||||
|
"value": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Panel Title",
|
||||||
|
"tooltip": {
|
||||||
|
"msResolution": false,
|
||||||
|
"ordering": "alphabetical",
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "cumulative"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editable": true,
|
||||||
|
"error": false,
|
||||||
|
"id": 2,
|
||||||
|
"isNew": true,
|
||||||
|
"limit": 10,
|
||||||
|
"links": [],
|
||||||
|
"show": "current",
|
||||||
|
"span": 2,
|
||||||
|
"stateFilter": [
|
||||||
|
"alerting"
|
||||||
|
],
|
||||||
|
"title": "Alert status",
|
||||||
|
"type": "alertlist"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Row"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": {
|
||||||
|
"from": "now-5m",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"now": true,
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"schemaVersion": 13,
|
||||||
|
"version": 120,
|
||||||
|
"links": [],
|
||||||
|
"gnetId": null
|
||||||
|
}
|
@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"gopkg.in/gomail.v2"
|
gomail "gopkg.in/mail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mailQueue chan *Message
|
var mailQueue chan *Message
|
||||||
|
@ -170,8 +170,8 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dash.Dashboard.Id != 0 {
|
if dash.Dashboard.Id != 0 {
|
||||||
fr.log.Error("provisioned dashboard json files cannot contain id")
|
dash.Dashboard.Data.Set("id", nil)
|
||||||
return provisioningMetadata, nil
|
dash.Dashboard.Id = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if alreadyProvisioned {
|
if alreadyProvisioned {
|
||||||
|
@ -15,9 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultDashboards string = "./test-dashboards/folder-one"
|
defaultDashboards = "./test-dashboards/folder-one"
|
||||||
brokenDashboards string = "./test-dashboards/broken-dashboards"
|
brokenDashboards = "./test-dashboards/broken-dashboards"
|
||||||
oneDashboard string = "./test-dashboards/one-dashboard"
|
oneDashboard = "./test-dashboards/one-dashboard"
|
||||||
|
containingId = "./test-dashboards/containing-id"
|
||||||
|
|
||||||
fakeService *fakeDashboardProvisioningService
|
fakeService *fakeDashboardProvisioningService
|
||||||
)
|
)
|
||||||
@ -85,6 +86,18 @@ func TestDashboardFileReader(t *testing.T) {
|
|||||||
So(len(fakeService.inserted), ShouldEqual, 1)
|
So(len(fakeService.inserted), ShouldEqual, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Overrides id from dashboard.json files", func() {
|
||||||
|
cfg.Options["path"] = containingId
|
||||||
|
|
||||||
|
reader, err := NewDashboardFileReader(cfg, logger)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = reader.startWalkingDisk()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
So(len(fakeService.inserted), ShouldEqual, 1)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Invalid configuration should return error", func() {
|
Convey("Invalid configuration should return error", func() {
|
||||||
cfg := &DashboardsAsConfig{
|
cfg := &DashboardsAsConfig{
|
||||||
Name: "Default",
|
Name: "Default",
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"title": "Grafana1",
|
||||||
|
"tags": [],
|
||||||
|
"id": 3,
|
||||||
|
"style": "dark",
|
||||||
|
"timezone": "browser",
|
||||||
|
"editable": true,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"title": "New row",
|
||||||
|
"height": "150px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"span": 12,
|
||||||
|
"editable": true,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Welcome to"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nav": [
|
||||||
|
{
|
||||||
|
"type": "timepicker",
|
||||||
|
"collapse": false,
|
||||||
|
"enable": true,
|
||||||
|
"status": "Stable",
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
],
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"now": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"version": 5
|
||||||
|
}
|
@ -223,7 +223,7 @@ func shouldRedactURLKey(s string) bool {
|
|||||||
return strings.Contains(uppercased, "DATABASE_URL")
|
return strings.Contains(uppercased, "DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyEnvVariableOverrides() {
|
func applyEnvVariableOverrides() error {
|
||||||
appliedEnvOverrides = make([]string, 0)
|
appliedEnvOverrides = make([]string, 0)
|
||||||
for _, section := range Cfg.Sections() {
|
for _, section := range Cfg.Sections() {
|
||||||
for _, key := range section.Keys() {
|
for _, key := range section.Keys() {
|
||||||
@ -238,7 +238,10 @@ func applyEnvVariableOverrides() {
|
|||||||
envValue = "*********"
|
envValue = "*********"
|
||||||
}
|
}
|
||||||
if shouldRedactURLKey(envKey) {
|
if shouldRedactURLKey(envKey) {
|
||||||
u, _ := url.Parse(envValue)
|
u, err := url.Parse(envValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse environment variable. key: %s, value: %s. error: %v", envKey, envValue, err)
|
||||||
|
}
|
||||||
ui := u.User
|
ui := u.User
|
||||||
if ui != nil {
|
if ui != nil {
|
||||||
_, exists := ui.Password()
|
_, exists := ui.Password()
|
||||||
@ -252,6 +255,8 @@ func applyEnvVariableOverrides() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCommandLineDefaultProperties(props map[string]string) {
|
func applyCommandLineDefaultProperties(props map[string]string) {
|
||||||
@ -377,7 +382,7 @@ func loadSpecifedConfigFile(configFile string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfiguration(args *CommandLineArgs) {
|
func loadConfiguration(args *CommandLineArgs) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// load config defaults
|
// load config defaults
|
||||||
@ -395,7 +400,7 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(fmt.Sprintf("Failed to parse defaults.ini, %v", err))
|
fmt.Println(fmt.Sprintf("Failed to parse defaults.ini, %v", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Cfg.BlockMode = false
|
Cfg.BlockMode = false
|
||||||
@ -413,7 +418,10 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply environment overrides
|
// apply environment overrides
|
||||||
applyEnvVariableOverrides()
|
err = applyEnvVariableOverrides()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// apply command line overrides
|
// apply command line overrides
|
||||||
applyCommandLineProperties(commandLineProps)
|
applyCommandLineProperties(commandLineProps)
|
||||||
@ -424,6 +432,8 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
// update data path and logging config
|
// update data path and logging config
|
||||||
DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
|
DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
|
||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathExists(path string) bool {
|
func pathExists(path string) bool {
|
||||||
@ -471,7 +481,10 @@ func validateStaticRootPath() error {
|
|||||||
|
|
||||||
func NewConfigContext(args *CommandLineArgs) error {
|
func NewConfigContext(args *CommandLineArgs) error {
|
||||||
setHomePath(args)
|
setHomePath(args)
|
||||||
loadConfiguration(args)
|
err := loadConfiguration(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
||||||
InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
|
InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
|
||||||
|
@ -37,6 +37,13 @@ func TestLoadingSettings(t *testing.T) {
|
|||||||
So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********")
|
So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should return an error when url is invalid", func() {
|
||||||
|
os.Setenv("GF_DATABASE_URL", "postgres.%31://grafana:secret@postgres:5432/grafana")
|
||||||
|
err := NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should replace password in URL when url environment is defined", func() {
|
Convey("Should replace password in URL when url environment is defined", func() {
|
||||||
os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
|
os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
|
||||||
NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
||||||
|
@ -31,6 +31,7 @@ export class HelpCtrl {
|
|||||||
{ keys: ['e'], description: 'Toggle panel edit view' },
|
{ keys: ['e'], description: 'Toggle panel edit view' },
|
||||||
{ keys: ['v'], description: 'Toggle panel fullscreen view' },
|
{ keys: ['v'], description: 'Toggle panel fullscreen view' },
|
||||||
{ keys: ['p', 's'], description: 'Open Panel Share Modal' },
|
{ keys: ['p', 's'], description: 'Open Panel Share Modal' },
|
||||||
|
{ keys: ['p', 'd'], description: 'Duplicate Panel' },
|
||||||
{ keys: ['p', 'r'], description: 'Remove Panel' },
|
{ keys: ['p', 'r'], description: 'Remove Panel' },
|
||||||
],
|
],
|
||||||
'Time Range': [
|
'Time Range': [
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<div class="search-section__header" ng-show="section.hideHeader"></div>
|
<div class="search-section__header" ng-show="section.hideHeader"></div>
|
||||||
|
|
||||||
<div ng-if="section.expanded">
|
<div ng-if="section.expanded">
|
||||||
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
|
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
|
||||||
<div ng-click="ctrl.toggleSelection(item, $event)">
|
<div ng-click="ctrl.toggleSelection(item, $event)">
|
||||||
<gf-form-switch
|
<gf-form-switch
|
||||||
ng-show="ctrl.editable"
|
ng-show="ctrl.editable"
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
define([
|
|
||||||
'./alert_srv',
|
|
||||||
'./util_srv',
|
|
||||||
'./context_srv',
|
|
||||||
'./timer',
|
|
||||||
'./analytics',
|
|
||||||
'./popover_srv',
|
|
||||||
'./segment_srv',
|
|
||||||
'./backend_srv',
|
|
||||||
'./dynamic_directive_srv',
|
|
||||||
'./bridge_srv'
|
|
||||||
],
|
|
||||||
function () {});
|
|
10
public/app/core/services/all.ts
Normal file
10
public/app/core/services/all.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import './alert_srv';
|
||||||
|
import './util_srv';
|
||||||
|
import './context_srv';
|
||||||
|
import './timer';
|
||||||
|
import './analytics';
|
||||||
|
import './popover_srv';
|
||||||
|
import './segment_srv';
|
||||||
|
import './backend_srv';
|
||||||
|
import './dynamic_directive_srv';
|
||||||
|
import './bridge_srv';
|
@ -10,6 +10,7 @@ import 'mousetrap-global-bind';
|
|||||||
export class KeybindingSrv {
|
export class KeybindingSrv {
|
||||||
helpModal: boolean;
|
helpModal: boolean;
|
||||||
modalOpen = false;
|
modalOpen = false;
|
||||||
|
timepickerOpen = false;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope, private $location) {
|
constructor(private $rootScope, private $location) {
|
||||||
@ -22,6 +23,8 @@ export class KeybindingSrv {
|
|||||||
|
|
||||||
this.setupGlobal();
|
this.setupGlobal();
|
||||||
appEvents.on('show-modal', () => (this.modalOpen = true));
|
appEvents.on('show-modal', () => (this.modalOpen = true));
|
||||||
|
$rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
|
||||||
|
$rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGlobal() {
|
setupGlobal() {
|
||||||
@ -73,7 +76,12 @@ export class KeybindingSrv {
|
|||||||
appEvents.emit('hide-modal');
|
appEvents.emit('hide-modal');
|
||||||
|
|
||||||
if (!this.modalOpen) {
|
if (!this.modalOpen) {
|
||||||
|
if (this.timepickerOpen) {
|
||||||
|
this.$rootScope.appEvent('closeTimepicker');
|
||||||
|
this.timepickerOpen = false;
|
||||||
|
} else {
|
||||||
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
|
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.modalOpen = false;
|
this.modalOpen = false;
|
||||||
}
|
}
|
||||||
|
@ -447,6 +447,7 @@ kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr');
|
|||||||
kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
|
kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
|
||||||
kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
|
kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
|
||||||
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
|
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
|
||||||
|
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
|
||||||
|
|
||||||
// Data (Binary)
|
// Data (Binary)
|
||||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||||
@ -869,6 +870,7 @@ kbn.getUnitFormats = function() {
|
|||||||
{ text: 'Icelandic Króna (kr)', value: 'currencyISK' },
|
{ text: 'Icelandic Króna (kr)', value: 'currencyISK' },
|
||||||
{ text: 'Norwegian Krone (kr)', value: 'currencyNOK' },
|
{ text: 'Norwegian Krone (kr)', value: 'currencyNOK' },
|
||||||
{ text: 'Swedish Krona (kr)', value: 'currencySEK' },
|
{ text: 'Swedish Krona (kr)', value: 'currencySEK' },
|
||||||
|
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -43,6 +43,7 @@ export class AlertNotificationEditCtrl {
|
|||||||
return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then(result => {
|
return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then(result => {
|
||||||
this.navModel.breadcrumbs.push({ text: result.name });
|
this.navModel.breadcrumbs.push({ text: result.name });
|
||||||
this.navModel.node = { text: result.name };
|
this.navModel.node = { text: result.name };
|
||||||
|
result.settings = _.defaults(result.settings, this.defaults.settings);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -89,7 +90,7 @@ export class AlertNotificationEditCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typeChanged() {
|
typeChanged() {
|
||||||
this.model.settings = {};
|
this.model.settings = _.defaults({}, this.defaults.settings);
|
||||||
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
|
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
export class ThresholdMapper {
|
export class ThresholdMapper {
|
||||||
static alertToGraphThresholds(panel) {
|
static alertToGraphThresholds(panel) {
|
||||||
if (panel.type !== 'graph') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < panel.alert.conditions.length; i++) {
|
for (var i = 0; i < panel.alert.conditions.length; i++) {
|
||||||
let condition = panel.alert.conditions[i];
|
let condition = panel.alert.conditions[i];
|
||||||
if (condition.type !== 'query') {
|
if (condition.type !== 'query') {
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
define([
|
|
||||||
'./panellinks/module',
|
|
||||||
'./dashlinks/module',
|
|
||||||
'./annotations/all',
|
|
||||||
'./templating/all',
|
|
||||||
'./plugins/all',
|
|
||||||
'./dashboard/all',
|
|
||||||
'./playlist/all',
|
|
||||||
'./snapshot/all',
|
|
||||||
'./panel/all',
|
|
||||||
'./org/all',
|
|
||||||
'./admin/admin',
|
|
||||||
'./alerting/all',
|
|
||||||
'./styleguide/styleguide',
|
|
||||||
], function () {});
|
|
13
public/app/features/all.ts
Normal file
13
public/app/features/all.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import './panellinks/module';
|
||||||
|
import './dashlinks/module';
|
||||||
|
import './annotations/all';
|
||||||
|
import './templating/all';
|
||||||
|
import './plugins/all';
|
||||||
|
import './dashboard/all';
|
||||||
|
import './playlist/all';
|
||||||
|
import './snapshot/all';
|
||||||
|
import './panel/all';
|
||||||
|
import './org/all';
|
||||||
|
import './admin/admin';
|
||||||
|
import './alerting/all';
|
||||||
|
import './styleguide/styleguide';
|
@ -22,7 +22,6 @@ export class TimePickerCtrl {
|
|||||||
refresh: any;
|
refresh: any;
|
||||||
isUtc: boolean;
|
isUtc: boolean;
|
||||||
firstDayOfWeek: number;
|
firstDayOfWeek: number;
|
||||||
closeDropdown: any;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -32,6 +31,7 @@ export class TimePickerCtrl {
|
|||||||
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
||||||
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
||||||
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
|
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
|
||||||
|
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
|
||||||
|
|
||||||
// init options
|
// init options
|
||||||
this.panel = this.dashboard.timepicker;
|
this.panel = this.dashboard.timepicker;
|
||||||
@ -96,7 +96,7 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
openDropdown() {
|
openDropdown() {
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
this.isOpen = false;
|
this.closeDropdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +112,12 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
this.refresh.options.unshift({ text: 'off' });
|
this.refresh.options.unshift({ text: 'off' });
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
|
this.$rootScope.appEvent('timepickerOpen');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.$rootScope.appEvent('timepickerClosed');
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCustom() {
|
applyCustom() {
|
||||||
@ -120,7 +126,7 @@ export class TimePickerCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timeSrv.setTime(this.editTimeRaw);
|
this.timeSrv.setTime(this.editTimeRaw);
|
||||||
this.isOpen = false;
|
this.closeDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
absoluteFromChanged() {
|
absoluteFromChanged() {
|
||||||
@ -143,7 +149,7 @@ export class TimePickerCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timeSrv.setTime(range);
|
this.timeSrv.setTime(range);
|
||||||
this.isOpen = false;
|
this.closeDropdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,12 +35,12 @@ export class Tracker {
|
|||||||
|
|
||||||
$window.onbeforeunload = () => {
|
$window.onbeforeunload = () => {
|
||||||
if (this.ignoreChanges()) {
|
if (this.ignoreChanges()) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (this.hasChanges()) {
|
if (this.hasChanges()) {
|
||||||
return 'There are unsaved changes to this dashboard';
|
return 'There are unsaved changes to this dashboard';
|
||||||
}
|
}
|
||||||
return null;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.$on('$locationChangeStart', (event, next) => {
|
scope.$on('$locationChangeStart', (event, next) => {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
define([
|
|
||||||
'./panel_header',
|
|
||||||
'./panel_directive',
|
|
||||||
'./solo_panel_ctrl',
|
|
||||||
'./query_ctrl',
|
|
||||||
'./panel_editor_tab',
|
|
||||||
'./query_editor_row',
|
|
||||||
'./query_troubleshooter',
|
|
||||||
], function () {});
|
|
7
public/app/features/panel/all.ts
Normal file
7
public/app/features/panel/all.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import './panel_header';
|
||||||
|
import './panel_directive';
|
||||||
|
import './solo_panel_ctrl';
|
||||||
|
import './query_ctrl';
|
||||||
|
import './panel_editor_tab';
|
||||||
|
import './query_editor_row';
|
||||||
|
import './query_troubleshooter';
|
@ -190,6 +190,7 @@ export class PanelCtrl {
|
|||||||
text: 'Duplicate',
|
text: 'Duplicate',
|
||||||
click: 'ctrl.duplicate()',
|
click: 'ctrl.duplicate()',
|
||||||
role: 'Editor',
|
role: 'Editor',
|
||||||
|
shortcut: 'p d',
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.push({
|
menu.push({
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
define([
|
|
||||||
'./playlists_ctrl',
|
|
||||||
'./playlist_search',
|
|
||||||
'./playlist_srv',
|
|
||||||
'./playlist_edit_ctrl',
|
|
||||||
'./playlist_routes'
|
|
||||||
], function () {});
|
|
5
public/app/features/playlist/all.ts
Normal file
5
public/app/features/playlist/all.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import './playlists_ctrl';
|
||||||
|
import './playlist_search';
|
||||||
|
import './playlist_srv';
|
||||||
|
import './playlist_edit_ctrl';
|
||||||
|
import './playlist_routes';
|
@ -1,39 +0,0 @@
|
|||||||
define([
|
|
||||||
'angular',
|
|
||||||
'lodash'
|
|
||||||
],
|
|
||||||
function (angular) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var module = angular.module('grafana.routes');
|
|
||||||
|
|
||||||
module.config(function($routeProvider) {
|
|
||||||
$routeProvider
|
|
||||||
.when('/playlists', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistsCtrl'
|
|
||||||
})
|
|
||||||
.when('/playlists/create', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistEditCtrl'
|
|
||||||
})
|
|
||||||
.when('/playlists/edit/:id', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistEditCtrl'
|
|
||||||
})
|
|
||||||
.when('/playlists/play/:id', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistsCtrl',
|
|
||||||
resolve: {
|
|
||||||
init: function(playlistSrv, $route) {
|
|
||||||
var playlistId = $route.current.params.id;
|
|
||||||
playlistSrv.start(playlistId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
33
public/app/features/playlist/playlist_routes.ts
Normal file
33
public/app/features/playlist/playlist_routes.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
function grafanaRoutes($routeProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.when('/playlists', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistsCtrl',
|
||||||
|
})
|
||||||
|
.when('/playlists/create', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistEditCtrl',
|
||||||
|
})
|
||||||
|
.when('/playlists/edit/:id', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistEditCtrl',
|
||||||
|
})
|
||||||
|
.when('/playlists/play/:id', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistsCtrl',
|
||||||
|
resolve: {
|
||||||
|
init: function(playlistSrv, $route) {
|
||||||
|
let playlistId = $route.current.params.id;
|
||||||
|
playlistSrv.start(playlistId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('grafana.routes').config(grafanaRoutes);
|
@ -23,6 +23,8 @@ export class VariableEditorCtrl {
|
|||||||
{ value: 2, text: 'Alphabetical (desc)' },
|
{ value: 2, text: 'Alphabetical (desc)' },
|
||||||
{ value: 3, text: 'Numerical (asc)' },
|
{ value: 3, text: 'Numerical (asc)' },
|
||||||
{ value: 4, text: 'Numerical (desc)' },
|
{ value: 4, text: 'Numerical (desc)' },
|
||||||
|
{ value: 5, text: 'Alphabetical (case-insensitive, asc)' },
|
||||||
|
{ value: 6, text: 'Alphabetical (case-insensitive, desc)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];
|
$scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];
|
||||||
|
@ -197,6 +197,8 @@ export class QueryVariable implements Variable {
|
|||||||
return parseInt(matches[1], 10);
|
return parseInt(matches[1], 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (sortType === 3) {
|
||||||
|
options = _.sortBy(options, opt => { return _.toLower(opt.text); });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reverseSort) {
|
if (reverseSort) {
|
||||||
|
@ -40,11 +40,11 @@ describe('QueryVariable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('can convert and sort metric names', () => {
|
describe('can convert and sort metric names', () => {
|
||||||
var variable = new QueryVariable({}, null, null, null, null);
|
const variable = new QueryVariable({}, null, null, null, null);
|
||||||
variable.sort = 3; // Numerical (asc)
|
let input;
|
||||||
|
|
||||||
describe('can sort a mixed array of metric variables', () => {
|
beforeEach(() => {
|
||||||
var input = [
|
input = [
|
||||||
{ text: '0', value: '0' },
|
{ text: '0', value: '0' },
|
||||||
{ text: '1', value: '1' },
|
{ text: '1', value: '1' },
|
||||||
{ text: null, value: 3 },
|
{ text: null, value: 3 },
|
||||||
@ -58,11 +58,18 @@ describe('QueryVariable', () => {
|
|||||||
{ text: '', value: undefined },
|
{ text: '', value: undefined },
|
||||||
{ text: undefined, value: '' },
|
{ text: undefined, value: '' },
|
||||||
];
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('can sort a mixed array of metric variables in numeric order', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
variable.sort = 3; // Numerical (asc)
|
||||||
|
result = variable.metricNamesToVariableValues(input);
|
||||||
|
});
|
||||||
|
|
||||||
var result = variable.metricNamesToVariableValues(input);
|
|
||||||
it('should return in same order', () => {
|
it('should return in same order', () => {
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
|
||||||
expect(result.length).toBe(11);
|
expect(result.length).toBe(11);
|
||||||
expect(result[i++].text).toBe('');
|
expect(result[i++].text).toBe('');
|
||||||
expect(result[i++].text).toBe('0');
|
expect(result[i++].text).toBe('0');
|
||||||
@ -73,5 +80,27 @@ describe('QueryVariable', () => {
|
|||||||
expect(result[i++].text).toBe('6');
|
expect(result[i++].text).toBe('6');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('can sort a mixed array of metric variables in alphabetical order', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
variable.sort = 5; // Alphabetical CI (asc)
|
||||||
|
result = variable.metricNamesToVariableValues(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return in same order', () => {
|
||||||
|
var i = 0;
|
||||||
|
console.log(result);
|
||||||
|
expect(result.length).toBe(11);
|
||||||
|
expect(result[i++].text).toBe('');
|
||||||
|
expect(result[i++].text).toBe('0');
|
||||||
|
expect(result[i++].text).toBe('1');
|
||||||
|
expect(result[i++].text).toBe('10');
|
||||||
|
expect(result[i++].text).toBe('3');
|
||||||
|
expect(result[i++].text).toBe('4');
|
||||||
|
expect(result[i++].text).toBe('5');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import $ from 'jquery';
|
|||||||
import rst2html from 'rst2html';
|
import rst2html from 'rst2html';
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
export function graphiteAddFunc($compile) {
|
export function graphiteAddFunc($compile) {
|
||||||
const inputTemplate =
|
const inputTemplate =
|
||||||
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
|
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
|
||||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import rst2html from 'rst2html';
|
import rst2html from 'rst2html';
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
||||||
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
|
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
|
||||||
const paramTemplate =
|
const paramTemplate =
|
||||||
|
@ -28,7 +28,7 @@ An annotation is an event that is overlayed on top of graphs. The query can have
|
|||||||
Macros:
|
Macros:
|
||||||
- $__time(column) -> column AS time
|
- $__time(column) -> column AS time
|
||||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
|
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column <= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||||
|
|
||||||
Or build your own conditionals using these macros which just return the values:
|
Or build your own conditionals using these macros which just return the values:
|
||||||
|
@ -49,7 +49,7 @@ Table:
|
|||||||
Macros:
|
Macros:
|
||||||
- $__time(column) -> column AS time
|
- $__time(column) -> column AS time
|
||||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
|
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column <= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||||
- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
|
- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
|
||||||
|
|
||||||
|
@ -6,8 +6,12 @@ import * as dateMath from 'app/core/utils/datemath';
|
|||||||
import PrometheusMetricFindQuery from './metric_find_query';
|
import PrometheusMetricFindQuery from './metric_find_query';
|
||||||
import { ResultTransformer } from './result_transformer';
|
import { ResultTransformer } from './result_transformer';
|
||||||
|
|
||||||
function prometheusSpecialRegexEscape(value) {
|
export function prometheusRegularEscape(value) {
|
||||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
|
return value.replace(/'/g, "\\\\'");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prometheusSpecialRegexEscape(value) {
|
||||||
|
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrometheusDatasource {
|
export class PrometheusDatasource {
|
||||||
@ -80,7 +84,7 @@ export class PrometheusDatasource {
|
|||||||
interpolateQueryExpr(value, variable, defaultFormatFn) {
|
interpolateQueryExpr(value, variable, defaultFormatFn) {
|
||||||
// if no multi or include all do not regexEscape
|
// if no multi or include all do not regexEscape
|
||||||
if (!variable.multi && !variable.includeAll) {
|
if (!variable.multi && !variable.includeAll) {
|
||||||
return value;
|
return prometheusRegularEscape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
|
||||||
|
|
||||||
describe('PrometheusDatasource', () => {
|
describe('PrometheusDatasource', () => {
|
||||||
let ctx: any = {};
|
let ctx: any = {};
|
||||||
@ -101,4 +101,41 @@ describe('PrometheusDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Prometheus regular escaping', function() {
|
||||||
|
it('should not escape simple string', function() {
|
||||||
|
expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
|
||||||
|
});
|
||||||
|
it("should escape '", function() {
|
||||||
|
expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
|
||||||
|
});
|
||||||
|
it('should escape multiple characters', function() {
|
||||||
|
expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Prometheus regexes escaping', function() {
|
||||||
|
it('should not escape simple string', function() {
|
||||||
|
expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
|
||||||
|
});
|
||||||
|
it('should escape $^*+?.()\\', function() {
|
||||||
|
expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
|
||||||
|
expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
|
||||||
|
});
|
||||||
|
it('should escape multiple special characters', function() {
|
||||||
|
expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,292 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery',
|
|
||||||
'app/core/core',
|
|
||||||
],
|
|
||||||
function ($, core) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var appEvents = core.appEvents;
|
|
||||||
|
|
||||||
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
|
|
||||||
var self = this;
|
|
||||||
var ctrl = scope.ctrl;
|
|
||||||
var panel = ctrl.panel;
|
|
||||||
|
|
||||||
var $tooltip = $('<div class="graph-tooltip">');
|
|
||||||
|
|
||||||
this.destroy = function() {
|
|
||||||
$tooltip.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.findHoverIndexFromDataPoints = function(posX, series, last) {
|
|
||||||
var ps = series.datapoints.pointsize;
|
|
||||||
var initial = last*ps;
|
|
||||||
var len = series.datapoints.points.length;
|
|
||||||
for (var j = initial; j < len; j += ps) {
|
|
||||||
// Special case of a non stepped line, highlight the very last point just before a null point
|
|
||||||
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
|
|
||||||
//normal case
|
|
||||||
|| series.datapoints.points[j] > posX) {
|
|
||||||
return Math.max(j - ps, 0)/ps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return j/ps - 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.findHoverIndexFromData = function(posX, series) {
|
|
||||||
var lower = 0;
|
|
||||||
var upper = series.data.length - 1;
|
|
||||||
var middle;
|
|
||||||
while (true) {
|
|
||||||
if (lower > upper) {
|
|
||||||
return Math.max(upper, 0);
|
|
||||||
}
|
|
||||||
middle = Math.floor((lower + upper) / 2);
|
|
||||||
if (series.data[middle][0] === posX) {
|
|
||||||
return middle;
|
|
||||||
} else if (series.data[middle][0] < posX) {
|
|
||||||
lower = middle + 1;
|
|
||||||
} else {
|
|
||||||
upper = middle - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
|
|
||||||
if (xMode === 'time') {
|
|
||||||
innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
|
|
||||||
}
|
|
||||||
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
|
|
||||||
var value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
|
|
||||||
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
|
|
||||||
var results = [[],[],[]];
|
|
||||||
|
|
||||||
//now we know the current X (j) position for X and Y values
|
|
||||||
var last_value = 0; //needed for stacked values
|
|
||||||
|
|
||||||
var minDistance, minTime;
|
|
||||||
|
|
||||||
for (i = 0; i < seriesList.length; i++) {
|
|
||||||
series = seriesList[i];
|
|
||||||
|
|
||||||
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
|
|
||||||
// Init value so that it does not brake series sorting
|
|
||||||
results[0].push({ hidden: true, value: 0 });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
|
|
||||||
// Init value so that it does not brake series sorting
|
|
||||||
results[0].push({ hidden: true, value: 0 });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
|
||||||
hoverDistance = pos.x - series.data[hoverIndex][0];
|
|
||||||
pointTime = series.data[hoverIndex][0];
|
|
||||||
|
|
||||||
// Take the closest point before the cursor, or if it does not exist, the closest after
|
|
||||||
if (! minDistance
|
|
||||||
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
|
|
||||||
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
|
|
||||||
minDistance = hoverDistance;
|
|
||||||
minTime = pointTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series.stack) {
|
|
||||||
if (panel.tooltip.value_type === 'individual') {
|
|
||||||
value = series.data[hoverIndex][1];
|
|
||||||
} else if (!series.stack) {
|
|
||||||
value = series.data[hoverIndex][1];
|
|
||||||
} else {
|
|
||||||
last_value += series.data[hoverIndex][1];
|
|
||||||
value = last_value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = series.data[hoverIndex][1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlighting multiple Points depending on the plot type
|
|
||||||
if (series.lines.steps || series.stack) {
|
|
||||||
// stacked and steppedLine plots can have series with different length.
|
|
||||||
// Stacked series can increase its length on each new stacked serie if null points found,
|
|
||||||
// to speed the index search we begin always on the last found hoverIndex.
|
|
||||||
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Be sure we have a yaxis so that it does not brake series sorting
|
|
||||||
yaxis = 0;
|
|
||||||
if (series.yaxis) {
|
|
||||||
yaxis = series.yaxis.n;
|
|
||||||
}
|
|
||||||
|
|
||||||
results[yaxis].push({
|
|
||||||
value: value,
|
|
||||||
hoverIndex: hoverIndex,
|
|
||||||
color: series.color,
|
|
||||||
label: series.aliasEscaped,
|
|
||||||
time: pointTime,
|
|
||||||
distance: hoverDistance,
|
|
||||||
index: i
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contat the 3 sub-arrays
|
|
||||||
results = results[0].concat(results[1],results[2]);
|
|
||||||
|
|
||||||
// Time of the point closer to pointer
|
|
||||||
results.time = minTime;
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
elem.mouseleave(function () {
|
|
||||||
if (panel.tooltip.shared) {
|
|
||||||
var plot = elem.data().plot;
|
|
||||||
if (plot) {
|
|
||||||
$tooltip.detach();
|
|
||||||
plot.unhighlight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
appEvents.emit('graph-hover-clear');
|
|
||||||
});
|
|
||||||
|
|
||||||
elem.bind("plothover", function (event, pos, item) {
|
|
||||||
self.show(pos, item);
|
|
||||||
|
|
||||||
// broadcast to other graph panels that we are hovering!
|
|
||||||
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
|
|
||||||
appEvents.emit('graph-hover', {pos: pos, panel: panel});
|
|
||||||
});
|
|
||||||
|
|
||||||
elem.bind("plotclick", function (event, pos, item) {
|
|
||||||
appEvents.emit('graph-click', {pos: pos, panel: panel, item: item});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.clear = function(plot) {
|
|
||||||
$tooltip.detach();
|
|
||||||
plot.clearCrosshair();
|
|
||||||
plot.unhighlight();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.show = function(pos, item) {
|
|
||||||
var plot = elem.data().plot;
|
|
||||||
var plotData = plot.getData();
|
|
||||||
var xAxes = plot.getXAxes();
|
|
||||||
var xMode = xAxes[0].options.mode;
|
|
||||||
var seriesList = getSeriesFn();
|
|
||||||
var allSeriesMode = panel.tooltip.shared;
|
|
||||||
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
|
|
||||||
|
|
||||||
// if panelRelY is defined another panel wants us to show a tooltip
|
|
||||||
// get pageX from position on x axis and pageY from relative position in original panel
|
|
||||||
if (pos.panelRelY) {
|
|
||||||
var pointOffset = plot.pointOffset({x: pos.x});
|
|
||||||
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
|
|
||||||
self.clear(plot);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pos.pageX = elem.offset().left + pointOffset.left;
|
|
||||||
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
|
|
||||||
var isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
|
|
||||||
if (!isVisible) {
|
|
||||||
self.clear(plot);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
plot.setCrosshair(pos);
|
|
||||||
allSeriesMode = true;
|
|
||||||
|
|
||||||
if (dashboard.sharedCrosshairModeOnly()) {
|
|
||||||
// if only crosshair mode we are done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seriesList.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seriesList[0].hasMsResolution) {
|
|
||||||
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
|
|
||||||
} else {
|
|
||||||
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allSeriesMode) {
|
|
||||||
plot.unhighlight();
|
|
||||||
|
|
||||||
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
|
|
||||||
|
|
||||||
seriesHtml = '';
|
|
||||||
|
|
||||||
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
|
|
||||||
|
|
||||||
// Dynamically reorder the hovercard for the current time point if the
|
|
||||||
// option is enabled.
|
|
||||||
if (panel.tooltip.sort === 2) {
|
|
||||||
seriesHoverInfo.sort(function(a, b) {
|
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
} else if (panel.tooltip.sort === 1) {
|
|
||||||
seriesHoverInfo.sort(function(a, b) {
|
|
||||||
return a.value - b.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < seriesHoverInfo.length; i++) {
|
|
||||||
hoverInfo = seriesHoverInfo[i];
|
|
||||||
|
|
||||||
if (hoverInfo.hidden) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var highlightClass = '';
|
|
||||||
if (item && hoverInfo.index === item.seriesIndex) {
|
|
||||||
highlightClass = 'graph-tooltip-list-item--highlight';
|
|
||||||
}
|
|
||||||
|
|
||||||
series = seriesList[hoverInfo.index];
|
|
||||||
|
|
||||||
value = series.formatValue(hoverInfo.value);
|
|
||||||
|
|
||||||
seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
|
|
||||||
seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
|
|
||||||
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
|
||||||
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
|
|
||||||
}
|
|
||||||
// single series tooltip
|
|
||||||
else if (item) {
|
|
||||||
series = seriesList[item.seriesIndex];
|
|
||||||
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
|
||||||
group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.aliasEscaped + ':</div>';
|
|
||||||
|
|
||||||
if (panel.stack && panel.tooltip.value_type === 'individual') {
|
|
||||||
value = item.datapoint[1] - item.datapoint[2];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
value = item.datapoint[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
value = series.formatValue(value);
|
|
||||||
|
|
||||||
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
|
|
||||||
|
|
||||||
group += '<div class="graph-tooltip-value">' + value + '</div>';
|
|
||||||
|
|
||||||
self.renderAndShow(absoluteTime, group, pos, xMode);
|
|
||||||
}
|
|
||||||
// no hit
|
|
||||||
else {
|
|
||||||
$tooltip.detach();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return GraphTooltip;
|
|
||||||
});
|
|
289
public/app/plugins/panel/graph/graph_tooltip.ts
Normal file
289
public/app/plugins/panel/graph/graph_tooltip.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
|
||||||
|
export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
|
||||||
|
let self = this;
|
||||||
|
let ctrl = scope.ctrl;
|
||||||
|
let panel = ctrl.panel;
|
||||||
|
|
||||||
|
let $tooltip = $('<div class="graph-tooltip">');
|
||||||
|
|
||||||
|
this.destroy = function() {
|
||||||
|
$tooltip.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.findHoverIndexFromDataPoints = function(posX, series, last) {
|
||||||
|
let ps = series.datapoints.pointsize;
|
||||||
|
let initial = last * ps;
|
||||||
|
let len = series.datapoints.points.length;
|
||||||
|
let j;
|
||||||
|
for (j = initial; j < len; j += ps) {
|
||||||
|
// Special case of a non stepped line, highlight the very last point just before a null point
|
||||||
|
if (
|
||||||
|
(!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) ||
|
||||||
|
//normal case
|
||||||
|
series.datapoints.points[j] > posX
|
||||||
|
) {
|
||||||
|
return Math.max(j - ps, 0) / ps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return j / ps - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.findHoverIndexFromData = function(posX, series) {
|
||||||
|
let lower = 0;
|
||||||
|
let upper = series.data.length - 1;
|
||||||
|
let middle;
|
||||||
|
while (true) {
|
||||||
|
if (lower > upper) {
|
||||||
|
return Math.max(upper, 0);
|
||||||
|
}
|
||||||
|
middle = Math.floor((lower + upper) / 2);
|
||||||
|
if (series.data[middle][0] === posX) {
|
||||||
|
return middle;
|
||||||
|
} else if (series.data[middle][0] < posX) {
|
||||||
|
lower = middle + 1;
|
||||||
|
} else {
|
||||||
|
upper = middle - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
|
||||||
|
if (xMode === 'time') {
|
||||||
|
innerHtml = '<div class="graph-tooltip-time">' + absoluteTime + '</div>' + innerHtml;
|
||||||
|
}
|
||||||
|
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
|
||||||
|
let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
|
||||||
|
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
|
||||||
|
let results: any = [[], [], []];
|
||||||
|
|
||||||
|
//now we know the current X (j) position for X and Y values
|
||||||
|
let last_value = 0; //needed for stacked values
|
||||||
|
|
||||||
|
let minDistance, minTime;
|
||||||
|
|
||||||
|
for (i = 0; i < seriesList.length; i++) {
|
||||||
|
series = seriesList[i];
|
||||||
|
|
||||||
|
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
|
||||||
|
// Init value so that it does not brake series sorting
|
||||||
|
results[0].push({ hidden: true, value: 0 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
|
||||||
|
// Init value so that it does not brake series sorting
|
||||||
|
results[0].push({ hidden: true, value: 0 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
||||||
|
hoverDistance = pos.x - series.data[hoverIndex][0];
|
||||||
|
pointTime = series.data[hoverIndex][0];
|
||||||
|
|
||||||
|
// Take the closest point before the cursor, or if it does not exist, the closest after
|
||||||
|
if (
|
||||||
|
!minDistance ||
|
||||||
|
(hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||
|
||||||
|
(hoverDistance < 0 && hoverDistance > minDistance)
|
||||||
|
) {
|
||||||
|
minDistance = hoverDistance;
|
||||||
|
minTime = pointTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (series.stack) {
|
||||||
|
if (panel.tooltip.value_type === 'individual') {
|
||||||
|
value = series.data[hoverIndex][1];
|
||||||
|
} else if (!series.stack) {
|
||||||
|
value = series.data[hoverIndex][1];
|
||||||
|
} else {
|
||||||
|
last_value += series.data[hoverIndex][1];
|
||||||
|
value = last_value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = series.data[hoverIndex][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlighting multiple Points depending on the plot type
|
||||||
|
if (series.lines.steps || series.stack) {
|
||||||
|
// stacked and steppedLine plots can have series with different length.
|
||||||
|
// Stacked series can increase its length on each new stacked serie if null points found,
|
||||||
|
// to speed the index search we begin always on the last found hoverIndex.
|
||||||
|
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Be sure we have a yaxis so that it does not brake series sorting
|
||||||
|
yaxis = 0;
|
||||||
|
if (series.yaxis) {
|
||||||
|
yaxis = series.yaxis.n;
|
||||||
|
}
|
||||||
|
|
||||||
|
results[yaxis].push({
|
||||||
|
value: value,
|
||||||
|
hoverIndex: hoverIndex,
|
||||||
|
color: series.color,
|
||||||
|
label: series.aliasEscaped,
|
||||||
|
time: pointTime,
|
||||||
|
distance: hoverDistance,
|
||||||
|
index: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contat the 3 sub-arrays
|
||||||
|
results = results[0].concat(results[1], results[2]);
|
||||||
|
|
||||||
|
// Time of the point closer to pointer
|
||||||
|
results.time = minTime;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
elem.mouseleave(function() {
|
||||||
|
if (panel.tooltip.shared) {
|
||||||
|
let plot = elem.data().plot;
|
||||||
|
if (plot) {
|
||||||
|
$tooltip.detach();
|
||||||
|
plot.unhighlight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appEvents.emit('graph-hover-clear');
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.bind('plothover', function(event, pos, item) {
|
||||||
|
self.show(pos, item);
|
||||||
|
|
||||||
|
// broadcast to other graph panels that we are hovering!
|
||||||
|
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
|
||||||
|
appEvents.emit('graph-hover', { pos: pos, panel: panel });
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.bind('plotclick', function(event, pos, item) {
|
||||||
|
appEvents.emit('graph-click', { pos: pos, panel: panel, item: item });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clear = function(plot) {
|
||||||
|
$tooltip.detach();
|
||||||
|
plot.clearCrosshair();
|
||||||
|
plot.unhighlight();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.show = function(pos, item) {
|
||||||
|
let plot = elem.data().plot;
|
||||||
|
let plotData = plot.getData();
|
||||||
|
let xAxes = plot.getXAxes();
|
||||||
|
let xMode = xAxes[0].options.mode;
|
||||||
|
let seriesList = getSeriesFn();
|
||||||
|
let allSeriesMode = panel.tooltip.shared;
|
||||||
|
let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
|
||||||
|
|
||||||
|
// if panelRelY is defined another panel wants us to show a tooltip
|
||||||
|
// get pageX from position on x axis and pageY from relative position in original panel
|
||||||
|
if (pos.panelRelY) {
|
||||||
|
let pointOffset = plot.pointOffset({ x: pos.x });
|
||||||
|
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
|
||||||
|
self.clear(plot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pos.pageX = elem.offset().left + pointOffset.left;
|
||||||
|
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
|
||||||
|
let isVisible =
|
||||||
|
pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
|
||||||
|
if (!isVisible) {
|
||||||
|
self.clear(plot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plot.setCrosshair(pos);
|
||||||
|
allSeriesMode = true;
|
||||||
|
|
||||||
|
if (dashboard.sharedCrosshairModeOnly()) {
|
||||||
|
// if only crosshair mode we are done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesList[0].hasMsResolution) {
|
||||||
|
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
|
||||||
|
} else {
|
||||||
|
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allSeriesMode) {
|
||||||
|
plot.unhighlight();
|
||||||
|
|
||||||
|
let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
|
||||||
|
|
||||||
|
seriesHtml = '';
|
||||||
|
|
||||||
|
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
|
||||||
|
|
||||||
|
// Dynamically reorder the hovercard for the current time point if the
|
||||||
|
// option is enabled.
|
||||||
|
if (panel.tooltip.sort === 2) {
|
||||||
|
seriesHoverInfo.sort(function(a, b) {
|
||||||
|
return b.value - a.value;
|
||||||
|
});
|
||||||
|
} else if (panel.tooltip.sort === 1) {
|
||||||
|
seriesHoverInfo.sort(function(a, b) {
|
||||||
|
return a.value - b.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < seriesHoverInfo.length; i++) {
|
||||||
|
hoverInfo = seriesHoverInfo[i];
|
||||||
|
|
||||||
|
if (hoverInfo.hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlightClass = '';
|
||||||
|
if (item && hoverInfo.index === item.seriesIndex) {
|
||||||
|
highlightClass = 'graph-tooltip-list-item--highlight';
|
||||||
|
}
|
||||||
|
|
||||||
|
series = seriesList[hoverInfo.index];
|
||||||
|
|
||||||
|
value = series.formatValue(hoverInfo.value);
|
||||||
|
|
||||||
|
seriesHtml +=
|
||||||
|
'<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
|
||||||
|
seriesHtml +=
|
||||||
|
'<i class="fa fa-minus" style="color:' + hoverInfo.color + ';"></i> ' + hoverInfo.label + ':</div>';
|
||||||
|
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
||||||
|
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
|
||||||
|
} else if (item) {
|
||||||
|
// single series tooltip
|
||||||
|
series = seriesList[item.seriesIndex];
|
||||||
|
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||||
|
group +=
|
||||||
|
'<i class="fa fa-minus" style="color:' + item.series.color + ';"></i> ' + series.aliasEscaped + ':</div>';
|
||||||
|
|
||||||
|
if (panel.stack && panel.tooltip.value_type === 'individual') {
|
||||||
|
value = item.datapoint[1] - item.datapoint[2];
|
||||||
|
} else {
|
||||||
|
value = item.datapoint[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = series.formatValue(value);
|
||||||
|
|
||||||
|
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
|
||||||
|
|
||||||
|
group += '<div class="graph-tooltip-value">' + value + '</div>';
|
||||||
|
|
||||||
|
self.renderAndShow(absoluteTime, group, pos, xMode);
|
||||||
|
} else {
|
||||||
|
// no hit
|
||||||
|
$tooltip.detach();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -131,8 +131,11 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
|
|||||||
elem.empty();
|
elem.empty();
|
||||||
|
|
||||||
// Set min-width if side style and there is a value, otherwise remove the CSS propery
|
// Set min-width if side style and there is a value, otherwise remove the CSS propery
|
||||||
var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
|
// Set width so it works with IE11
|
||||||
|
var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
|
||||||
|
var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
|
||||||
elem.css('min-width', width);
|
elem.css('min-width', width);
|
||||||
|
elem.css('width', ieWidth);
|
||||||
|
|
||||||
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ var scope = {
|
|||||||
|
|
||||||
var elem = $('<div></div>');
|
var elem = $('<div></div>');
|
||||||
var dashboard = {};
|
var dashboard = {};
|
||||||
|
var getSeriesFn;
|
||||||
|
|
||||||
function describeSharedTooltip(desc, fn) {
|
function describeSharedTooltip(desc, fn) {
|
||||||
var ctx: any = {};
|
var ctx: any = {};
|
||||||
@ -30,7 +31,7 @@ function describeSharedTooltip(desc, fn) {
|
|||||||
describe(desc, function() {
|
describe(desc, function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.setupFn();
|
ctx.setupFn();
|
||||||
var tooltip = new GraphTooltip(elem, dashboard, scope);
|
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
|
||||||
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
|
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ function describeSharedTooltip(desc, fn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('findHoverIndexFromData', function() {
|
describe('findHoverIndexFromData', function() {
|
||||||
var tooltip = new GraphTooltip(elem, dashboard, scope);
|
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
|
||||||
var series = {
|
var series = {
|
||||||
data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
|
data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
|
||||||
};
|
};
|
||||||
|
@ -69,7 +69,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group" ng-if="style.type === 'number'">
|
<div class="section gf-form-group" ng-if="style.type === 'string'">
|
||||||
|
<h5 class="section-heading">Value Mappings</h5>
|
||||||
|
<div class="editor-row">
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label">
|
||||||
|
Type
|
||||||
|
</span>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input" ng-model="style.mappingType"
|
||||||
|
ng-options="c.value as c.text for c in editor.mappingTypes" ng-change="editor.render()"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form-group" ng-if="style.mappingType==1">
|
||||||
|
<div class="gf-form" ng-repeat="map in style.valueMaps">
|
||||||
|
<span class="gf-form-label">
|
||||||
|
<i class="fa fa-remove pointer" ng-click="editor.removeValueMap(style, $index)"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="gf-form-input max-width-6" ng-model="map.value" placeholder="Value" ng-blur="editor.render()">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<i class="fa fa-arrow-right"></i>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="gf-form-input max-width-8" ng-model="map.text" placeholder="Text" ng-blur="editor.render()">
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="editor.addValueMap(style)"><i class="fa fa-plus"></i></a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form-group" ng-if="style.mappingType==2">
|
||||||
|
<div class="gf-form" ng-repeat="rangeMap in style.rangeMaps">
|
||||||
|
<span class="gf-form-label">
|
||||||
|
<i class="fa fa-remove pointer" ng-click="editor.removeRangeMap(style, $index)"></i>
|
||||||
|
</span>
|
||||||
|
<span class="gf-form-label">From</span>
|
||||||
|
<input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="editor.render()">
|
||||||
|
<span class="gf-form-label">To</span>
|
||||||
|
<input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="editor.render()">
|
||||||
|
<span class="gf-form-label">Text</span>
|
||||||
|
<input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="editor.render()">
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="editor.addRangeMap(style)"><i class="fa fa-plus"></i></a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section gf-form-group" ng-if="['number', 'string'].indexOf(style.type) !== -1">
|
||||||
<h5 class="section-heading">Thresholds</h5>
|
<h5 class="section-heading">Thresholds</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-8">Thresholds
|
<label class="gf-form-label width-8">Thresholds
|
||||||
@ -111,10 +163,10 @@
|
|||||||
<span>
|
<span>
|
||||||
Use special variables to specify cell values:
|
Use special variables to specify cell values:
|
||||||
<br>
|
<br>
|
||||||
<em>$__cell</em> refers to current cell value
|
<em>${__cell}</em> refers to current cell value
|
||||||
<br>
|
<br>
|
||||||
<em>$__cell_n</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
<em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
||||||
<em>$__cell_1</em> refers to second column's value.
|
<em>${__cell_1}</em> refers to second column's value.
|
||||||
</span>
|
</span>
|
||||||
</info-popover>
|
</info-popover>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ export class ColumnOptionsCtrl {
|
|||||||
unitFormats: any;
|
unitFormats: any;
|
||||||
getColumnNames: any;
|
getColumnNames: any;
|
||||||
activeStyleIndex: number;
|
activeStyleIndex: number;
|
||||||
|
mappingTypes: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope) {
|
constructor($scope) {
|
||||||
@ -41,6 +42,7 @@ export class ColumnOptionsCtrl {
|
|||||||
{ text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a' },
|
{ text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a' },
|
||||||
{ text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT' },
|
{ text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT' },
|
||||||
];
|
];
|
||||||
|
this.mappingTypes = [{ text: 'Value to text', value: 1 }, { text: 'Range to text', value: 2 }];
|
||||||
|
|
||||||
this.getColumnNames = () => {
|
this.getColumnNames = () => {
|
||||||
if (!this.panelCtrl.table) {
|
if (!this.panelCtrl.table) {
|
||||||
@ -74,6 +76,7 @@ export class ColumnOptionsCtrl {
|
|||||||
pattern: '',
|
pattern: '',
|
||||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
|
mappingType: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
var styles = this.panel.styles;
|
var styles = this.panel.styles;
|
||||||
@ -110,6 +113,32 @@ export class ColumnOptionsCtrl {
|
|||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addValueMap(style) {
|
||||||
|
if (!style.valueMaps) {
|
||||||
|
style.valueMaps = [];
|
||||||
|
}
|
||||||
|
style.valueMaps.push({ value: '', text: '' });
|
||||||
|
this.panelCtrl.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeValueMap(style, index) {
|
||||||
|
style.valueMaps.splice(index, 1);
|
||||||
|
this.panelCtrl.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
addRangeMap(style) {
|
||||||
|
if (!style.rangeMaps) {
|
||||||
|
style.rangeMaps = [];
|
||||||
|
}
|
||||||
|
style.rangeMaps.push({ from: '', to: '', text: '' });
|
||||||
|
this.panelCtrl.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRangeMap(style, index) {
|
||||||
|
style.rangeMaps.splice(index, 1);
|
||||||
|
this.panelCtrl.render();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
|
@ -47,7 +47,6 @@ export class TableRenderer {
|
|||||||
if (!style.thresholds) {
|
if (!style.thresholds) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = style.thresholds.length; i > 0; i--) {
|
for (var i = style.thresholds.length; i > 0; i--) {
|
||||||
if (value >= style.thresholds[i - 1]) {
|
if (value >= style.thresholds[i - 1]) {
|
||||||
return style.colors[i];
|
return style.colors[i];
|
||||||
@ -100,6 +99,60 @@ export class TableRenderer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (column.style.type === 'string') {
|
||||||
|
return v => {
|
||||||
|
if (_.isArray(v)) {
|
||||||
|
v = v.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingType = column.style.mappingType || 0;
|
||||||
|
|
||||||
|
if (mappingType === 1 && column.style.valueMaps) {
|
||||||
|
for (let i = 0; i < column.style.valueMaps.length; i++) {
|
||||||
|
const map = column.style.valueMaps[i];
|
||||||
|
|
||||||
|
if (v === null) {
|
||||||
|
if (map.value === 'null') {
|
||||||
|
return map.text;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow both numeric and string values to be mapped
|
||||||
|
if ((!_.isString(v) && Number(map.value) === Number(v)) || map.value === v) {
|
||||||
|
this.setColorState(v, column.style);
|
||||||
|
return this.defaultCellFormatter(map.text, column.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappingType === 2 && column.style.rangeMaps) {
|
||||||
|
for (let i = 0; i < column.style.rangeMaps.length; i++) {
|
||||||
|
const map = column.style.rangeMaps[i];
|
||||||
|
|
||||||
|
if (v === null) {
|
||||||
|
if (map.from === 'null' && map.to === 'null') {
|
||||||
|
return map.text;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) {
|
||||||
|
this.setColorState(v, column.style);
|
||||||
|
return this.defaultCellFormatter(map.text, column.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === null || v === void 0) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setColorState(v, column.style);
|
||||||
|
return this.defaultCellFormatter(v, column.style);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (column.style.type === 'number') {
|
if (column.style.type === 'number') {
|
||||||
let valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
|
let valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
|
||||||
|
|
||||||
@ -112,10 +165,7 @@ export class TableRenderer {
|
|||||||
return this.defaultCellFormatter(v, column.style);
|
return this.defaultCellFormatter(v, column.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (column.style.colorMode) {
|
this.setColorState(v, column.style);
|
||||||
this.colorState[column.style.colorMode] = this.getColorForValue(v, column.style);
|
|
||||||
}
|
|
||||||
|
|
||||||
return valueFormatter(v, column.style.decimals, null);
|
return valueFormatter(v, column.style.decimals, null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -125,6 +175,23 @@ export class TableRenderer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setColorState(value, style) {
|
||||||
|
if (!style.colorMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === null || value === void 0 || _.isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var numericValue = Number(value);
|
||||||
|
if (numericValue === NaN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.colorState[style.colorMode] = this.getColorForValue(numericValue, style);
|
||||||
|
}
|
||||||
|
|
||||||
renderRowVariables(rowIndex) {
|
renderRowVariables(rowIndex) {
|
||||||
let scopedVars = {};
|
let scopedVars = {};
|
||||||
let cell_variable;
|
let cell_variable;
|
||||||
|
@ -3,7 +3,7 @@ import TableModel from 'app/core/table_model';
|
|||||||
import { TableRenderer } from '../renderer';
|
import { TableRenderer } from '../renderer';
|
||||||
|
|
||||||
describe('when rendering table', () => {
|
describe('when rendering table', () => {
|
||||||
describe('given 2 columns', () => {
|
describe('given 13 columns', () => {
|
||||||
var table = new TableModel();
|
var table = new TableModel();
|
||||||
table.columns = [
|
table.columns = [
|
||||||
{ text: 'Time' },
|
{ text: 'Time' },
|
||||||
@ -15,8 +15,14 @@ describe('when rendering table', () => {
|
|||||||
{ text: 'Sanitized' },
|
{ text: 'Sanitized' },
|
||||||
{ text: 'Link' },
|
{ text: 'Link' },
|
||||||
{ text: 'Array' },
|
{ text: 'Array' },
|
||||||
|
{ text: 'Mapping' },
|
||||||
|
{ text: 'RangeMapping' },
|
||||||
|
{ text: 'MappingColored' },
|
||||||
|
{ text: 'RangeMappingColored' },
|
||||||
|
];
|
||||||
|
table.rows = [
|
||||||
|
[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2],
|
||||||
];
|
];
|
||||||
table.rows = [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2']]];
|
|
||||||
|
|
||||||
var panel = {
|
var panel = {
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -47,6 +53,10 @@ describe('when rendering table', () => {
|
|||||||
pattern: 'String',
|
pattern: 'String',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: 'String',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: 'United',
|
pattern: 'United',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@ -72,6 +82,84 @@ describe('when rendering table', () => {
|
|||||||
unit: 'ms',
|
unit: 'ms',
|
||||||
decimals: 3,
|
decimals: 3,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: 'Mapping',
|
||||||
|
type: 'string',
|
||||||
|
mappingType: 1,
|
||||||
|
valueMaps: [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
text: 'on',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '0',
|
||||||
|
text: 'off',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'HELLO WORLD',
|
||||||
|
text: 'HELLO GRAFANA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'value1, value2',
|
||||||
|
text: 'value3, value4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'RangeMapping',
|
||||||
|
type: 'string',
|
||||||
|
mappingType: 2,
|
||||||
|
rangeMaps: [
|
||||||
|
{
|
||||||
|
from: '1',
|
||||||
|
to: '3',
|
||||||
|
text: 'on',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: '3',
|
||||||
|
to: '6',
|
||||||
|
text: 'off',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'MappingColored',
|
||||||
|
type: 'string',
|
||||||
|
mappingType: 1,
|
||||||
|
valueMaps: [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
text: 'on',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '0',
|
||||||
|
text: 'off',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
colorMode: 'value',
|
||||||
|
thresholds: [1, 2],
|
||||||
|
colors: ['green', 'orange', 'red'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'RangeMappingColored',
|
||||||
|
type: 'string',
|
||||||
|
mappingType: 2,
|
||||||
|
rangeMaps: [
|
||||||
|
{
|
||||||
|
from: '1',
|
||||||
|
to: '3',
|
||||||
|
text: 'on',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: '3',
|
||||||
|
to: '6',
|
||||||
|
text: 'off',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
colorMode: 'value',
|
||||||
|
thresholds: [2, 5],
|
||||||
|
colors: ['green', 'orange', 'red'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -192,6 +280,86 @@ describe('when rendering table', () => {
|
|||||||
var html = renderer.renderCell(8, 0, ['value1', 'value2']);
|
var html = renderer.renderCell(8, 0, ['value1', 'value2']);
|
||||||
expect(html).toBe('<td>value1, value2</td>');
|
expect(html).toBe('<td>value1, value2</td>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('numeric value should be mapped to text', () => {
|
||||||
|
var html = renderer.renderCell(9, 0, 1);
|
||||||
|
expect(html).toBe('<td>on</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string numeric value should be mapped to text', () => {
|
||||||
|
var html = renderer.renderCell(9, 0, '0');
|
||||||
|
expect(html).toBe('<td>off</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string value should be mapped to text', () => {
|
||||||
|
var html = renderer.renderCell(9, 0, 'HELLO WORLD');
|
||||||
|
expect(html).toBe('<td>HELLO GRAFANA</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array column value should be mapped to text', () => {
|
||||||
|
var html = renderer.renderCell(9, 0, ['value1', 'value2']);
|
||||||
|
expect(html).toBe('<td>value3, value4</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text (range)', () => {
|
||||||
|
var html = renderer.renderCell(10, 0, 2);
|
||||||
|
expect(html).toBe('<td>on</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text (range)', () => {
|
||||||
|
var html = renderer.renderCell(10, 0, 5);
|
||||||
|
expect(html).toBe('<td>off</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array column value should not be mapped to text', () => {
|
||||||
|
var html = renderer.renderCell(10, 0, ['value1', 'value2']);
|
||||||
|
expect(html).toBe('<td>value1, value2</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(11, 0, 1);
|
||||||
|
expect(html).toBe('<td style="color:orange">on</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(11, 0, '1');
|
||||||
|
expect(html).toBe('<td style="color:orange">on</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(11, 0, 0);
|
||||||
|
expect(html).toBe('<td style="color:green">off</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(11, 0, '0');
|
||||||
|
expect(html).toBe('<td style="color:green">off</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(11, 0, '2.1');
|
||||||
|
expect(html).toBe('<td style="color:red">2.1</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text (range) and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(12, 0, 0);
|
||||||
|
expect(html).toBe('<td style="color:green">0</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text (range) and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(12, 0, 1);
|
||||||
|
expect(html).toBe('<td style="color:green">on</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text (range) and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(12, 0, 4);
|
||||||
|
expect(html).toBe('<td style="color:orange">off</td>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should be mapped to text (range) and colored cell should have style', () => {
|
||||||
|
var html = renderer.renderCell(12, 0, '7.1');
|
||||||
|
expect(html).toBe('<td style="color:red">7.1</td>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,9 +59,8 @@ $critical: #ec2128;
|
|||||||
$body-bg: $gray-7;
|
$body-bg: $gray-7;
|
||||||
$page-bg: $gray-7;
|
$page-bg: $gray-7;
|
||||||
$body-color: $gray-1;
|
$body-color: $gray-1;
|
||||||
//$text-color: $dark-4;
|
|
||||||
$text-color: $gray-1;
|
$text-color: $gray-1;
|
||||||
$text-color-strong: $white;
|
$text-color-strong: $dark-2;
|
||||||
$text-color-weak: $gray-2;
|
$text-color-weak: $gray-2;
|
||||||
$text-color-faint: $gray-4;
|
$text-color-faint: $gray-4;
|
||||||
$text-color-emphasis: $dark-5;
|
$text-color-emphasis: $dark-5;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
.gicon {
|
.gicon {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.1057142857em;
|
//width: 1.1057142857em;
|
||||||
height: 1.1057142857em;
|
//height: 1.1057142857em;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
//padding: 0.5rem 1.5rem 0.5rem 0;
|
//padding: 0.5rem 1.5rem 0.5rem 0;
|
||||||
padding: 1rem 1rem 0.75rem 1rem;
|
padding: 1rem 1rem 0.75rem 1rem;
|
||||||
height: 51px;
|
height: 51px;
|
||||||
line-height: 51px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
background: $side-menu-bg;
|
background: $side-menu-bg;
|
||||||
@ -61,6 +60,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.search-item--indent {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-dropdown__col_2 {
|
.search-dropdown__col_2 {
|
||||||
|
@ -178,6 +178,7 @@ li.sidemenu-org-switcher {
|
|||||||
padding: 0.4rem 1rem 0.4rem 0.65rem;
|
padding: 0.4rem 1rem 0.4rem 0.65rem;
|
||||||
min-height: $navbarHeight;
|
min-height: $navbarHeight;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: $navbarHeight - 1px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $navbarButtonBackgroundHighlight;
|
background: $navbarButtonBackgroundHighlight;
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $white;
|
color: $text-color-strong;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
padding: 0 28px 0 16px;
|
//margin-right: 8px;
|
||||||
|
padding: 0 4px 0 2px;
|
||||||
.icon-gf,
|
.icon-gf,
|
||||||
.fa {
|
.fa {
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
|
@ -33,7 +33,7 @@ div.flot-text {
|
|||||||
border: $panel-border;
|
border: $panel-border;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 100%;
|
//height: 100%;
|
||||||
|
|
||||||
&.panel-transparent {
|
&.panel-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -3,6 +3,7 @@ $login-border: #8daac5;
|
|||||||
.login {
|
.login {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
min-height: 85vh;
|
min-height: 85vh;
|
||||||
|
height: 80vh;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@ -290,9 +291,14 @@ select:-webkit-autofill:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
|
.login-content {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.login-branding {
|
.login-branding {
|
||||||
width: 45%;
|
width: 45%;
|
||||||
padding: 2rem 4rem;
|
padding: 2rem 4rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
.logo-icon {
|
.logo-icon {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
@ -371,7 +377,7 @@ select:-webkit-autofill:focus {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
content: "";
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
vendor/gopkg.in/gomail.v2/LICENSE → vendor/gopkg.in/mail.v2/LICENSE
generated
vendored
0
vendor/gopkg.in/gomail.v2/LICENSE → vendor/gopkg.in/mail.v2/LICENSE
generated
vendored
2
vendor/gopkg.in/gomail.v2/auth.go → vendor/gopkg.in/mail.v2/auth.go
generated
vendored
2
vendor/gopkg.in/gomail.v2/auth.go → vendor/gopkg.in/mail.v2/auth.go
generated
vendored
@ -1,4 +1,4 @@
|
|||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
5
vendor/gopkg.in/gomail.v2/doc.go → vendor/gopkg.in/mail.v2/doc.go
generated
vendored
5
vendor/gopkg.in/gomail.v2/doc.go → vendor/gopkg.in/mail.v2/doc.go
generated
vendored
@ -1,5 +1,6 @@
|
|||||||
// Package gomail provides a simple interface to compose emails and to mail them
|
// Package gomail provides a simple interface to compose emails and to mail them
|
||||||
// efficiently.
|
// efficiently.
|
||||||
//
|
//
|
||||||
// More info on Github: https://github.com/go-gomail/gomail
|
// More info on Github: https://github.com/go-mail/mail
|
||||||
package gomail
|
//
|
||||||
|
package mail
|
16
vendor/gopkg.in/mail.v2/errors.go
generated
vendored
Normal file
16
vendor/gopkg.in/mail.v2/errors.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// A SendError represents the failure to transmit a Message, detailing the cause
|
||||||
|
// of the failure and index of the Message within a batch.
|
||||||
|
type SendError struct {
|
||||||
|
// Index specifies the index of the Message within a batch.
|
||||||
|
Index uint
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *SendError) Error() string {
|
||||||
|
return fmt.Sprintf("gomail: could not send email %d: %v",
|
||||||
|
err.Index+1, err.Cause)
|
||||||
|
}
|
22
vendor/gopkg.in/gomail.v2/message.go → vendor/gopkg.in/mail.v2/message.go
generated
vendored
22
vendor/gopkg.in/gomail.v2/message.go → vendor/gopkg.in/mail.v2/message.go
generated
vendored
@ -1,4 +1,4 @@
|
|||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -18,6 +18,7 @@ type Message struct {
|
|||||||
encoding Encoding
|
encoding Encoding
|
||||||
hEncoder mimeEncoder
|
hEncoder mimeEncoder
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
|
boundary string
|
||||||
}
|
}
|
||||||
|
|
||||||
type header map[string][]string
|
type header map[string][]string
|
||||||
@ -97,6 +98,11 @@ const (
|
|||||||
Unencoded Encoding = "8bit"
|
Unencoded Encoding = "8bit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetBoundary sets a custom multipart boundary.
|
||||||
|
func (m *Message) SetBoundary(boundary string) {
|
||||||
|
m.boundary = boundary
|
||||||
|
}
|
||||||
|
|
||||||
// SetHeader sets a value to the given header field.
|
// SetHeader sets a value to the given header field.
|
||||||
func (m *Message) SetHeader(field string, value ...string) {
|
func (m *Message) SetHeader(field string, value ...string) {
|
||||||
m.encodeHeader(value)
|
m.encodeHeader(value)
|
||||||
@ -183,9 +189,15 @@ func (m *Message) GetHeader(field string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetBody sets the body of the message. It replaces any content previously set
|
// SetBody sets the body of the message. It replaces any content previously set
|
||||||
// by SetBody, AddAlternative or AddAlternativeWriter.
|
// by SetBody, SetBodyWriter, AddAlternative or AddAlternativeWriter.
|
||||||
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
||||||
m.parts = []*part{m.newPart(contentType, newCopier(body), settings)}
|
m.SetBodyWriter(contentType, newCopier(body), settings...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBodyWriter sets the body of the message. It can be useful with the
|
||||||
|
// text/template or html/template packages.
|
||||||
|
func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||||
|
m.parts = []*part{m.newPart(contentType, f, settings)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAlternative adds an alternative part to the message.
|
// AddAlternative adds an alternative part to the message.
|
||||||
@ -226,8 +238,8 @@ func (m *Message) newPart(contentType string, f func(io.Writer) error, settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A PartSetting can be used as an argument in Message.SetBody,
|
// A PartSetting can be used as an argument in Message.SetBody,
|
||||||
// Message.AddAlternative or Message.AddAlternativeWriter to configure the part
|
// Message.SetBodyWriter, Message.AddAlternative or Message.AddAlternativeWriter
|
||||||
// added to a message.
|
// to configure the part added to a message.
|
||||||
type PartSetting func(*part)
|
type PartSetting func(*part)
|
||||||
|
|
||||||
// SetPartEncoding sets the encoding of the part added to the message. By
|
// SetPartEncoding sets the encoding of the part added to the message. By
|
2
vendor/gopkg.in/gomail.v2/mime.go → vendor/gopkg.in/mail.v2/mime.go
generated
vendored
2
vendor/gopkg.in/gomail.v2/mime.go → vendor/gopkg.in/mail.v2/mime.go
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
// +build go1.5
|
// +build go1.5
|
||||||
|
|
||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mime"
|
"mime"
|
2
vendor/gopkg.in/gomail.v2/mime_go14.go → vendor/gopkg.in/mail.v2/mime_go14.go
generated
vendored
2
vendor/gopkg.in/gomail.v2/mime_go14.go → vendor/gopkg.in/mail.v2/mime_go14.go
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
// +build !go1.5
|
// +build !go1.5
|
||||||
|
|
||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import "gopkg.in/alexcesaro/quotedprintable.v3"
|
import "gopkg.in/alexcesaro/quotedprintable.v3"
|
||||||
|
|
8
vendor/gopkg.in/gomail.v2/send.go → vendor/gopkg.in/mail.v2/send.go
generated
vendored
8
vendor/gopkg.in/gomail.v2/send.go → vendor/gopkg.in/mail.v2/send.go
generated
vendored
@ -1,10 +1,10 @@
|
|||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
stdmail "net/mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sender is the interface that wraps the Send method.
|
// Sender is the interface that wraps the Send method.
|
||||||
@ -36,7 +36,7 @@ func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error {
|
|||||||
func Send(s Sender, msg ...*Message) error {
|
func Send(s Sender, msg ...*Message) error {
|
||||||
for i, m := range msg {
|
for i, m := range msg {
|
||||||
if err := send(s, m); err != nil {
|
if err := send(s, m); err != nil {
|
||||||
return fmt.Errorf("gomail: could not send email %d: %v", i+1, err)
|
return &SendError{Cause: err, Index: uint(i)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ func addAddress(list []string, addr string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseAddress(field string) (string, error) {
|
func parseAddress(field string) (string, error) {
|
||||||
addr, err := mail.ParseAddress(field)
|
addr, err := stdmail.ParseAddress(field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
|
return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
|
||||||
}
|
}
|
106
vendor/gopkg.in/gomail.v2/smtp.go → vendor/gopkg.in/mail.v2/smtp.go
generated
vendored
106
vendor/gopkg.in/gomail.v2/smtp.go → vendor/gopkg.in/mail.v2/smtp.go
generated
vendored
@ -1,4 +1,4 @@
|
|||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
@ -27,12 +27,26 @@ type Dialer struct {
|
|||||||
// most cases since the authentication mechanism should use the STARTTLS
|
// most cases since the authentication mechanism should use the STARTTLS
|
||||||
// extension instead.
|
// extension instead.
|
||||||
SSL bool
|
SSL bool
|
||||||
// TSLConfig represents the TLS configuration used for the TLS (when the
|
// TLSConfig represents the TLS configuration used for the TLS (when the
|
||||||
// STARTTLS extension is used) or SSL connection.
|
// STARTTLS extension is used) or SSL connection.
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
|
// StartTLSPolicy represents the TLS security level required to
|
||||||
|
// communicate with the SMTP server.
|
||||||
|
//
|
||||||
|
// This defaults to OpportunisticStartTLS for backwards compatibility,
|
||||||
|
// but we recommend MandatoryStartTLS for all modern SMTP servers.
|
||||||
|
//
|
||||||
|
// This option has no effect if SSL is set to true.
|
||||||
|
StartTLSPolicy StartTLSPolicy
|
||||||
// LocalName is the hostname sent to the SMTP server with the HELO command.
|
// LocalName is the hostname sent to the SMTP server with the HELO command.
|
||||||
// By default, "localhost" is sent.
|
// By default, "localhost" is sent.
|
||||||
LocalName string
|
LocalName string
|
||||||
|
// Timeout to use for read/write operations. Defaults to 10 seconds, can
|
||||||
|
// be set to 0 to disable timeouts.
|
||||||
|
Timeout time.Duration
|
||||||
|
// Whether we should retry mailing if the connection returned an error,
|
||||||
|
// defaults to true.
|
||||||
|
RetryFailure bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
|
// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
|
||||||
@ -44,6 +58,8 @@ func NewDialer(host string, port int, username, password string) *Dialer {
|
|||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
SSL: port == 465,
|
SSL: port == 465,
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
RetryFailure: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,10 +71,15 @@ func NewPlainDialer(host string, port int, username, password string) *Dialer {
|
|||||||
return NewDialer(host, port, username, password)
|
return NewDialer(host, port, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetDialTimeout specifies the DialTimeout function to establish a connection
|
||||||
|
// to the SMTP server. This can be used to override dialing in the case that a
|
||||||
|
// proxy or other special behavior is needed.
|
||||||
|
var NetDialTimeout = net.DialTimeout
|
||||||
|
|
||||||
// Dial dials and authenticates to an SMTP server. The returned SendCloser
|
// Dial dials and authenticates to an SMTP server. The returned SendCloser
|
||||||
// should be closed when done using it.
|
// should be closed when done using it.
|
||||||
func (d *Dialer) Dial() (SendCloser, error) {
|
func (d *Dialer) Dial() (SendCloser, error) {
|
||||||
conn, err := netDialTimeout("tcp", addr(d.Host, d.Port), 10*time.Second)
|
conn, err := NetDialTimeout("tcp", addr(d.Host, d.Port), d.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -72,14 +93,25 @@ func (d *Dialer) Dial() (SendCloser, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.Timeout > 0 {
|
||||||
|
conn.SetDeadline(time.Now().Add(d.Timeout))
|
||||||
|
}
|
||||||
|
|
||||||
if d.LocalName != "" {
|
if d.LocalName != "" {
|
||||||
if err := c.Hello(d.LocalName); err != nil {
|
if err := c.Hello(d.LocalName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !d.SSL {
|
if !d.SSL && d.StartTLSPolicy != NoStartTLS {
|
||||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
ok, _ := c.Extension("STARTTLS")
|
||||||
|
if !ok && d.StartTLSPolicy == MandatoryStartTLS {
|
||||||
|
err := StartTLSUnsupportedError{
|
||||||
|
Policy: d.StartTLSPolicy}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
if err := c.StartTLS(d.tlsConfig()); err != nil {
|
if err := c.StartTLS(d.tlsConfig()); err != nil {
|
||||||
c.Close()
|
c.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -111,7 +143,7 @@ func (d *Dialer) Dial() (SendCloser, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &smtpSender{c, d}, nil
|
return &smtpSender{c, conn, d}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dialer) tlsConfig() *tls.Config {
|
func (d *Dialer) tlsConfig() *tls.Config {
|
||||||
@ -121,6 +153,47 @@ func (d *Dialer) tlsConfig() *tls.Config {
|
|||||||
return d.TLSConfig
|
return d.TLSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartTLSPolicy constants are valid values for Dialer.StartTLSPolicy.
|
||||||
|
type StartTLSPolicy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OpportunisticStartTLS means that SMTP transactions are encrypted if
|
||||||
|
// STARTTLS is supported by the SMTP server. Otherwise, messages are
|
||||||
|
// sent in the clear. This is the default setting.
|
||||||
|
OpportunisticStartTLS StartTLSPolicy = iota
|
||||||
|
// MandatoryStartTLS means that SMTP transactions must be encrypted.
|
||||||
|
// SMTP transactions are aborted unless STARTTLS is supported by the
|
||||||
|
// SMTP server.
|
||||||
|
MandatoryStartTLS
|
||||||
|
// NoStartTLS means encryption is disabled and messages are sent in the
|
||||||
|
// clear.
|
||||||
|
NoStartTLS = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (policy *StartTLSPolicy) String() string {
|
||||||
|
switch *policy {
|
||||||
|
case OpportunisticStartTLS:
|
||||||
|
return "OpportunisticStartTLS"
|
||||||
|
case MandatoryStartTLS:
|
||||||
|
return "MandatoryStartTLS"
|
||||||
|
case NoStartTLS:
|
||||||
|
return "NoStartTLS"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("StartTLSPolicy:%v", *policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTLSUnsupportedError is returned by Dial when connecting to an SMTP
|
||||||
|
// server that does not support STARTTLS.
|
||||||
|
type StartTLSUnsupportedError struct {
|
||||||
|
Policy StartTLSPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StartTLSUnsupportedError) Error() string {
|
||||||
|
return "gomail: " + e.Policy.String() + " required, but " +
|
||||||
|
"SMTP server does not support STARTTLS"
|
||||||
|
}
|
||||||
|
|
||||||
func addr(host string, port int) string {
|
func addr(host string, port int) string {
|
||||||
return fmt.Sprintf("%s:%d", host, port)
|
return fmt.Sprintf("%s:%d", host, port)
|
||||||
}
|
}
|
||||||
@ -139,12 +212,29 @@ func (d *Dialer) DialAndSend(m ...*Message) error {
|
|||||||
|
|
||||||
type smtpSender struct {
|
type smtpSender struct {
|
||||||
smtpClient
|
smtpClient
|
||||||
|
conn net.Conn
|
||||||
d *Dialer
|
d *Dialer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *smtpSender) retryError(err error) bool {
|
||||||
|
if !c.d.RetryFailure {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return err == io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
|
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
|
||||||
|
if c.d.Timeout > 0 {
|
||||||
|
c.conn.SetDeadline(time.Now().Add(c.d.Timeout))
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.Mail(from); err != nil {
|
if err := c.Mail(from); err != nil {
|
||||||
if err == io.EOF {
|
if c.retryError(err) {
|
||||||
// This is probably due to a timeout, so reconnect and try again.
|
// This is probably due to a timeout, so reconnect and try again.
|
||||||
sc, derr := c.d.Dial()
|
sc, derr := c.d.Dial()
|
||||||
if derr == nil {
|
if derr == nil {
|
||||||
@ -154,6 +244,7 @@ func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +273,6 @@ func (c *smtpSender) Close() error {
|
|||||||
|
|
||||||
// Stubbed out for tests.
|
// Stubbed out for tests.
|
||||||
var (
|
var (
|
||||||
netDialTimeout = net.DialTimeout
|
|
||||||
tlsClient = tls.Client
|
tlsClient = tls.Client
|
||||||
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
|
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
|
||||||
return smtp.NewClient(conn, host)
|
return smtp.NewClient(conn, host)
|
13
vendor/gopkg.in/gomail.v2/writeto.go → vendor/gopkg.in/mail.v2/writeto.go
generated
vendored
13
vendor/gopkg.in/gomail.v2/writeto.go → vendor/gopkg.in/mail.v2/writeto.go
generated
vendored
@ -1,4 +1,4 @@
|
|||||||
package gomail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@ -28,15 +28,15 @@ func (w *messageWriter) writeMessage(m *Message) {
|
|||||||
w.writeHeaders(m.header)
|
w.writeHeaders(m.header)
|
||||||
|
|
||||||
if m.hasMixedPart() {
|
if m.hasMixedPart() {
|
||||||
w.openMultipart("mixed")
|
w.openMultipart("mixed", m.boundary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.hasRelatedPart() {
|
if m.hasRelatedPart() {
|
||||||
w.openMultipart("related")
|
w.openMultipart("related", m.boundary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.hasAlternativePart() {
|
if m.hasAlternativePart() {
|
||||||
w.openMultipart("alternative")
|
w.openMultipart("alternative", m.boundary)
|
||||||
}
|
}
|
||||||
for _, part := range m.parts {
|
for _, part := range m.parts {
|
||||||
w.writePart(part, m.charset)
|
w.writePart(part, m.charset)
|
||||||
@ -77,8 +77,11 @@ type messageWriter struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *messageWriter) openMultipart(mimeType string) {
|
func (w *messageWriter) openMultipart(mimeType, boundary string) {
|
||||||
mw := multipart.NewWriter(w)
|
mw := multipart.NewWriter(w)
|
||||||
|
if boundary != "" {
|
||||||
|
mw.SetBoundary(boundary)
|
||||||
|
}
|
||||||
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
|
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
|
||||||
w.writers[w.depth] = mw
|
w.writers[w.depth] = mw
|
||||||
|
|
Loading…
Reference in New Issue
Block a user