Merge remote-tracking branch 'grafana/master'
* grafana/master: (51 commits) changing callback fn into arrow functions for correct usage of this (#12673) Fix requested changes Update CHANGELOG.md Add support for interval in query variable Change to arrow functions Add graph_ctrl jest changelog: add notes about closing #12691 Update kbn.ts Add jest test file Id validation of CloudWatch GetMetricData changelog: adds note for #11487 Datasource for Grafana logging platform fix: postgres/mysql engine cache was not being used, fixes #12636 (#12642) added: replaces added to grafana fix: datasource search was not working properly docs: minor docs fix Fix label suggestions in Explore query field pluginloader: expose flot gauge plugin alert: add missing test after refactor Handle query string in storage public_url (#9351) (#12555) ...
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
|
||||
* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
|
||||
* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
|
||||
* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
|
||||
|
||||
### Minor
|
||||
|
||||
@@ -11,11 +13,13 @@
|
||||
* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
|
||||
* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
|
||||
* **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
|
||||
* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
|
||||
* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
|
||||
* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
|
||||
* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
|
||||
* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
|
||||
|
||||
# 5.2.2 (unreleased)
|
||||
|
||||
|
||||
Generated
+12
-3
@@ -32,6 +32,7 @@
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
"aws/ec2metadata",
|
||||
"aws/endpoints",
|
||||
@@ -43,6 +44,8 @@
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/ec2query",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
@@ -54,8 +57,8 @@
|
||||
"service/s3",
|
||||
"service/sts"
|
||||
]
|
||||
revision = "c7cd1ebe87257cde9b65112fc876b0339ea0ac30"
|
||||
version = "v1.13.49"
|
||||
revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
|
||||
version = "v1.14.12"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -424,6 +427,12 @@
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||
packages = ["."]
|
||||
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/smartystreets/assertions"
|
||||
packages = [
|
||||
@@ -670,6 +679,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
|
||||
inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ ignored = [
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
version = "1.12.65"
|
||||
version = "1.13.56"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
|
||||
@@ -330,6 +330,7 @@ func createPackage(options linuxPackageOptions) {
|
||||
name := "grafana"
|
||||
if enterprise {
|
||||
name += "-enterprise"
|
||||
args = append(args, "--replaces", "grafana")
|
||||
}
|
||||
args = append(args, "--name", name)
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ email = "email"
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=admins,dc=grafana,dc=org"
|
||||
org_role = "Admin"
|
||||
# To make user an instance admin (Grafana Admin) uncomment line below
|
||||
# grafana_admin = true
|
||||
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
|
||||
# org_id = 1
|
||||
|
||||
|
||||
+10
-5
@@ -1,11 +1,16 @@
|
||||
This folder contains useful scripts and configuration for...
|
||||
|
||||
* Configuring datasources in Grafana
|
||||
* Provision example dashboards in Grafana
|
||||
* Run preconfiured datasources as docker containers
|
||||
|
||||
want to know more? run setup!
|
||||
* Configuring dev datasources in Grafana
|
||||
* Configuring dev & test scenarios dashboards.
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
After restarting grafana server there should now be a number of datasources named `gdev-<type>` provisioned as well as a dashboard folder named `gdev dashboards`. This folder contains dashboard & panel features tests dashboards.
|
||||
|
||||
# Dev dashboards
|
||||
|
||||
Please update these dashboards or make new ones as new panels & dashboards features are developed or new bugs are found. The dashboards are located in the `devenv/dev-dashboards` folder.
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ datasources:
|
||||
isDefault: true
|
||||
url: http://localhost:9090
|
||||
|
||||
- name: gdev-testdata
|
||||
type: testdata
|
||||
|
||||
- name: gdev-influxdb
|
||||
type: influxdb
|
||||
access: proxy
|
||||
@@ -60,7 +63,8 @@ datasources:
|
||||
url: localhost:5432
|
||||
database: grafana
|
||||
user: grafana
|
||||
password: password
|
||||
secureJsonData:
|
||||
password: password
|
||||
jsonData:
|
||||
sslmode: "disable"
|
||||
|
||||
@@ -71,3 +75,4 @@ datasources:
|
||||
authType: credentials
|
||||
defaultRegion: eu-west-2
|
||||
|
||||
|
||||
|
||||
@@ -1,592 +0,0 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 59,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 9,
|
||||
"panels": [],
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"id": 12,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 1
|
||||
},
|
||||
"id": 5,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 5
|
||||
},
|
||||
"id": 7,
|
||||
"panels": [],
|
||||
"title": "Row",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 6
|
||||
},
|
||||
"id": 13,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
},
|
||||
"id": 11,
|
||||
"panels": [],
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 11
|
||||
},
|
||||
"id": 4,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 11
|
||||
},
|
||||
"id": 3,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Dashboard with rows",
|
||||
"uid": "1DdOzBNmk",
|
||||
"version": 5
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,574 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorValue": true,
|
||||
"colors": [
|
||||
"#299c46",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#d44a3a"
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"decimals": null,
|
||||
"description": "",
|
||||
"format": "ms",
|
||||
"gauge": {
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"show": false,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "postfix",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "prefix",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": false,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": true
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,2,3,4,5"
|
||||
}
|
||||
],
|
||||
"thresholds": "5,10",
|
||||
"title": "prefix 3 ms (green) postfixt + sparkline",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "80%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "avg"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorPrefix": false,
|
||||
"colorValue": true,
|
||||
"colors": [
|
||||
"#d44a3a",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#299c46"
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"decimals": null,
|
||||
"description": "",
|
||||
"format": "ms",
|
||||
"gauge": {
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"show": false,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": true,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": true
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,2,3,4,5"
|
||||
}
|
||||
],
|
||||
"thresholds": "5,10",
|
||||
"title": "3 ms (red) + full height sparkline",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "200%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "avg"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": true,
|
||||
"colorPrefix": false,
|
||||
"colorValue": false,
|
||||
"colors": [
|
||||
"#d44a3a",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#299c46"
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"decimals": null,
|
||||
"description": "",
|
||||
"format": "ms",
|
||||
"gauge": {
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"show": false,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": true,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": false
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,2,3,4,5"
|
||||
}
|
||||
],
|
||||
"thresholds": "5,10",
|
||||
"title": "3 ms + red background",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "200%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "avg"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorPrefix": false,
|
||||
"colorValue": true,
|
||||
"colors": [
|
||||
"#299c46",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#d44a3a"
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"decimals": null,
|
||||
"description": "",
|
||||
"format": "ms",
|
||||
"gauge": {
|
||||
"maxValue": 150,
|
||||
"minValue": 0,
|
||||
"show": true,
|
||||
"thresholdLabels": true,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 5,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": true,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": false
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10,20,80"
|
||||
}
|
||||
],
|
||||
"thresholds": "81,90",
|
||||
"title": "80 ms green gauge, thresholds 81, 90",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "80%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "current"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorPrefix": false,
|
||||
"colorValue": true,
|
||||
"colors": [
|
||||
"#299c46",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#d44a3a"
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"decimals": null,
|
||||
"description": "",
|
||||
"format": "ms",
|
||||
"gauge": {
|
||||
"maxValue": 150,
|
||||
"minValue": 0,
|
||||
"show": true,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 7
|
||||
},
|
||||
"id": 6,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": true,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": false
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10,20,80"
|
||||
}
|
||||
],
|
||||
"thresholds": "81,90",
|
||||
"title": "80 ms green gauge, thresholds 81, 90, no labels",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "80%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "current"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorPrefix": false,
|
||||
"colorValue": true,
|
||||
"colors": [
|
||||
"#299c46",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#d44a3a"
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"decimals": null,
|
||||
"description": "",
|
||||
"format": "ms",
|
||||
"gauge": {
|
||||
"maxValue": 150,
|
||||
"minValue": 0,
|
||||
"show": true,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": false
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 7
|
||||
},
|
||||
"id": 7,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": true,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": false
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10,20,80"
|
||||
}
|
||||
],
|
||||
"thresholds": "81,90",
|
||||
"title": "80 ms green gauge, thresholds 81, 90, no markers or labels",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "80%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "current"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"revision": 8,
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Singlestat",
|
||||
"uid": "singlestat",
|
||||
"version": 14
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"columns": [],
|
||||
"datasource": "gdev-testdata",
|
||||
"fontSize": "100%",
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"links": [],
|
||||
"pageSize": 10,
|
||||
"scroll": true,
|
||||
"showHeader": true,
|
||||
"sort": {
|
||||
"col": 0,
|
||||
"desc": true
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"alias": "Time",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"pattern": "Time",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "cell",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "ColorCell",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "currencyUSD"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "value",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "ColorValue",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "Bps"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": null,
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"decimals": 2,
|
||||
"pattern": "/.*/",
|
||||
"thresholds": [],
|
||||
"type": "number",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"alias": "server1",
|
||||
"expr": "",
|
||||
"format": "table",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0,20,10"
|
||||
},
|
||||
{
|
||||
"alias": "server2",
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0"
|
||||
}
|
||||
],
|
||||
"title": "Time series to rows (2 pages)",
|
||||
"transform": "timeseries_to_rows",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"text": "Avg",
|
||||
"value": "avg"
|
||||
},
|
||||
{
|
||||
"text": "Max",
|
||||
"value": "max"
|
||||
},
|
||||
{
|
||||
"text": "Current",
|
||||
"value": "current"
|
||||
}
|
||||
],
|
||||
"datasource": "gdev-testdata",
|
||||
"fontSize": "100%",
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"links": [],
|
||||
"pageSize": 10,
|
||||
"scroll": true,
|
||||
"showHeader": true,
|
||||
"sort": {
|
||||
"col": 0,
|
||||
"desc": true
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"alias": "Time",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"pattern": "Time",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "cell",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "ColorCell",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "currencyUSD"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "value",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "ColorValue",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "Bps"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": null,
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"decimals": 2,
|
||||
"pattern": "/.*/",
|
||||
"thresholds": [],
|
||||
"type": "number",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"alias": "server1",
|
||||
"expr": "",
|
||||
"format": "table",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0,20,10"
|
||||
},
|
||||
{
|
||||
"alias": "server2",
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0"
|
||||
}
|
||||
],
|
||||
"title": "Time series aggregations",
|
||||
"transform": "timeseries_aggregations",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"columns": [],
|
||||
"datasource": "gdev-testdata",
|
||||
"fontSize": "100%",
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 11
|
||||
},
|
||||
"id": 5,
|
||||
"links": [],
|
||||
"pageSize": null,
|
||||
"scroll": true,
|
||||
"showHeader": true,
|
||||
"sort": {
|
||||
"col": 0,
|
||||
"desc": true
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"alias": "Time",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"pattern": "Time",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "row",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "/Color/",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "currencyUSD"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": null,
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"decimals": 2,
|
||||
"pattern": "/.*/",
|
||||
"thresholds": [],
|
||||
"type": "number",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"alias": "ColorValue",
|
||||
"expr": "",
|
||||
"format": "table",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0,20,10"
|
||||
}
|
||||
],
|
||||
"title": "color row by threshold",
|
||||
"transform": "timeseries_to_columns",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"columns": [],
|
||||
"datasource": "gdev-testdata",
|
||||
"fontSize": "100%",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 2,
|
||||
"links": [],
|
||||
"pageSize": null,
|
||||
"scroll": true,
|
||||
"showHeader": true,
|
||||
"sort": {
|
||||
"col": 0,
|
||||
"desc": true
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"alias": "Time",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"pattern": "Time",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "cell",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "ColorCell",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "currencyUSD"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": "value",
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"decimals": 2,
|
||||
"mappingType": 1,
|
||||
"pattern": "ColorValue",
|
||||
"thresholds": [
|
||||
"5",
|
||||
"10"
|
||||
],
|
||||
"type": "number",
|
||||
"unit": "Bps"
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"colorMode": null,
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"decimals": 2,
|
||||
"pattern": "/.*/",
|
||||
"thresholds": [],
|
||||
"type": "number",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"alias": "ColorValue",
|
||||
"expr": "",
|
||||
"format": "table",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0,20,10"
|
||||
},
|
||||
{
|
||||
"alias": "ColorCell",
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "5,1,2,3,4,5,10,20"
|
||||
}
|
||||
],
|
||||
"title": "Column style thresholds & units",
|
||||
"transform": "timeseries_to_columns",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"revision": 8,
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Table",
|
||||
"uid": "pttable",
|
||||
"version": 1
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"revision": 2,
|
||||
"title": "TestData - Alerts",
|
||||
"title": "Alerting with TestData",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
@@ -161,7 +161,7 @@
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Grafana TestData",
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
+13
-7
@@ -1,4 +1,4 @@
|
||||
#/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
bulkDashboard() {
|
||||
|
||||
@@ -22,31 +22,37 @@ requiresJsonnet() {
|
||||
fi
|
||||
}
|
||||
|
||||
defaultDashboards() {
|
||||
devDashboards() {
|
||||
echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning"
|
||||
ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
|
||||
}
|
||||
|
||||
defaultDatasources() {
|
||||
echo "setting up all default datasources using provisioning"
|
||||
devDatasources() {
|
||||
echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning"
|
||||
|
||||
ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
|
||||
echo -e "\n"
|
||||
echo "Usage:"
|
||||
echo " bulk-dashboards - create and provisioning 400 dashboards"
|
||||
echo " no args - provisiong core datasources and dev dashboards"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo -e "------------------------------------------------------------------"
|
||||
echo -e "This script setups provisioning for dev datasources and dashboards"
|
||||
echo -e "------------------------------------------------------------------"
|
||||
echo -e "\n"
|
||||
|
||||
local cmd=$1
|
||||
|
||||
if [[ $cmd == "bulk-dashboards" ]]; then
|
||||
bulkDashboard
|
||||
else
|
||||
defaultDashboards
|
||||
defaultDatasources
|
||||
devDashboards
|
||||
devDatasources
|
||||
fi
|
||||
|
||||
if [[ -z "$cmd" ]]; then
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY htpasswd /etc/nginx/htpasswd
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0
|
||||
user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9.
|
||||
admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1
|
||||
@@ -13,7 +13,26 @@ http {
|
||||
listen 10080;
|
||||
|
||||
location /grafana/ {
|
||||
################################################################
|
||||
# Enable these settings to test with basic auth and an auth proxy header
|
||||
# the htpasswd file contains an admin user with password admin and
|
||||
# user1: grafana and user2: grafana
|
||||
################################################################
|
||||
|
||||
# auth_basic "Restricted Content";
|
||||
# auth_basic_user_file /etc/nginx/htpasswd;
|
||||
|
||||
################################################################
|
||||
# To use the auth proxy header, set the following in custom.ini:
|
||||
# [auth.proxy]
|
||||
# enabled = true
|
||||
# header_name = X-WEBAUTH-USER
|
||||
# header_property = username
|
||||
################################################################
|
||||
|
||||
# proxy_set_header X-WEBAUTH-USER $remote_user;
|
||||
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
|
||||
# [log]
|
||||
# filters = ldap:debug
|
||||
|
||||
[[servers]]
|
||||
# Ldap server host (specify multiple hosts space separated)
|
||||
host = "127.0.0.1"
|
||||
# Default port is 389 or 636 if use_ssl = true
|
||||
port = 389
|
||||
# Set to true if ldap server supports TLS
|
||||
use_ssl = false
|
||||
# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
|
||||
start_tls = false
|
||||
# set to true if you want to skip ssl cert validation
|
||||
ssl_skip_verify = false
|
||||
# set to the path to your root CA certificate or leave unset to use system defaults
|
||||
# root_ca_cert = "/path/to/certificate.crt"
|
||||
|
||||
# Search user bind dn
|
||||
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# Search user bind password
|
||||
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
|
||||
bind_password = 'grafana'
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
search_filter = "(cn=%s)"
|
||||
|
||||
# An array of base dns to search through
|
||||
search_base_dns = ["dc=grafana,dc=org"]
|
||||
|
||||
# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
|
||||
# This is done by enabling group_search_filter below. You must also set member_of= "cn"
|
||||
# in [servers.attributes] below.
|
||||
|
||||
# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
|
||||
# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
|
||||
# below in such a way that the user's recursive group membership is considered.
|
||||
#
|
||||
# Nested Groups + Active Directory (AD) Example:
|
||||
#
|
||||
# AD groups store the Distinguished Names (DNs) of members, so your filter must
|
||||
# recursively search your groups for the authenticating user's DN. For example:
|
||||
#
|
||||
# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
|
||||
# group_search_filter_user_attribute = "distinguishedName"
|
||||
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
||||
#
|
||||
# [servers.attributes]
|
||||
# ...
|
||||
# member_of = "distinguishedName"
|
||||
|
||||
## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
|
||||
# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
|
||||
## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
|
||||
## Defaults to the value of username in [server.attributes]
|
||||
## Valid options are any of your values in [servers.attributes]
|
||||
## If you are using nested groups you probably want to set this and member_of in
|
||||
## [servers.attributes] to "distinguishedName"
|
||||
# group_search_filter_user_attribute = "distinguishedName"
|
||||
## An array of the base DNs to search through for groups. Typically uses ou=groups
|
||||
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
||||
|
||||
# Specify names of the ldap attributes your ldap uses
|
||||
[servers.attributes]
|
||||
name = "givenName"
|
||||
surname = "sn"
|
||||
username = "cn"
|
||||
member_of = "memberOf"
|
||||
email = "email"
|
||||
|
||||
# Map ldap groups to grafana org roles
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
|
||||
org_role = "Admin"
|
||||
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
|
||||
# org_id = 1
|
||||
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=editors,ou=groups,dc=grafana,dc=org"
|
||||
org_role = "Editor"
|
||||
|
||||
[[servers.group_mappings]]
|
||||
# If you want to match all (or no ldap groups) then you can use wildcard
|
||||
group_dn = "*"
|
||||
org_role = "Viewer"
|
||||
@@ -14,12 +14,12 @@ After adding ldif files to `prepopulate`:
|
||||
|
||||
## Enabling LDAP in Grafana
|
||||
|
||||
The default `ldap.toml` file in `conf` has host set to `127.0.0.1` and port to set to 389 so all you need to do is enable it in the .ini file to get Grafana to use this block:
|
||||
Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
|
||||
|
||||
```ini
|
||||
[auth.ldap]
|
||||
enabled = true
|
||||
config_file = conf/ldap.toml
|
||||
config_file = conf/ldap_dev.toml
|
||||
; allow_sign_up = true
|
||||
```
|
||||
|
||||
@@ -43,6 +43,3 @@ editors
|
||||
|
||||
no groups
|
||||
ldap-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
+++
|
||||
title = "Playlist HTTP API "
|
||||
description = "Playlist Admin HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "playlist"]
|
||||
aliases = ["/http_api/playlist/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Playlist"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
# Playlist API
|
||||
|
||||
## Search Playlist
|
||||
|
||||
`GET /api/playlists`
|
||||
|
||||
Get all existing playlist for the current organization using pagination
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
GET /api/playlists HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
Querystring Parameters:
|
||||
|
||||
These parameters are used as querystring parameters.
|
||||
|
||||
- **query** - Limit response to playlist having a name like this value.
|
||||
- **limit** - Limit response to *X* number of playlist.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "my playlist",
|
||||
"interval": "5m"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get one playlist
|
||||
|
||||
`GET /api/playlists/:id`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
GET /api/playlists/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id" : 1,
|
||||
"name": "my playlist",
|
||||
"interval": "5m",
|
||||
"orgId": "my org",
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_id",
|
||||
"value": "3",
|
||||
"order": 1,
|
||||
"title":"my third dasboard"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "myTag",
|
||||
"order": 2,
|
||||
"title":"my other dasboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Get Playlist items
|
||||
|
||||
`GET /api/playlists/:id/items`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
GET /api/playlists/1/items HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_id",
|
||||
"value": "3",
|
||||
"order": 1,
|
||||
"title":"my third dasboard"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "myTag",
|
||||
"order": 2,
|
||||
"title":"my other dasboard"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get Playlist dashboards
|
||||
|
||||
`GET /api/playlists/:id/dashboards`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
GET /api/playlists/1/dashboards HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
{
|
||||
"id": 3,
|
||||
"title": "my third dasboard",
|
||||
"order": 1,
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title":"my other dasboard"
|
||||
"order": 2,
|
||||
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Create a playlist
|
||||
|
||||
`POST /api/playlists/`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
PUT /api/playlists/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
{
|
||||
"name": "my playlist",
|
||||
"interval": "5m",
|
||||
"items": [
|
||||
{
|
||||
"type": "dashboard_by_id",
|
||||
"value": "3",
|
||||
"order": 1,
|
||||
"title":"my third dasboard"
|
||||
},
|
||||
{
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "myTag",
|
||||
"order": 2,
|
||||
"title":"my other dasboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "my playlist",
|
||||
"interval": "5m"
|
||||
}
|
||||
```
|
||||
|
||||
## Update a playlist
|
||||
|
||||
`PUT /api/playlists/:id`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
PUT /api/playlists/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
{
|
||||
"name": "my playlist",
|
||||
"interval": "5m",
|
||||
"items": [
|
||||
{
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_id",
|
||||
"value": "3",
|
||||
"order": 1,
|
||||
"title":"my third dasboard"
|
||||
},
|
||||
{
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "myTag",
|
||||
"order": 2,
|
||||
"title":"my other dasboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id" : 1,
|
||||
"name": "my playlist",
|
||||
"interval": "5m",
|
||||
"orgId": "my org",
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_id",
|
||||
"value": "3",
|
||||
"order": 1,
|
||||
"title":"my third dasboard"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"playlistId": 1,
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "myTag",
|
||||
"order": 2,
|
||||
"title":"my other dasboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete a playlist
|
||||
|
||||
`DELETE /api/playlists/:id`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
DELETE /api/playlists/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{}
|
||||
```
|
||||
@@ -296,6 +296,12 @@ Set to `true` to automatically add new users to the main organization
|
||||
(id 1). When set to `false`, new users will automatically cause a new
|
||||
organization to be created for that new user.
|
||||
|
||||
### auto_assign_org_id
|
||||
|
||||
Set this value to automatically add new users to the provided org.
|
||||
This requires `auto_assign_org` to be set to `true`. Please make sure
|
||||
that this organization does already exists.
|
||||
|
||||
### auto_assign_org_role
|
||||
|
||||
The role new users will be assigned for the main organization (if the
|
||||
@@ -857,7 +863,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
Url to where Grafana will send PUT request with images
|
||||
|
||||
### public_url
|
||||
Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name.
|
||||
Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged.
|
||||
|
||||
### username
|
||||
basic auth username
|
||||
|
||||
@@ -23,8 +23,9 @@ specific configuration file (default: `/etc/grafana/ldap.toml`).
|
||||
### Example config
|
||||
|
||||
```toml
|
||||
# Set to true to log user information returned from LDAP
|
||||
verbose_logging = false
|
||||
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
|
||||
# [log]
|
||||
# filters = ldap:debug
|
||||
|
||||
[[servers]]
|
||||
# Ldap server host (specify multiple hosts space separated)
|
||||
@@ -73,6 +74,8 @@ email = "email"
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=admins,dc=grafana,dc=org"
|
||||
org_role = "Admin"
|
||||
# To make user an instance admin (Grafana Admin) uncomment line below
|
||||
# grafana_admin = true
|
||||
# The Grafana organization database id, optional, if left out the default org (id 1) will be used. Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
|
||||
# org_id = 1
|
||||
|
||||
@@ -132,6 +135,10 @@ Users page, this change will be reset the next time the user logs in. If you
|
||||
change the LDAP groups of a user, the change will take effect the next
|
||||
time the user logs in.
|
||||
|
||||
### Grafana Admin
|
||||
with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs &
|
||||
users.
|
||||
|
||||
### Priority
|
||||
The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ weight = 1
|
||||
# Variables
|
||||
|
||||
Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
|
||||
and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
|
||||
and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
|
||||
the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}
|
||||
|
||||
+1
-2
@@ -73,8 +73,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
|
||||
r.Get("/explore/", reqEditorRole, Index)
|
||||
r.Get("/explore/*", reqEditorRole, Index)
|
||||
r.Get("/explore", reqEditorRole, Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, Index)
|
||||
r.Get("/playlists/*", reqSignedIn, Index)
|
||||
|
||||
@@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
|
||||
|
||||
func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.Id = c.ParamsInt64(":id")
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return Error(500, "Failed to save playlist", err)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -35,6 +36,16 @@ var netClient = &http.Client{
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
func (u *WebdavUploader) PublicURL(filename string) string {
|
||||
if strings.Contains(u.public_url, "${file}") {
|
||||
return strings.Replace(u.public_url, "${file}", filename, -1)
|
||||
} else {
|
||||
publicURL, _ := url.Parse(u.public_url)
|
||||
publicURL.Path = path.Join(publicURL.Path, filename)
|
||||
return publicURL.String()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
|
||||
url, _ := url.Parse(u.url)
|
||||
filename := util.GetRandomString(20) + ".png"
|
||||
@@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
|
||||
}
|
||||
|
||||
if u.public_url != "" {
|
||||
publicURL, _ := url.Parse(u.public_url)
|
||||
publicURL.Path = path.Join(publicURL.Path, filename)
|
||||
return publicURL.String(), nil
|
||||
return u.PublicURL(filename), nil
|
||||
}
|
||||
|
||||
return url.String(), nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package imguploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
|
||||
So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPublicURL(t *testing.T) {
|
||||
Convey("Given a public URL with parameters, and no template", t, func() {
|
||||
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
|
||||
parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
|
||||
So(parsed.Path, ShouldEndWith, "fileyfile.png")
|
||||
})
|
||||
Convey("Given a public URL with parameters, and a template", t, func() {
|
||||
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
|
||||
So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,6 +72,13 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync isGrafanaAdmin permission
|
||||
if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
|
||||
if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = bus.Dispatch(&m.SyncTeamsCommand{
|
||||
User: cmd.Result,
|
||||
ExternalUser: extUser,
|
||||
|
||||
+4
-3
@@ -175,6 +175,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,18 +191,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
userQuery := &m.UpsertUserCommand{
|
||||
upsertUserCmd := &m.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: setting.LdapAllowSignup,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(userQuery)
|
||||
err := bus.Dispatch(upsertUserCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userQuery.Result, nil
|
||||
return upsertUserCmd.Result, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) serverBind() error {
|
||||
|
||||
@@ -44,9 +44,10 @@ type LdapAttributeMap struct {
|
||||
}
|
||||
|
||||
type LdapGroupToOrgRole struct {
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgId int64 `toml:"org_id"`
|
||||
OrgRole m.RoleType `toml:"org_role"`
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgId int64 `toml:"org_id"`
|
||||
IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability)
|
||||
OrgRole m.RoleType `toml:"org_role"`
|
||||
}
|
||||
|
||||
var LdapCfg LdapConfig
|
||||
|
||||
+42
-8
@@ -98,6 +98,10 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(result.Login, ShouldEqual, "torkelo")
|
||||
})
|
||||
|
||||
Convey("Should set isGrafanaAdmin to false by default", func() {
|
||||
So(result.IsAdmin, ShouldBeFalse)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@@ -223,8 +227,32 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("Should not update permissions unless specified", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateUserPermissionsCmd, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
|
||||
trueVal := true
|
||||
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
Convey("Should create user with admin set to true", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When calling SyncUser", t, func() {
|
||||
@@ -332,6 +360,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error {
|
||||
sc.updateUserPermissionsCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
|
||||
sc.getUserByAuthInfoQuery = cmd
|
||||
sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
|
||||
@@ -379,14 +412,15 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
|
||||
getUserOrgListQuery *m.GetUserOrgListQuery
|
||||
createUserCmd *m.CreateUserCommand
|
||||
addOrgUserCmd *m.AddOrgUserCommand
|
||||
updateOrgUserCmd *m.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *m.RemoveOrgUserCommand
|
||||
updateUserCmd *m.UpdateUserCommand
|
||||
setUsingOrgCmd *m.SetUsingOrgCommand
|
||||
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
|
||||
getUserOrgListQuery *m.GetUserOrgListQuery
|
||||
createUserCmd *m.CreateUserCommand
|
||||
addOrgUserCmd *m.AddOrgUserCommand
|
||||
updateOrgUserCmd *m.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *m.RemoveOrgUserCommand
|
||||
updateUserCmd *m.UpdateUserCommand
|
||||
setUsingOrgCmd *m.SetUsingOrgCommand
|
||||
updateUserPermissionsCmd *m.UpdateUserPermissionsCommand
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userQueryReturns(user *m.User) {
|
||||
|
||||
@@ -44,6 +44,7 @@ var (
|
||||
M_Alerting_Notification_Sent *prometheus.CounterVec
|
||||
M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
|
||||
M_Aws_CloudWatch_ListMetrics prometheus.Counter
|
||||
M_Aws_CloudWatch_GetMetricData prometheus.Counter
|
||||
M_DB_DataSource_QueryById prometheus.Counter
|
||||
|
||||
// Timers
|
||||
@@ -218,6 +219,12 @@ func init() {
|
||||
Namespace: exporterName,
|
||||
})
|
||||
|
||||
M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: "aws_cloudwatch_get_metric_data_total",
|
||||
Help: "counter for getting metric data time series from aws",
|
||||
Namespace: exporterName,
|
||||
})
|
||||
|
||||
M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: "db_datasource_query_by_id_total",
|
||||
Help: "counter for getting datasource by id",
|
||||
@@ -307,6 +314,7 @@ func initMetricVars() {
|
||||
M_Alerting_Notification_Sent,
|
||||
M_Aws_CloudWatch_GetMetricStatistics,
|
||||
M_Aws_CloudWatch_ListMetrics,
|
||||
M_Aws_CloudWatch_GetMetricData,
|
||||
M_DB_DataSource_QueryById,
|
||||
M_Alerting_Active_Alerts,
|
||||
M_StatTotal_Dashboards,
|
||||
|
||||
@@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard
|
||||
|
||||
type UpdatePlaylistCommand struct {
|
||||
OrgId int64 `json:"-"`
|
||||
Id int64 `json:"id" binding:"Required"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Interval string `json:"interval"`
|
||||
Items []PlaylistItemDTO `json:"items"`
|
||||
|
||||
@@ -13,14 +13,15 @@ type UserAuth struct {
|
||||
}
|
||||
|
||||
type ExternalUserInfo struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Groups []string
|
||||
OrgRoles map[int64]RoleType
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Groups []string
|
||||
OrgRoles map[int64]RoleType
|
||||
IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync)
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
|
||||
@@ -17,11 +17,14 @@ import (
|
||||
plugin "github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
// DataSourcePlugin contains all metadata about a datasource plugin
|
||||
type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
Explore bool `json:"explore"`
|
||||
Logs bool `json:"logs"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
|
||||
@@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
alert.name,
|
||||
alert.state,
|
||||
alert.new_state_date,
|
||||
alert.eval_data,
|
||||
alert.eval_date,
|
||||
alert.execution_error,
|
||||
dashboard.uid as dashboard_uid,
|
||||
|
||||
@@ -13,7 +13,7 @@ func mockTimeNow() {
|
||||
var timeSeed int64
|
||||
timeNow = func() time.Time {
|
||||
fakeNow := time.Unix(timeSeed, 0)
|
||||
timeSeed += 1
|
||||
timeSeed++
|
||||
return fakeNow
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
InitTestDB(t)
|
||||
|
||||
testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
|
||||
|
||||
evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`))
|
||||
items := []*m.Alert{
|
||||
{
|
||||
PanelId: 1,
|
||||
@@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
Message: "Alerting message",
|
||||
Settings: simplejson.New(),
|
||||
Frequency: 1,
|
||||
EvalData: evalData,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
|
||||
alert := alertQuery.Result[0]
|
||||
So(err2, ShouldBeNil)
|
||||
So(alert.Id, ShouldBeGreaterThan, 0)
|
||||
So(alert.DashboardId, ShouldEqual, testDash.Id)
|
||||
So(alert.PanelId, ShouldEqual, 1)
|
||||
So(alert.Name, ShouldEqual, "Alerting title")
|
||||
So(alert.State, ShouldEqual, "pending")
|
||||
So(alert.NewStateDate, ShouldNotBeNil)
|
||||
So(alert.EvalData, ShouldNotBeNil)
|
||||
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
|
||||
So(alert.EvalDate, ShouldNotBeNil)
|
||||
So(alert.ExecutionError, ShouldEqual, "")
|
||||
So(alert.DashboardUid, ShouldNotBeNil)
|
||||
So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
|
||||
})
|
||||
|
||||
Convey("Viewer cannot read alerts", func() {
|
||||
|
||||
@@ -387,6 +387,7 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF
|
||||
|
||||
func createUser(name string, role string, isAdmin bool) m.User {
|
||||
setting.AutoAssignOrg = true
|
||||
setting.AutoAssignOrgId = 1
|
||||
setting.AutoAssignOrgRole = role
|
||||
|
||||
currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
|
||||
|
||||
@@ -17,6 +17,7 @@ func TestAccountDataAccess(t *testing.T) {
|
||||
|
||||
Convey("Given single org mode", func() {
|
||||
setting.AutoAssignOrg = true
|
||||
setting.AutoAssignOrgId = 1
|
||||
setting.AutoAssignOrgRole = "Viewer"
|
||||
|
||||
Convey("Users should be added to default organization", func() {
|
||||
|
||||
@@ -42,16 +42,23 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
|
||||
var org m.Org
|
||||
|
||||
if setting.AutoAssignOrg {
|
||||
// right now auto assign to org with id 1
|
||||
has, err := sess.Where("id=?", 1).Get(&org)
|
||||
has, err := sess.Where("id=?", setting.AutoAssignOrgId).Get(&org)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if has {
|
||||
return org.Id, nil
|
||||
} else {
|
||||
if setting.AutoAssignOrgId == 1 {
|
||||
org.Name = "Main Org."
|
||||
org.Id = int64(setting.AutoAssignOrgId)
|
||||
} else {
|
||||
sqlog.Info("Could not create user: organization id %v does not exist",
|
||||
setting.AutoAssignOrgId)
|
||||
return 0, fmt.Errorf("Could not create user: organization id %v does not exist",
|
||||
setting.AutoAssignOrgId)
|
||||
}
|
||||
}
|
||||
org.Name = "Main Org."
|
||||
org.Id = 1
|
||||
} else {
|
||||
org.Name = cmd.OrgName
|
||||
if len(org.Name) == 0 {
|
||||
|
||||
@@ -100,6 +100,7 @@ var (
|
||||
AllowUserSignUp bool
|
||||
AllowUserOrgCreate bool
|
||||
AutoAssignOrg bool
|
||||
AutoAssignOrgId int
|
||||
AutoAssignOrgRole string
|
||||
VerifyEmailEnabled bool
|
||||
LoginHint string
|
||||
@@ -592,6 +593,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
|
||||
AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
|
||||
AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
|
||||
AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1)
|
||||
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
|
||||
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
||||
LoginHint = users.Key("login_hint").String()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
@@ -88,48 +89,67 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
resCh := make(chan *tsdb.QueryResult, 1)
|
||||
eg, ectx := errgroup.WithContext(ctx)
|
||||
|
||||
currentlyExecuting := 0
|
||||
getMetricDataQueries := make(map[string]map[string]*CloudWatchQuery)
|
||||
for i, model := range queryContext.Queries {
|
||||
queryType := model.Model.Get("type").MustString()
|
||||
if queryType != "timeSeriesQuery" && queryType != "" {
|
||||
continue
|
||||
}
|
||||
currentlyExecuting++
|
||||
go func(refId string, index int) {
|
||||
queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext)
|
||||
currentlyExecuting--
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
queryRes.RefId = refId
|
||||
resCh <- queryRes
|
||||
|
||||
query, err := parseQuery(queryContext.Queries[i].Model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query.RefId = queryContext.Queries[i].RefId
|
||||
|
||||
if query.Id != "" {
|
||||
if _, ok := getMetricDataQueries[query.Region]; !ok {
|
||||
getMetricDataQueries[query.Region] = make(map[string]*CloudWatchQuery)
|
||||
}
|
||||
}(model.RefId, i)
|
||||
getMetricDataQueries[query.Region][query.Id] = query
|
||||
continue
|
||||
}
|
||||
|
||||
if query.Id == "" && query.Expression != "" {
|
||||
return nil, fmt.Errorf("Invalid query: id should be set if using expression")
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
queryRes, err := e.executeQuery(ectx, query, queryContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Results[queryRes.RefId] = queryRes
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for currentlyExecuting != 0 {
|
||||
select {
|
||||
case res := <-resCh:
|
||||
result.Results[res.RefId] = res
|
||||
case err := <-errCh:
|
||||
return result, err
|
||||
case <-ctx.Done():
|
||||
return result, ctx.Err()
|
||||
if len(getMetricDataQueries) > 0 {
|
||||
for region, getMetricDataQuery := range getMetricDataQueries {
|
||||
q := getMetricDataQuery
|
||||
eg.Go(func() error {
|
||||
queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, queryRes := range queryResponses {
|
||||
result.Results[queryRes.RefId] = queryRes
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
|
||||
query, err := parseQuery(parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
|
||||
client, err := e.getClient(query.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -201,6 +221,139 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl
|
||||
return queryRes, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, region string, queries map[string]*CloudWatchQuery, queryContext *tsdb.TsdbQuery) ([]*tsdb.QueryResult, error) {
|
||||
queryResponses := make([]*tsdb.QueryResult, 0)
|
||||
|
||||
// validate query
|
||||
for _, query := range queries {
|
||||
if !(len(query.Statistics) == 1 && len(query.ExtendedStatistics) == 0) &&
|
||||
!(len(query.Statistics) == 0 && len(query.ExtendedStatistics) == 1) {
|
||||
return queryResponses, errors.New("Statistics count should be 1")
|
||||
}
|
||||
}
|
||||
|
||||
client, err := e.getClient(region)
|
||||
if err != nil {
|
||||
return queryResponses, err
|
||||
}
|
||||
|
||||
startTime, err := queryContext.TimeRange.ParseFrom()
|
||||
if err != nil {
|
||||
return queryResponses, err
|
||||
}
|
||||
|
||||
endTime, err := queryContext.TimeRange.ParseTo()
|
||||
if err != nil {
|
||||
return queryResponses, err
|
||||
}
|
||||
|
||||
params := &cloudwatch.GetMetricDataInput{
|
||||
StartTime: aws.Time(startTime),
|
||||
EndTime: aws.Time(endTime),
|
||||
ScanBy: aws.String("TimestampAscending"),
|
||||
}
|
||||
for _, query := range queries {
|
||||
// 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600
|
||||
if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
|
||||
return nil, errors.New("too long query period")
|
||||
}
|
||||
|
||||
mdq := &cloudwatch.MetricDataQuery{
|
||||
Id: aws.String(query.Id),
|
||||
ReturnData: aws.Bool(query.ReturnData),
|
||||
}
|
||||
if query.Expression != "" {
|
||||
mdq.Expression = aws.String(query.Expression)
|
||||
} else {
|
||||
mdq.MetricStat = &cloudwatch.MetricStat{
|
||||
Metric: &cloudwatch.Metric{
|
||||
Namespace: aws.String(query.Namespace),
|
||||
MetricName: aws.String(query.MetricName),
|
||||
},
|
||||
Period: aws.Int64(int64(query.Period)),
|
||||
}
|
||||
for _, d := range query.Dimensions {
|
||||
mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions,
|
||||
&cloudwatch.Dimension{
|
||||
Name: d.Name,
|
||||
Value: d.Value,
|
||||
})
|
||||
}
|
||||
if len(query.Statistics) == 1 {
|
||||
mdq.MetricStat.Stat = query.Statistics[0]
|
||||
} else {
|
||||
mdq.MetricStat.Stat = query.ExtendedStatistics[0]
|
||||
}
|
||||
}
|
||||
params.MetricDataQueries = append(params.MetricDataQueries, mdq)
|
||||
}
|
||||
|
||||
nextToken := ""
|
||||
mdr := make(map[string]*cloudwatch.MetricDataResult)
|
||||
for {
|
||||
if nextToken != "" {
|
||||
params.NextToken = aws.String(nextToken)
|
||||
}
|
||||
resp, err := client.GetMetricDataWithContext(ctx, params)
|
||||
if err != nil {
|
||||
return queryResponses, err
|
||||
}
|
||||
metrics.M_Aws_CloudWatch_GetMetricData.Add(float64(len(params.MetricDataQueries)))
|
||||
|
||||
for _, r := range resp.MetricDataResults {
|
||||
if _, ok := mdr[*r.Id]; !ok {
|
||||
mdr[*r.Id] = r
|
||||
} else {
|
||||
mdr[*r.Id].Timestamps = append(mdr[*r.Id].Timestamps, r.Timestamps...)
|
||||
mdr[*r.Id].Values = append(mdr[*r.Id].Values, r.Values...)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.NextToken == nil || *resp.NextToken == "" {
|
||||
break
|
||||
}
|
||||
nextToken = *resp.NextToken
|
||||
}
|
||||
|
||||
for i, r := range mdr {
|
||||
if *r.StatusCode != "Complete" {
|
||||
return queryResponses, fmt.Errorf("Part of query is failed: %s", *r.StatusCode)
|
||||
}
|
||||
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
queryRes.RefId = queries[i].RefId
|
||||
query := queries[*r.Id]
|
||||
|
||||
series := tsdb.TimeSeries{
|
||||
Tags: map[string]string{},
|
||||
Points: make([]tsdb.TimePoint, 0),
|
||||
}
|
||||
for _, d := range query.Dimensions {
|
||||
series.Tags[*d.Name] = *d.Value
|
||||
}
|
||||
s := ""
|
||||
if len(query.Statistics) == 1 {
|
||||
s = *query.Statistics[0]
|
||||
} else {
|
||||
s = *query.ExtendedStatistics[0]
|
||||
}
|
||||
series.Name = formatAlias(query, s, series.Tags)
|
||||
|
||||
for j, t := range r.Timestamps {
|
||||
expectedTimestamp := r.Timestamps[j].Add(time.Duration(query.Period) * time.Second)
|
||||
if j > 0 && expectedTimestamp.Before(*t) {
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000)))
|
||||
}
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*r.Values[j]), float64((*t).Unix())*1000))
|
||||
}
|
||||
|
||||
queryRes.Series = append(queryRes.Series, &series)
|
||||
queryResponses = append(queryResponses, queryRes)
|
||||
}
|
||||
|
||||
return queryResponses, nil
|
||||
}
|
||||
|
||||
func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) {
|
||||
var result []*cloudwatch.Dimension
|
||||
|
||||
@@ -257,6 +410,9 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := model.Get("id").MustString("")
|
||||
expression := model.Get("expression").MustString("")
|
||||
|
||||
dimensions, err := parseDimensions(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -295,6 +451,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
|
||||
alias = "{{metric}}_{{stat}}"
|
||||
}
|
||||
|
||||
returnData := model.Get("returnData").MustBool(false)
|
||||
highResolution := model.Get("highResolution").MustBool(false)
|
||||
|
||||
return &CloudWatchQuery{
|
||||
@@ -306,11 +463,18 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
|
||||
ExtendedStatistics: aws.StringSlice(extendedStatistics),
|
||||
Period: period,
|
||||
Alias: alias,
|
||||
Id: id,
|
||||
Expression: expression,
|
||||
ReturnData: returnData,
|
||||
HighResolution: highResolution,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
|
||||
if len(query.Id) > 0 && len(query.Expression) > 0 {
|
||||
return query.Id
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
data["region"] = query.Region
|
||||
data["namespace"] = query.Namespace
|
||||
@@ -338,6 +502,7 @@ func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]stri
|
||||
func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) {
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
queryRes.RefId = query.RefId
|
||||
var value float64
|
||||
for _, s := range append(query.Statistics, query.ExtendedStatistics...) {
|
||||
series := tsdb.TimeSeries{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type CloudWatchQuery struct {
|
||||
RefId string
|
||||
Region string
|
||||
Namespace string
|
||||
MetricName string
|
||||
@@ -13,5 +14,8 @@ type CloudWatchQuery struct {
|
||||
ExtendedStatistics []*string
|
||||
Period int
|
||||
Alias string
|
||||
Id string
|
||||
Expression string
|
||||
ReturnData bool
|
||||
HighResolution bool
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
|
||||
engine.SetMaxOpenConns(10)
|
||||
engine.SetMaxIdleConns(10)
|
||||
|
||||
engineCache.versions[dsInfo.Id] = dsInfo.Version
|
||||
engineCache.cache[dsInfo.Id] = engine
|
||||
e.XormEngine = engine
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -21,7 +21,7 @@ func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
tsdb.RegisterTsdbQueryEndpoint("grafana-testdata-datasource", NewTestDataExecutor)
|
||||
tsdb.RegisterTsdbQueryEndpoint("testdata", NewTestDataExecutor)
|
||||
}
|
||||
|
||||
func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import Select from 'react-select';
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import { decodePathComponent } from 'app/core/utils/location_util';
|
||||
import { parse as parseDate } from 'app/core/utils/datemath';
|
||||
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
import QueryRows from './QueryRows';
|
||||
import Graph from './Graph';
|
||||
import Logs from './Logs';
|
||||
import Table from './Table';
|
||||
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
import { decodePathComponent } from 'app/core/utils/location_util';
|
||||
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
|
||||
function makeTimeSeriesList(dataList, options) {
|
||||
return dataList.map((seriesData, index) => {
|
||||
@@ -30,74 +34,136 @@ function makeTimeSeriesList(dataList, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseInitialState(initial) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodePathComponent(initial));
|
||||
return {
|
||||
queries: parsed.queries.map(q => q.query),
|
||||
range: parsed.range,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { queries: [], range: DEFAULT_RANGE };
|
||||
function parseInitialState(initial: string | undefined) {
|
||||
if (initial) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodePathComponent(initial));
|
||||
return {
|
||||
datasource: parsed.datasource,
|
||||
queries: parsed.queries.map(q => q.query),
|
||||
range: parsed.range,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
||||
}
|
||||
|
||||
interface IExploreState {
|
||||
datasource: any;
|
||||
datasourceError: any;
|
||||
datasourceLoading: any;
|
||||
datasourceLoading: boolean | null;
|
||||
datasourceMissing: boolean;
|
||||
graphResult: any;
|
||||
initialDatasource?: string;
|
||||
latency: number;
|
||||
loading: any;
|
||||
logsResult: any;
|
||||
queries: any;
|
||||
queryError: any;
|
||||
range: any;
|
||||
requestOptions: any;
|
||||
showingGraph: boolean;
|
||||
showingLogs: boolean;
|
||||
showingTable: boolean;
|
||||
supportsGraph: boolean | null;
|
||||
supportsLogs: boolean | null;
|
||||
supportsTable: boolean | null;
|
||||
tableResult: any;
|
||||
}
|
||||
|
||||
// @observer
|
||||
export class Explore extends React.Component<any, IExploreState> {
|
||||
datasourceSrv: DatasourceSrv;
|
||||
el: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { range, queries } = parseInitialState(props.routeParams.initial);
|
||||
const { datasource, queries, range } = parseInitialState(props.routeParams.state);
|
||||
this.state = {
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
datasourceLoading: true,
|
||||
datasourceLoading: null,
|
||||
datasourceMissing: false,
|
||||
graphResult: null,
|
||||
initialDatasource: datasource,
|
||||
latency: 0,
|
||||
loading: false,
|
||||
logsResult: null,
|
||||
queries: ensureQueries(queries),
|
||||
queryError: null,
|
||||
range: range || { ...DEFAULT_RANGE },
|
||||
requestOptions: null,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
tableResult: null,
|
||||
...props.initialState,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const datasource = await this.props.datasourceSrv.get();
|
||||
const testResult = await datasource.testDatasource();
|
||||
if (testResult.status === 'success') {
|
||||
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
|
||||
const { datasourceSrv } = this.props;
|
||||
const { initialDatasource } = this.state;
|
||||
if (!datasourceSrv) {
|
||||
throw new Error('No datasource service passed as props.');
|
||||
}
|
||||
const datasources = datasourceSrv.getExploreSources();
|
||||
if (datasources.length > 0) {
|
||||
this.setState({ datasourceLoading: true });
|
||||
// Priority: datasource in url, default datasource, first explore datasource
|
||||
let datasource;
|
||||
if (initialDatasource) {
|
||||
datasource = await datasourceSrv.get(initialDatasource);
|
||||
} else {
|
||||
datasource = await datasourceSrv.get();
|
||||
}
|
||||
if (!datasource.meta.explore) {
|
||||
datasource = await datasourceSrv.get(datasources[0].name);
|
||||
}
|
||||
this.setDatasource(datasource);
|
||||
} else {
|
||||
this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
|
||||
this.setState({ datasourceMissing: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch(error) {
|
||||
this.setState({ datasourceError: error });
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
async setDatasource(datasource) {
|
||||
const supportsGraph = datasource.meta.metrics;
|
||||
const supportsLogs = datasource.meta.logs;
|
||||
const supportsTable = datasource.meta.metrics;
|
||||
let datasourceError = null;
|
||||
|
||||
try {
|
||||
const testResult = await datasource.testDatasource();
|
||||
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||
} catch (error) {
|
||||
datasourceError = (error && error.statusText) || error;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
datasource,
|
||||
datasourceError,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
datasourceLoading: false,
|
||||
},
|
||||
() => datasourceError === null && this.handleSubmit()
|
||||
);
|
||||
}
|
||||
|
||||
getRef = el => {
|
||||
this.el = el;
|
||||
};
|
||||
|
||||
handleAddQueryRow = index => {
|
||||
const { queries } = this.state;
|
||||
const nextQueries = [
|
||||
@@ -108,6 +174,19 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
this.setState({ queries: nextQueries });
|
||||
};
|
||||
|
||||
handleChangeDatasource = async option => {
|
||||
this.setState({
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
datasourceLoading: true,
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
});
|
||||
const datasource = await this.props.datasourceSrv.get(option.value);
|
||||
this.setDatasource(datasource);
|
||||
};
|
||||
|
||||
handleChangeQuery = (query, index) => {
|
||||
const { queries } = this.state;
|
||||
const nextQuery = {
|
||||
@@ -138,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
this.setState(state => ({ showingGraph: !state.showingGraph }));
|
||||
};
|
||||
|
||||
handleClickLogsButton = () => {
|
||||
this.setState(state => ({ showingLogs: !state.showingLogs }));
|
||||
};
|
||||
|
||||
handleClickSplit = () => {
|
||||
const { onChangeSplit } = this.props;
|
||||
if (onChangeSplit) {
|
||||
@@ -159,29 +242,45 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
const { showingGraph, showingTable } = this.state;
|
||||
if (showingTable) {
|
||||
const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
|
||||
if (showingTable && supportsTable) {
|
||||
this.runTableQuery();
|
||||
}
|
||||
if (showingGraph) {
|
||||
if (showingGraph && supportsGraph) {
|
||||
this.runGraphQuery();
|
||||
}
|
||||
if (showingLogs && supportsLogs) {
|
||||
this.runLogsQuery();
|
||||
}
|
||||
};
|
||||
|
||||
async runGraphQuery() {
|
||||
buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
|
||||
const { datasource, queries, range } = this.state;
|
||||
const resolution = this.el.offsetWidth;
|
||||
const absoluteRange = {
|
||||
from: parseDate(range.from, false),
|
||||
to: parseDate(range.to, true),
|
||||
};
|
||||
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
||||
const targets = queries.map(q => ({
|
||||
...targetOptions,
|
||||
expr: q.query,
|
||||
}));
|
||||
return {
|
||||
interval,
|
||||
range,
|
||||
targets,
|
||||
};
|
||||
}
|
||||
|
||||
async runGraphQuery() {
|
||||
const { datasource, queries } = this.state;
|
||||
if (!hasQuery(queries)) {
|
||||
return;
|
||||
}
|
||||
this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
|
||||
const now = Date.now();
|
||||
const options = buildQueryOptions({
|
||||
format: 'time_series',
|
||||
interval: datasource.interval,
|
||||
instant: false,
|
||||
range,
|
||||
queries: queries.map(q => q.query),
|
||||
});
|
||||
const options = this.buildQueryOptions({ format: 'time_series', instant: false });
|
||||
try {
|
||||
const res = await datasource.query(options);
|
||||
const result = makeTimeSeriesList(res.data, options);
|
||||
@@ -195,18 +294,15 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
}
|
||||
|
||||
async runTableQuery() {
|
||||
const { datasource, queries, range } = this.state;
|
||||
const { datasource, queries } = this.state;
|
||||
if (!hasQuery(queries)) {
|
||||
return;
|
||||
}
|
||||
this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
|
||||
const now = Date.now();
|
||||
const options = buildQueryOptions({
|
||||
const options = this.buildQueryOptions({
|
||||
format: 'table',
|
||||
interval: datasource.interval,
|
||||
instant: true,
|
||||
range,
|
||||
queries: queries.map(q => q.query),
|
||||
});
|
||||
try {
|
||||
const res = await datasource.query(options);
|
||||
@@ -220,35 +316,71 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
}
|
||||
}
|
||||
|
||||
async runLogsQuery() {
|
||||
const { datasource, queries } = this.state;
|
||||
if (!hasQuery(queries)) {
|
||||
return;
|
||||
}
|
||||
this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
|
||||
const now = Date.now();
|
||||
const options = this.buildQueryOptions({
|
||||
format: 'logs',
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await datasource.query(options);
|
||||
const logsData = res.data;
|
||||
const latency = Date.now() - now;
|
||||
this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.setState({ loading: false, queryError });
|
||||
}
|
||||
}
|
||||
|
||||
request = url => {
|
||||
const { datasource } = this.state;
|
||||
return datasource.metadataRequest(url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { position, split } = this.props;
|
||||
const { datasourceSrv, position, split } = this.props;
|
||||
const {
|
||||
datasource,
|
||||
datasourceError,
|
||||
datasourceLoading,
|
||||
datasourceMissing,
|
||||
graphResult,
|
||||
latency,
|
||||
loading,
|
||||
logsResult,
|
||||
queries,
|
||||
queryError,
|
||||
range,
|
||||
requestOptions,
|
||||
showingGraph,
|
||||
showingLogs,
|
||||
showingTable,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
tableResult,
|
||||
} = this.state;
|
||||
const showingBoth = showingGraph && showingTable;
|
||||
const graphHeight = showingBoth ? '200px' : '400px';
|
||||
const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
|
||||
const logsButtonActive = showingLogs ? 'active' : '';
|
||||
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
const datasources = datasourceSrv.getExploreSources().map(ds => ({
|
||||
value: ds.name,
|
||||
label: ds.name,
|
||||
}));
|
||||
const selectedDatasource = datasource ? datasource.name : undefined;
|
||||
|
||||
return (
|
||||
<div className={exploreClass}>
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
<div className="navbar">
|
||||
{position === 'left' ? (
|
||||
<div>
|
||||
@@ -264,6 +396,18 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!datasourceMissing ? (
|
||||
<div className="navbar-buttons">
|
||||
<Select
|
||||
className="datasource-picker"
|
||||
clearable={false}
|
||||
onChange={this.handleChangeDatasource}
|
||||
options={datasources}
|
||||
placeholder="Loading datasources..."
|
||||
value={selectedDatasource}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="navbar__spacer" />
|
||||
{position === 'left' && !split ? (
|
||||
<div className="navbar-buttons">
|
||||
@@ -273,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="navbar-buttons">
|
||||
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
{supportsGraph ? (
|
||||
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{supportsTable ? (
|
||||
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
) : null}
|
||||
{supportsLogs ? (
|
||||
<button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
|
||||
Logs
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<TimePicker range={range} onChangeTime={this.handleChangeTime} />
|
||||
<div className="navbar-buttons relative">
|
||||
@@ -291,13 +444,15 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
|
||||
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
||||
|
||||
{datasourceError ? (
|
||||
<div className="explore-container" title={datasourceError}>
|
||||
Error connecting to datasource.
|
||||
</div>
|
||||
{datasourceMissing ? (
|
||||
<div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
|
||||
) : null}
|
||||
|
||||
{datasource ? (
|
||||
{datasourceError ? (
|
||||
<div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
|
||||
) : null}
|
||||
|
||||
{datasource && !datasourceError ? (
|
||||
<div className="explore-container">
|
||||
<QueryRows
|
||||
queries={queries}
|
||||
@@ -309,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
/>
|
||||
{queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
|
||||
<main className="m-t-2">
|
||||
{showingGraph ? (
|
||||
{supportsGraph && showingGraph ? (
|
||||
<Graph
|
||||
data={graphResult}
|
||||
id={`explore-graph-${position}`}
|
||||
@@ -318,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
split={split}
|
||||
/>
|
||||
) : null}
|
||||
{showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
|
||||
{supportsTable && showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
|
||||
{supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
|
||||
</main>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function({ value }) {
|
||||
return (
|
||||
<div>
|
||||
<pre>{JSON.stringify(value, undefined, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
|
||||
import { LogsModel, LogRow } from 'app/core/logs_model';
|
||||
|
||||
interface LogsProps {
|
||||
className?: string;
|
||||
data: LogsModel;
|
||||
}
|
||||
|
||||
const EXAMPLE_QUERY = '{job="default/prometheus"}';
|
||||
|
||||
const Entry: React.SFC<LogRow> = props => {
|
||||
const { entry, searchMatches } = props;
|
||||
if (searchMatches && searchMatches.length > 0) {
|
||||
let lastMatchEnd = 0;
|
||||
const spans = searchMatches.reduce((acc, match, i) => {
|
||||
// Insert non-match
|
||||
if (match.start !== lastMatchEnd) {
|
||||
acc.push(<>{entry.slice(lastMatchEnd, match.start)}</>);
|
||||
}
|
||||
// Match
|
||||
acc.push(
|
||||
<span className="logs-row-match-highlight" title={`Matching expression: ${match.text}`}>
|
||||
{entry.substr(match.start, match.length)}
|
||||
</span>
|
||||
);
|
||||
lastMatchEnd = match.start + match.length;
|
||||
// Non-matching end
|
||||
if (i === searchMatches.length - 1) {
|
||||
acc.push(<>{entry.slice(lastMatchEnd)}</>);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return <>{spans}</>;
|
||||
}
|
||||
return <>{props.entry}</>;
|
||||
};
|
||||
|
||||
export default class Logs extends PureComponent<LogsProps, any> {
|
||||
render() {
|
||||
const { className = '', data } = this.props;
|
||||
const hasData = data && data.rows && data.rows.length > 0;
|
||||
return (
|
||||
<div className={`${className} logs`}>
|
||||
{hasData ? (
|
||||
<div className="logs-entries panel-container">
|
||||
{data.rows.map(row => (
|
||||
<Fragment key={row.key}>
|
||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
|
||||
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
|
||||
<div>
|
||||
<Entry {...row} />
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasData ? (
|
||||
<div className="panel-container">
|
||||
Enter a query like <code>{EXAMPLE_QUERY}</code>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
|
||||
import Typeahead from './Typeahead';
|
||||
|
||||
const EMPTY_METRIC = '';
|
||||
const METRIC_MARK = 'metric';
|
||||
export const TYPEAHEAD_DEBOUNCE = 300;
|
||||
|
||||
function flattenSuggestions(s) {
|
||||
@@ -135,7 +136,7 @@ class QueryField extends React.Component<any, any> {
|
||||
if (!this.state.metrics) {
|
||||
return;
|
||||
}
|
||||
setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
|
||||
setPrismTokens(this.props.prismLanguage, METRIC_MARK, this.state.metrics);
|
||||
|
||||
// Trigger re-render
|
||||
window.requestAnimationFrame(() => {
|
||||
@@ -184,7 +185,7 @@ class QueryField extends React.Component<any, any> {
|
||||
let typeaheadContext = null;
|
||||
|
||||
// Take first metric as lucky guess
|
||||
const metricNode = editorNode.querySelector('.metric');
|
||||
const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
|
||||
|
||||
if (wrapperClasses.contains('context-range')) {
|
||||
// Rate ranges
|
||||
@@ -416,6 +417,7 @@ class QueryField extends React.Component<any, any> {
|
||||
const url = `/api/v1/label/${key}/values`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
console.log(res);
|
||||
const body = await (res.data || res.json());
|
||||
const pairs = this.state.labelValues[EMPTY_METRIC];
|
||||
const values = {
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
export function buildQueryOptions({ format, interval, instant, range, queries }) {
|
||||
return {
|
||||
interval,
|
||||
range,
|
||||
targets: queries.map(expr => ({
|
||||
expr,
|
||||
format,
|
||||
instant,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function generateQueryKey(index = 0) {
|
||||
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,13 @@ export function pageScrollbar() {
|
||||
scope.$on('$routeChangeSuccess', () => {
|
||||
lastPos = 0;
|
||||
elem[0].scrollTop = 0;
|
||||
elem[0].focus();
|
||||
// Focus page to enable scrolling by keyboard
|
||||
elem[0].focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
elem[0].tabIndex = -1;
|
||||
elem[0].focus();
|
||||
// Focus page to enable scrolling by keyboard
|
||||
elem[0].focus({ preventScroll: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export enum LogLevel {
|
||||
crit = 'crit',
|
||||
warn = 'warn',
|
||||
err = 'error',
|
||||
error = 'error',
|
||||
info = 'info',
|
||||
debug = 'debug',
|
||||
trace = 'trace',
|
||||
}
|
||||
|
||||
export interface LogSearchMatch {
|
||||
start: number;
|
||||
length: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface LogRow {
|
||||
key: string;
|
||||
entry: string;
|
||||
logLevel: LogLevel;
|
||||
timestamp: string;
|
||||
timeFromNow: string;
|
||||
timeLocal: string;
|
||||
searchMatches?: LogSearchMatch[];
|
||||
}
|
||||
|
||||
export interface LogsModel {
|
||||
rows: LogRow[];
|
||||
}
|
||||
@@ -191,7 +191,7 @@ export class KeybindingSrv {
|
||||
range,
|
||||
};
|
||||
const exploreState = encodePathComponent(JSON.stringify(state));
|
||||
this.$location.url(`/explore/${exploreState}`);
|
||||
this.$location.url(`/explore?state=${exploreState}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -449,6 +449,7 @@ kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
|
||||
kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
|
||||
kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
|
||||
|
||||
// Data (Binary)
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
@@ -880,6 +881,7 @@ kbn.getUnitFormats = function() {
|
||||
{ text: 'Swedish Krona (kr)', value: 'currencySEK' },
|
||||
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
|
||||
{ text: 'Swiss franc (CHF)', value: 'currencyCHF' },
|
||||
{ text: 'Polish Złoty (PLN)', value: 'currencyPLN' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -222,7 +222,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
// and add built in variables interval and interval_ms
|
||||
var scopedVars = Object.assign({}, this.panel.scopedVars, {
|
||||
__interval: { text: this.interval, value: this.interval },
|
||||
__interval_ms: { text: String(this.intervalMs), value: String(this.intervalMs) },
|
||||
__interval_ms: { text: this.intervalMs, value: this.intervalMs },
|
||||
});
|
||||
|
||||
var metricsQuery = {
|
||||
@@ -332,7 +332,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
range,
|
||||
};
|
||||
const exploreState = encodePathComponent(JSON.stringify(state));
|
||||
this.$location.url(`/explore/${exploreState}`);
|
||||
this.$location.url(`/explore?state=${exploreState}`);
|
||||
}
|
||||
|
||||
addQuery(target) {
|
||||
|
||||
@@ -4,11 +4,13 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
|
||||
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
|
||||
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
|
||||
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
|
||||
import * as loggingPlugin from 'app/plugins/datasource/logging/module';
|
||||
import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
|
||||
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
|
||||
import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
|
||||
import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
|
||||
import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
|
||||
import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
|
||||
|
||||
import * as textPanel from 'app/plugins/panel/text/module';
|
||||
import * as graphPanel from 'app/plugins/panel/graph/module';
|
||||
@@ -20,9 +22,6 @@ import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
|
||||
import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
|
||||
import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
|
||||
|
||||
const builtInPlugins = {
|
||||
'app/plugins/datasource/graphite/module': graphitePlugin,
|
||||
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
|
||||
@@ -30,13 +29,13 @@ const builtInPlugins = {
|
||||
'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
|
||||
'app/plugins/datasource/grafana/module': grafanaPlugin,
|
||||
'app/plugins/datasource/influxdb/module': influxdbPlugin,
|
||||
'app/plugins/datasource/logging/module': loggingPlugin,
|
||||
'app/plugins/datasource/mixed/module': mixedPlugin,
|
||||
'app/plugins/datasource/mysql/module': mysqlPlugin,
|
||||
'app/plugins/datasource/postgres/module': postgresPlugin,
|
||||
'app/plugins/datasource/mssql/module': mssqlPlugin,
|
||||
'app/plugins/datasource/prometheus/module': prometheusPlugin,
|
||||
'app/plugins/app/testdata/module': testDataAppPlugin,
|
||||
'app/plugins/app/testdata/datasource/module': testDataDSPlugin,
|
||||
'app/plugins/datasource/testdata/module': testDataDSPlugin,
|
||||
|
||||
'app/plugins/panel/text/module': textPanel,
|
||||
'app/plugins/panel/graph/module': graphPanel,
|
||||
|
||||
@@ -34,13 +34,13 @@ export class DatasourceSrv {
|
||||
}
|
||||
|
||||
loadDatasource(name) {
|
||||
var dsConfig = config.datasources[name];
|
||||
const dsConfig = config.datasources[name];
|
||||
if (!dsConfig) {
|
||||
return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
|
||||
}
|
||||
|
||||
var deferred = this.$q.defer();
|
||||
var pluginDef = dsConfig.meta;
|
||||
const deferred = this.$q.defer();
|
||||
const pluginDef = dsConfig.meta;
|
||||
|
||||
importPluginModule(pluginDef.module)
|
||||
.then(plugin => {
|
||||
@@ -55,7 +55,7 @@ export class DatasourceSrv {
|
||||
throw new Error('Plugin module is missing Datasource constructor');
|
||||
}
|
||||
|
||||
var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
|
||||
const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
|
||||
instance.meta = pluginDef;
|
||||
instance.name = name;
|
||||
this.datasources[name] = instance;
|
||||
@@ -73,7 +73,7 @@ export class DatasourceSrv {
|
||||
}
|
||||
|
||||
getAnnotationSources() {
|
||||
var sources = [];
|
||||
const sources = [];
|
||||
|
||||
this.addDataSourceVariables(sources);
|
||||
|
||||
@@ -86,6 +86,14 @@ export class DatasourceSrv {
|
||||
return sources;
|
||||
}
|
||||
|
||||
getExploreSources() {
|
||||
const { datasources } = config;
|
||||
const es = Object.keys(datasources)
|
||||
.map(name => datasources[name])
|
||||
.filter(ds => ds.meta && ds.meta.explore);
|
||||
return _.sortBy(es, ['name']);
|
||||
}
|
||||
|
||||
getMetricSources(options) {
|
||||
var metricSources = [];
|
||||
|
||||
@@ -155,3 +163,4 @@ export class DatasourceSrv {
|
||||
}
|
||||
|
||||
coreModule.service('datasourceSrv', DatasourceSrv);
|
||||
export default DatasourceSrv;
|
||||
|
||||
@@ -19,6 +19,7 @@ export class DataSourcesCtrl {
|
||||
onQueryUpdated() {
|
||||
let regex = new RegExp(this.searchQuery, 'ig');
|
||||
this.datasources = _.filter(this.unfiltered, item => {
|
||||
regex.lastIndex = 0;
|
||||
return regex.test(item.name) || regex.test(item.type);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ System.config({
|
||||
css: 'vendor/plugin-css/css.js',
|
||||
},
|
||||
meta: {
|
||||
'*': {
|
||||
'/*': {
|
||||
esModule: true,
|
||||
authorization: true,
|
||||
loader: 'plugin-loader',
|
||||
@@ -126,6 +126,7 @@ import 'vendor/flot/jquery.flot.stackpercent';
|
||||
import 'vendor/flot/jquery.flot.fillbelow';
|
||||
import 'vendor/flot/jquery.flot.crosshair';
|
||||
import 'vendor/flot/jquery.flot.dashes';
|
||||
import 'vendor/flot/jquery.flot.gauge';
|
||||
|
||||
const flotDeps = [
|
||||
'jquery.flot',
|
||||
@@ -137,6 +138,7 @@ const flotDeps = [
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.stackpercent',
|
||||
'jquery.flot.events',
|
||||
'jquery.flot.gauge',
|
||||
];
|
||||
for (let flotDep of flotDeps) {
|
||||
exposeToPlugin(flotDep, { fakeDep: 1 });
|
||||
|
||||
@@ -17,9 +17,35 @@ const templateSrv = {
|
||||
|
||||
describe('datasource_srv', function() {
|
||||
let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
|
||||
let metricSources;
|
||||
|
||||
describe('when loading explore sources', () => {
|
||||
beforeEach(() => {
|
||||
config.datasources = {
|
||||
explore1: {
|
||||
name: 'explore1',
|
||||
meta: { explore: true, metrics: true },
|
||||
},
|
||||
explore2: {
|
||||
name: 'explore2',
|
||||
meta: { explore: true, metrics: false },
|
||||
},
|
||||
nonExplore: {
|
||||
name: 'nonExplore',
|
||||
meta: { explore: false, metrics: true },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should return list of explore sources', () => {
|
||||
const exploreSources = _datasourceSrv.getExploreSources();
|
||||
expect(exploreSources.length).toBe(2);
|
||||
expect(exploreSources[0].name).toBe('explore1');
|
||||
expect(exploreSources[1].name).toBe('explore2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading metric sources', () => {
|
||||
let metricSources;
|
||||
let unsortedDatasources = {
|
||||
mmm: {
|
||||
type: 'test-db',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
-34
@@ -1,34 +0,0 @@
|
||||
export class ConfigCtrl {
|
||||
static template = '';
|
||||
|
||||
appEditCtrl: any;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private backendSrv) {
|
||||
this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
|
||||
}
|
||||
|
||||
initDatasource() {
|
||||
return this.backendSrv.get('/api/datasources').then(res => {
|
||||
var found = false;
|
||||
for (let ds of res) {
|
||||
if (ds.type === 'grafana-testdata-datasource') {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
var dsInstance = {
|
||||
name: 'Grafana TestData',
|
||||
type: 'grafana-testdata-datasource',
|
||||
access: 'direct',
|
||||
jsonData: {},
|
||||
};
|
||||
|
||||
return this.backendSrv.post('/api/datasources', dsInstance);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"type": "app",
|
||||
"name": "Grafana TestData",
|
||||
"id": "testdata",
|
||||
|
||||
"info": {
|
||||
"description": "Grafana test data app",
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"version": "1.0.17",
|
||||
"updated": "2016-09-26"
|
||||
},
|
||||
|
||||
"includes": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "TestData - Graph Last 1h",
|
||||
"path": "dashboards/graph_last_1h.json"
|
||||
},
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "TestData - Alerts",
|
||||
"path": "dashboards/alerts.json"
|
||||
}
|
||||
],
|
||||
|
||||
"dependencies": {
|
||||
"grafanaVersion": "4.x.x"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,9 @@ export default class CloudWatchDatasource {
|
||||
|
||||
var queries = _.filter(options.targets, item => {
|
||||
return (
|
||||
item.hide !== true && !!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)
|
||||
(item.id !== '' || item.hide !== true) &&
|
||||
((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
|
||||
item.expression.length > 0)
|
||||
);
|
||||
}).map(item => {
|
||||
item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
|
||||
@@ -38,6 +40,9 @@ export default class CloudWatchDatasource {
|
||||
item.metricName = this.templateSrv.replace(item.metricName, options.scopedVars);
|
||||
item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
|
||||
item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
|
||||
item.id = this.templateSrv.replace(item.id, options.scopedVars);
|
||||
item.expression = this.templateSrv.replace(item.expression, options.scopedVars);
|
||||
item.returnData = typeof item.hide === 'undefined' ? true : !item.hide;
|
||||
|
||||
// valid ExtendedStatistics is like p90.00, check the pattern
|
||||
let hasInvalidStatistics = item.statistics.some(s => {
|
||||
@@ -407,6 +412,11 @@ export default class CloudWatchDatasource {
|
||||
scopedVar[variable.name] = v;
|
||||
t.refId = target.refId + '_' + v.value;
|
||||
t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
|
||||
if (variable.multi && target.id) {
|
||||
t.id = target.id + window.btoa(v.value).replace(/=/g, '0'); // generate unique id
|
||||
} else {
|
||||
t.id = target.id;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form-inline" ng-if="target.expression.length === 0">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-8">Metric</label>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form-inline" ng-if="target.expression.length === 0">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-8">Dimensions</label>
|
||||
<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
|
||||
@@ -31,18 +31,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form-inline" ng-if="target.statistics.length === 1">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-8">
|
||||
Min period
|
||||
<info-popover mode="right-normal">Minimum interval between points in seconds</info-popover>
|
||||
<label class=" gf-form-label query-keyword width-8 ">
|
||||
Id
|
||||
<info-popover mode="right-normal ">Id can include numbers, letters, and underscore, and must start with a lowercase letter.</info-popover>
|
||||
</label>
|
||||
<input type="text" class="gf-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
|
||||
<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-pattern='/^[a-z][A-Z0-9_]*/' ng-model-onblur
|
||||
ng-change="onChange() ">
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<label class="gf-form-label query-keyword width-7">Alias</label>
|
||||
<input type="text" class="gf-form-input" ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="onChange()">
|
||||
<info-popover mode="right-absolute">
|
||||
<div class="gf-form max-width-30 ">
|
||||
<label class="gf-form-label query-keyword width-7 ">Expression</label>
|
||||
<input type="text " class="gf-form-input " ng-model="target.expression
|
||||
" spellcheck='false' ng-model-onblur ng-change="onChange() ">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline ">
|
||||
<div class="gf-form ">
|
||||
<label class="gf-form-label query-keyword width-8 ">
|
||||
Min period
|
||||
<info-popover mode="right-normal ">Minimum interval between points in seconds</info-popover>
|
||||
</label>
|
||||
<input type="text " class="gf-form-input " ng-model="target.period " spellcheck='false' placeholder="auto
|
||||
" ng-model-onblur ng-change="onChange() " />
|
||||
</div>
|
||||
<div class="gf-form max-width-30 ">
|
||||
<label class="gf-form-label query-keyword width-7 ">Alias</label>
|
||||
<input type="text " class="gf-form-input " ng-model="target.alias " spellcheck='false' ng-model-onblur ng-change="onChange() ">
|
||||
<info-popover mode="right-absolute ">
|
||||
Alias replacement variables:
|
||||
<ul ng-non-bindable>
|
||||
<li>{{metric}}</li>
|
||||
@@ -54,12 +71,12 @@
|
||||
</ul>
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch class="gf-form" label="HighRes" label-class="width-5" checked="target.highResolution" on-change="onChange()">
|
||||
<div class="gf-form ">
|
||||
<gf-form-switch class="gf-form " label="HighRes " label-class="width-5 " checked="target.highResolution " on-change="onChange() ">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
<div class="gf-form gf-form--grow ">
|
||||
<div class="gf-form-label gf-form-label--grow "></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,9 @@ export class CloudWatchQueryParameterCtrl {
|
||||
target.dimensions = target.dimensions || {};
|
||||
target.period = target.period || '';
|
||||
target.region = target.region || 'default';
|
||||
target.id = target.id || '';
|
||||
target.expression = target.expression || '';
|
||||
target.returnData = target.returnData || false;
|
||||
target.highResolution = target.highResolution || false;
|
||||
|
||||
$scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Grafana Logging Datasource - Native Plugin
|
||||
|
||||
This is a **built in** datasource that allows you to connect to Grafana's logging service.
|
||||
@@ -0,0 +1,38 @@
|
||||
import { parseQuery } from './datasource';
|
||||
|
||||
describe('parseQuery', () => {
|
||||
it('returns empty for empty string', () => {
|
||||
expect(parseQuery('')).toEqual({
|
||||
query: '',
|
||||
regexp: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns regexp for strings without query', () => {
|
||||
expect(parseQuery('test')).toEqual({
|
||||
query: '',
|
||||
regexp: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns query for strings without regexp', () => {
|
||||
expect(parseQuery('{foo="bar"}')).toEqual({
|
||||
query: '{foo="bar"}',
|
||||
regexp: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns query for strings with query and search string', () => {
|
||||
expect(parseQuery('x {foo="bar"}')).toEqual({
|
||||
query: '{foo="bar"}',
|
||||
regexp: 'x',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns query for strings with query and regexp', () => {
|
||||
expect(parseQuery('{foo="bar"} x|y')).toEqual({
|
||||
query: '{foo="bar"}',
|
||||
regexp: 'x|y',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
|
||||
import { processStreams } from './result_transformer';
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
const DEFAULT_QUERY_PARAMS = {
|
||||
direction: 'BACKWARD',
|
||||
limit: DEFAULT_LIMIT,
|
||||
regexp: '',
|
||||
query: '',
|
||||
};
|
||||
|
||||
const QUERY_REGEXP = /({\w+="[^"]+"})?\s*(\w[^{]+)?\s*({\w+="[^"]+"})?/;
|
||||
export function parseQuery(input: string) {
|
||||
const match = input.match(QUERY_REGEXP);
|
||||
let query = '';
|
||||
let regexp = '';
|
||||
|
||||
if (match) {
|
||||
if (match[1]) {
|
||||
query = match[1];
|
||||
}
|
||||
if (match[2]) {
|
||||
regexp = match[2].trim();
|
||||
}
|
||||
if (match[3]) {
|
||||
if (match[1]) {
|
||||
query = `${match[1].slice(0, -1)},${match[3].slice(1)}`;
|
||||
} else {
|
||||
query = match[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { query, regexp };
|
||||
}
|
||||
|
||||
function serializeParams(data: any) {
|
||||
return Object.keys(data)
|
||||
.map(k => {
|
||||
const v = data[k];
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(v);
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export default class LoggingDatasource {
|
||||
/** @ngInject */
|
||||
constructor(private instanceSettings, private backendSrv, private templateSrv) {}
|
||||
|
||||
_request(apiUrl: string, data?, options?: any) {
|
||||
const baseUrl = this.instanceSettings.url;
|
||||
const params = data ? serializeParams(data) : '';
|
||||
const url = `${baseUrl}${apiUrl}?${params}`;
|
||||
const req = {
|
||||
...options,
|
||||
url,
|
||||
};
|
||||
return this.backendSrv.datasourceRequest(req);
|
||||
}
|
||||
|
||||
prepareQueryTarget(target, options) {
|
||||
const interpolated = this.templateSrv.replace(target.expr);
|
||||
const start = this.getTime(options.range.from, false);
|
||||
const end = this.getTime(options.range.to, true);
|
||||
return {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...parseQuery(interpolated),
|
||||
start,
|
||||
end,
|
||||
};
|
||||
}
|
||||
|
||||
query(options) {
|
||||
const queryTargets = options.targets
|
||||
.filter(target => target.expr)
|
||||
.map(target => this.prepareQueryTarget(target, options));
|
||||
if (queryTargets.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
|
||||
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
|
||||
|
||||
return Promise.all(queries).then((results: any[]) => {
|
||||
// Flatten streams from multiple queries
|
||||
const allStreams = results.reduce((acc, response, i) => {
|
||||
const streams = response.data.streams || [];
|
||||
// Inject search for match highlighting
|
||||
const search = queryTargets[i].regexp;
|
||||
streams.forEach(s => {
|
||||
s.search = search;
|
||||
});
|
||||
return [...acc, ...streams];
|
||||
}, []);
|
||||
const model = processStreams(allStreams, DEFAULT_LIMIT);
|
||||
return { data: model };
|
||||
});
|
||||
}
|
||||
|
||||
metadataRequest(url) {
|
||||
// HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
|
||||
const apiUrl = url.replace('v1', 'prom');
|
||||
return this._request(apiUrl, { silent: true }).then(res => {
|
||||
const data = { data: { data: res.data.values || [] } };
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
getTime(date, roundUp) {
|
||||
if (_.isString(date)) {
|
||||
date = dateMath.parse(date, roundUp);
|
||||
}
|
||||
return Math.ceil(date.valueOf() * 1e6);
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
return this._request('/api/prom/label')
|
||||
.then(res => {
|
||||
if (res && res.data && res.data.values && res.data.values.length > 0) {
|
||||
return { status: 'success', message: 'Data source connected and labels found.' };
|
||||
}
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Data source connected, but no labels received. Verify that logging is configured properly.',
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
return { status: 'error', message: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<g id="Layer_1_1_">
|
||||
</g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
|
||||
<stop offset="0" style="stop-color:#FFF100"/>
|
||||
<stop offset="1" style="stop-color:#F05A28"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
|
||||
c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
|
||||
c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
|
||||
c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
|
||||
c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
|
||||
c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
|
||||
C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
|
||||
c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
|
||||
c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
|
||||
c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
|
||||
c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
|
||||
c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
|
||||
l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
|
||||
c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
|
||||
c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
|
||||
c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
|
||||
c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
|
||||
c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
|
||||
c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
|
||||
c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
|
||||
c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
|
||||
c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
|
||||
c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
|
||||
c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
|
||||
c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
|
||||
c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
|
||||
c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
|
||||
l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
|
||||
c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
|
||||
c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
|
||||
c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
|
||||
c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
|
||||
l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
|
||||
c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
|
||||
c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
|
||||
c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
|
||||
c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
|
||||
c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
|
||||
l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
|
||||
c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
|
||||
c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
|
||||
c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
|
||||
c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,7 @@
|
||||
import Datasource from './datasource';
|
||||
|
||||
export class LoggingConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
}
|
||||
|
||||
export { Datasource, LoggingConfigCtrl as ConfigCtrl };
|
||||
@@ -0,0 +1,2 @@
|
||||
<datasource-http-settings current="ctrl.current" no-direct-access="true">
|
||||
</datasource-http-settings>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Grafana Logging",
|
||||
"id": "logging",
|
||||
"metrics": false,
|
||||
"alerting": false,
|
||||
"annotations": false,
|
||||
"logs": true,
|
||||
"explore": true,
|
||||
"info": {
|
||||
"description": "Grafana Logging Data Source for Grafana",
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/grafana_icon.svg",
|
||||
"large": "img/grafana_icon.svg"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"name": "Grafana Logging",
|
||||
"url": "https://grafana.com/"
|
||||
}
|
||||
],
|
||||
"version": "5.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { LogLevel } from 'app/core/logs_model';
|
||||
|
||||
import { getLogLevel, getSearchMatches } from './result_transformer';
|
||||
|
||||
describe('getSearchMatches()', () => {
|
||||
it('gets no matches for when search and or line are empty', () => {
|
||||
expect(getSearchMatches('', '')).toEqual([]);
|
||||
expect(getSearchMatches('foo', '')).toEqual([]);
|
||||
expect(getSearchMatches('', 'foo')).toEqual([]);
|
||||
});
|
||||
|
||||
it('gets no matches for unmatched search string', () => {
|
||||
expect(getSearchMatches('foo', 'bar')).toEqual([]);
|
||||
});
|
||||
|
||||
it('gets matches for matched search string', () => {
|
||||
expect(getSearchMatches('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo' }]);
|
||||
expect(getSearchMatches(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo' }]);
|
||||
});
|
||||
|
||||
expect(getSearchMatches(' foo foo bar ', 'foo|bar')).toEqual([
|
||||
{ length: 3, start: 1, text: 'foo' },
|
||||
{ length: 3, start: 5, text: 'foo' },
|
||||
{ length: 3, start: 9, text: 'bar' },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('getLoglevel()', () => {
|
||||
it('returns no log level on empty line', () => {
|
||||
expect(getLogLevel('')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns no log level on when level is part of a word', () => {
|
||||
expect(getLogLevel('this is a warning')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns log level on line contains a log level', () => {
|
||||
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
|
||||
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
|
||||
});
|
||||
|
||||
it('returns first log level found', () => {
|
||||
expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { LogLevel, LogsModel, LogRow } from 'app/core/logs_model';
|
||||
|
||||
export function getLogLevel(line: string): LogLevel {
|
||||
if (!line) {
|
||||
return undefined;
|
||||
}
|
||||
let level: LogLevel;
|
||||
Object.keys(LogLevel).forEach(key => {
|
||||
if (!level) {
|
||||
const regexp = new RegExp(`\\b${key}\\b`, 'i');
|
||||
if (regexp.test(line)) {
|
||||
level = LogLevel[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return level;
|
||||
}
|
||||
|
||||
export function getSearchMatches(line: string, search: string) {
|
||||
// Empty search can send re.exec() into infinite loop, exit early
|
||||
if (!line || !search) {
|
||||
return [];
|
||||
}
|
||||
const regexp = new RegExp(`(?:${search})`, 'g');
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = regexp.exec(line))) {
|
||||
matches.push({
|
||||
text: match[0],
|
||||
start: match.index,
|
||||
length: match[0].length,
|
||||
});
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow {
|
||||
const { line, timestamp } = entry;
|
||||
const { labels } = stream;
|
||||
const key = `EK${timestamp}${labels}`;
|
||||
const time = moment(timestamp);
|
||||
const timeFromNow = time.fromNow();
|
||||
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
||||
const searchMatches = getSearchMatches(line, stream.search);
|
||||
const logLevel = getLogLevel(line);
|
||||
|
||||
return {
|
||||
key,
|
||||
logLevel,
|
||||
searchMatches,
|
||||
timeFromNow,
|
||||
timeLocal,
|
||||
entry: line,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export function processStreams(streams, limit?: number): LogsModel {
|
||||
const combinedEntries = streams.reduce((acc, stream) => {
|
||||
return [...acc, ...stream.entries.map(entry => processEntry(entry, stream))];
|
||||
}, []);
|
||||
const sortedEntries = _.chain(combinedEntries)
|
||||
.sortBy('timestamp')
|
||||
.reverse()
|
||||
.slice(0, limit || combinedEntries.length)
|
||||
.value();
|
||||
return { rows: sortedEntries };
|
||||
}
|
||||
@@ -480,17 +480,17 @@ export default class OpenTsDatasource {
|
||||
|
||||
mapMetricsToTargets(metrics, options, tsdbVersion) {
|
||||
var interpolatedTagValue, arrTagV;
|
||||
return _.map(metrics, function(metricData) {
|
||||
return _.map(metrics, metricData => {
|
||||
if (tsdbVersion === 3) {
|
||||
return metricData.query.index;
|
||||
} else {
|
||||
return _.findIndex(options.targets, function(target) {
|
||||
return _.findIndex(options.targets, target => {
|
||||
if (target.filters && target.filters.length > 0) {
|
||||
return target.metric === metricData.metric;
|
||||
} else {
|
||||
return (
|
||||
target.metric === metricData.metric &&
|
||||
_.every(target.tags, function(tagV, tagK) {
|
||||
_.every(target.tags, (tagV, tagK) => {
|
||||
interpolatedTagValue = this.templateSrv.replace(tagV, options.scopedVars, 'pipe');
|
||||
arrTagV = interpolatedTagValue.split('|');
|
||||
return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === '*';
|
||||
|
||||
@@ -17,11 +17,17 @@ export function alignRange(start, end, step) {
|
||||
}
|
||||
|
||||
export function prometheusRegularEscape(value) {
|
||||
return value.replace(/'/g, "\\\\'");
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/'/g, "\\\\'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function prometheusSpecialRegexEscape(value) {
|
||||
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
|
||||
if (typeof value === 'string') {
|
||||
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export class PrometheusDatasource {
|
||||
@@ -190,13 +196,14 @@ export class PrometheusDatasource {
|
||||
var intervalFactor = target.intervalFactor || 1;
|
||||
// Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
|
||||
var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
|
||||
var scopedVars = options.scopedVars;
|
||||
var scopedVars = { ...options.scopedVars, ...this.getRangeScopedVars() };
|
||||
// If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
|
||||
if (interval !== adjustedInterval) {
|
||||
interval = adjustedInterval;
|
||||
scopedVars = Object.assign({}, options.scopedVars, {
|
||||
__interval: { text: interval + 's', value: interval + 's' },
|
||||
__interval_ms: { text: String(interval * 1000), value: String(interval * 1000) },
|
||||
__interval_ms: { text: interval * 1000, value: interval * 1000 },
|
||||
...this.getRangeScopedVars(),
|
||||
});
|
||||
}
|
||||
query.step = interval;
|
||||
@@ -279,11 +286,26 @@ export class PrometheusDatasource {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
let interpolated = this.templateSrv.replace(query, {}, this.interpolateQueryExpr);
|
||||
let scopedVars = {
|
||||
__interval: { text: this.interval, value: this.interval },
|
||||
__interval_ms: { text: kbn.interval_to_ms(this.interval), value: kbn.interval_to_ms(this.interval) },
|
||||
...this.getRangeScopedVars(),
|
||||
};
|
||||
let interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
|
||||
var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv);
|
||||
return metricFindQuery.process();
|
||||
}
|
||||
|
||||
getRangeScopedVars() {
|
||||
let range = this.timeSrv.timeRange();
|
||||
let msRange = range.to.diff(range.from);
|
||||
let regularRange = kbn.secondsToHms(msRange / 1000);
|
||||
return {
|
||||
__range_ms: { text: msRange, value: msRange },
|
||||
__range: { text: regularRange, value: regularRange },
|
||||
};
|
||||
}
|
||||
|
||||
annotationQuery(options) {
|
||||
var annotation = options.annotation;
|
||||
var expr = annotation.expr || '';
|
||||
@@ -357,6 +379,7 @@ export class PrometheusDatasource {
|
||||
state = {
|
||||
...state,
|
||||
queries,
|
||||
datasource: this.name,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -2,21 +2,30 @@
|
||||
"type": "datasource",
|
||||
"name": "Prometheus",
|
||||
"id": "prometheus",
|
||||
|
||||
"includes": [
|
||||
{"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"},
|
||||
{"type": "dashboard", "name": "Prometheus 2.0 Stats", "path": "dashboards/prometheus_2_stats.json"},
|
||||
{"type": "dashboard", "name": "Grafana Stats", "path": "dashboards/grafana_stats.json"}
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "Prometheus Stats",
|
||||
"path": "dashboards/prometheus_stats.json"
|
||||
},
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "Prometheus 2.0 Stats",
|
||||
"path": "dashboards/prometheus_2_stats.json"
|
||||
},
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "Grafana Stats",
|
||||
"path": "dashboards/grafana_stats.json"
|
||||
}
|
||||
],
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"explore": true,
|
||||
"queryOptions": {
|
||||
"minInterval": true
|
||||
},
|
||||
|
||||
"info": {
|
||||
"description": "Prometheus Data Source for Grafana",
|
||||
"author": {
|
||||
@@ -28,8 +37,11 @@
|
||||
"large": "img/prometheus_logo.svg"
|
||||
},
|
||||
"links": [
|
||||
{"name": "Prometheus", "url": "https://prometheus.io/"}
|
||||
{
|
||||
"name": "Prometheus",
|
||||
"url": "https://prometheus.io/"
|
||||
}
|
||||
],
|
||||
"version": "5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import q from 'q';
|
||||
import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
|
||||
jest.mock('../metric_find_query');
|
||||
|
||||
describe('PrometheusDatasource', () => {
|
||||
let ctx: any = {};
|
||||
@@ -18,7 +19,14 @@ describe('PrometheusDatasource', () => {
|
||||
ctx.templateSrvMock = {
|
||||
replace: a => a,
|
||||
};
|
||||
ctx.timeSrvMock = {};
|
||||
ctx.timeSrvMock = {
|
||||
timeRange: () => {
|
||||
return {
|
||||
from: moment(1531468681),
|
||||
to: moment(1531489712),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
|
||||
@@ -166,6 +174,9 @@ describe('PrometheusDatasource', () => {
|
||||
});
|
||||
|
||||
describe('Prometheus regular escaping', function() {
|
||||
it('should not escape non-string', function() {
|
||||
expect(prometheusRegularEscape(12)).toEqual(12);
|
||||
});
|
||||
it('should not escape simple string', function() {
|
||||
expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
|
||||
});
|
||||
@@ -201,4 +212,37 @@ describe('PrometheusDatasource', () => {
|
||||
expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
|
||||
});
|
||||
});
|
||||
|
||||
describe('metricFindQuery', () => {
|
||||
beforeEach(() => {
|
||||
let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
|
||||
ctx.templateSrvMock.replace = jest.fn();
|
||||
ctx.timeSrvMock.timeRange = () => {
|
||||
return {
|
||||
from: moment(1531468681),
|
||||
to: moment(1531489712),
|
||||
};
|
||||
};
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
|
||||
ctx.ds.metricFindQuery(query);
|
||||
});
|
||||
|
||||
it('should call templateSrv.replace with scopedVars', () => {
|
||||
expect(ctx.templateSrvMock.replace.mock.calls[0][1]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have the correct range and range_ms', () => {
|
||||
let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
|
||||
let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
|
||||
expect(range).toEqual({ text: '21s', value: '21s' });
|
||||
expect(rangeMs).toEqual({ text: 21031, value: 21031 });
|
||||
});
|
||||
|
||||
it('should pass the default interval value', () => {
|
||||
let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval;
|
||||
let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms;
|
||||
expect(interval).toEqual({ text: '15s', value: '15s' });
|
||||
expect(intervalMs).toEqual({ text: 15000, value: 15000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -452,7 +452,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '10s',
|
||||
scopedVars: {
|
||||
__interval: { text: '10s', value: '10s' },
|
||||
__interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
|
||||
__interval_ms: { text: 10 * 1000, value: 10 * 1000 },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@@ -463,8 +463,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('10s');
|
||||
expect(query.scopedVars.__interval.value).to.be('10s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
|
||||
});
|
||||
it('should be min interval when it is greater than auto interval', function() {
|
||||
var query = {
|
||||
@@ -479,7 +479,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@@ -490,8 +490,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
it('should account for intervalFactor', function() {
|
||||
var query = {
|
||||
@@ -507,7 +507,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '10s',
|
||||
scopedVars: {
|
||||
__interval: { text: '10s', value: '10s' },
|
||||
__interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
|
||||
__interval_ms: { text: 10 * 1000, value: 10 * 1000 },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@@ -518,8 +518,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('10s');
|
||||
expect(query.scopedVars.__interval.value).to.be('10s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
|
||||
});
|
||||
it('should be interval * intervalFactor when greater than min interval', function() {
|
||||
var query = {
|
||||
@@ -535,7 +535,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@@ -546,8 +546,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
it('should be min interval when greater than interval * intervalFactor', function() {
|
||||
var query = {
|
||||
@@ -563,7 +563,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
},
|
||||
};
|
||||
var urlExpected =
|
||||
@@ -574,8 +574,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
|
||||
var query = {
|
||||
@@ -590,7 +590,7 @@ describe('PrometheusDatasource', function() {
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
__interval: { text: '5s', value: '5s' },
|
||||
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
|
||||
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
|
||||
},
|
||||
};
|
||||
var end = 7 * 24 * 60 * 60;
|
||||
@@ -609,8 +609,8 @@ describe('PrometheusDatasource', function() {
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be('5s');
|
||||
expect(query.scopedVars.__interval.value).to.be('5s');
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-1
@@ -37,4 +37,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</query-editor-row>
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Grafana TestDataDB",
|
||||
"id": "grafana-testdata-datasource",
|
||||
"name": "TestData DB",
|
||||
"id": "testdata",
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "",
|
||||
"large": ""
|
||||
"small": "../../../../img/grafana_icon.svg",
|
||||
"large": "../../../../img/grafana_icon.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,604 +0,0 @@
|
||||
define([
|
||||
'jquery',
|
||||
'lodash',
|
||||
'angular',
|
||||
'tether-drop',
|
||||
],
|
||||
function ($, _, angular, Drop) {
|
||||
'use strict';
|
||||
|
||||
function createAnnotationToolip(element, event, plot) {
|
||||
var injector = angular.element(document).injector();
|
||||
var content = document.createElement('div');
|
||||
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
|
||||
|
||||
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
||||
var eventManager = plot.getOptions().events.manager;
|
||||
var tmpScope = $rootScope.$new(true);
|
||||
tmpScope.event = event;
|
||||
tmpScope.onEdit = function() {
|
||||
eventManager.editEvent(event);
|
||||
};
|
||||
|
||||
$compile(content)(tmpScope);
|
||||
tmpScope.$digest();
|
||||
tmpScope.$destroy();
|
||||
|
||||
var drop = new Drop({
|
||||
target: element[0],
|
||||
content: content,
|
||||
position: "bottom center",
|
||||
classes: 'drop-popover drop-popover--annotation',
|
||||
openOn: 'hover',
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{to: 'window', pin: true, attachment: "both"}]
|
||||
}
|
||||
});
|
||||
|
||||
drop.open();
|
||||
|
||||
drop.on('close', function() {
|
||||
setTimeout(function() {
|
||||
drop.destroy();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
}
|
||||
|
||||
var markerElementToAttachTo = null;
|
||||
|
||||
function createEditPopover(element, event, plot) {
|
||||
var eventManager = plot.getOptions().events.manager;
|
||||
if (eventManager.editorOpen) {
|
||||
// update marker element to attach to (needed in case of legend on the right
|
||||
// when there is a double render pass and the initial marker element is removed)
|
||||
markerElementToAttachTo = element;
|
||||
return;
|
||||
}
|
||||
|
||||
// mark as openend
|
||||
eventManager.editorOpened();
|
||||
// set marker element to attache to
|
||||
markerElementToAttachTo = element;
|
||||
|
||||
// wait for element to be attached and positioned
|
||||
setTimeout(function() {
|
||||
|
||||
var injector = angular.element(document).injector();
|
||||
var content = document.createElement('div');
|
||||
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
|
||||
|
||||
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
||||
var scope = $rootScope.$new(true);
|
||||
var drop;
|
||||
|
||||
scope.event = event;
|
||||
scope.panelCtrl = eventManager.panelCtrl;
|
||||
scope.close = function() {
|
||||
drop.close();
|
||||
};
|
||||
|
||||
$compile(content)(scope);
|
||||
scope.$digest();
|
||||
|
||||
drop = new Drop({
|
||||
target: markerElementToAttachTo[0],
|
||||
content: content,
|
||||
position: "bottom center",
|
||||
classes: 'drop-popover drop-popover--form',
|
||||
openOn: 'click',
|
||||
tetherOptions: {
|
||||
constraints: [{to: 'window', pin: true, attachment: "both"}]
|
||||
}
|
||||
});
|
||||
|
||||
drop.open();
|
||||
eventManager.editorOpened();
|
||||
|
||||
drop.on('close', function() {
|
||||
// need timeout here in order call drop.destroy
|
||||
setTimeout(function() {
|
||||
eventManager.editorClosed();
|
||||
scope.$destroy();
|
||||
drop.destroy();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/*
|
||||
* jquery.flot.events
|
||||
*
|
||||
* description: Flot plugin for adding events/markers to the plot
|
||||
* version: 0.2.5
|
||||
* authors:
|
||||
* Alexander Wunschik <alex@wunschik.net>
|
||||
* Joel Oughton <joeloughton@gmail.com>
|
||||
* Nicolas Joseph <www.nicolasjoseph.com>
|
||||
*
|
||||
* website: https://github.com/mojoaxel/flot-events
|
||||
*
|
||||
* released under MIT License and GPLv2+
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class that allows for the drawing an remove of some object
|
||||
*/
|
||||
var DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
|
||||
var _object = object;
|
||||
var _drawFunc = drawFunc;
|
||||
var _clearFunc = clearFunc;
|
||||
var _moveFunc = moveFunc;
|
||||
var _position = { left: left, top: top };
|
||||
var _width = width;
|
||||
var _height = height;
|
||||
|
||||
this.width = function() { return _width; };
|
||||
this.height = function() { return _height; };
|
||||
this.position = function() { return _position; };
|
||||
this.draw = function() { _drawFunc(_object); };
|
||||
this.clear = function() { _clearFunc(_object); };
|
||||
this.getObject = function() { return _object; };
|
||||
this.moveTo = function(position) {
|
||||
_position = position;
|
||||
_moveFunc(_object, _position);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Event class that stores options (eventType, min, max, title, description) and the object to draw.
|
||||
*/
|
||||
var VisualEvent = function(options, drawableEvent) {
|
||||
var _parent;
|
||||
var _options = options;
|
||||
var _drawableEvent = drawableEvent;
|
||||
var _hidden = false;
|
||||
|
||||
this.visual = function() { return _drawableEvent; };
|
||||
this.getOptions = function() { return _options; };
|
||||
this.getParent = function() { return _parent; };
|
||||
this.isHidden = function() { return _hidden; };
|
||||
this.hide = function() { _hidden = true; };
|
||||
this.unhide = function() { _hidden = false; };
|
||||
};
|
||||
|
||||
/**
|
||||
* A Class that handles the event-markers inside the given plot
|
||||
*/
|
||||
var EventMarkers = function(plot) {
|
||||
var _events = [];
|
||||
|
||||
this._types = [];
|
||||
this._plot = plot;
|
||||
this.eventsEnabled = false;
|
||||
|
||||
this.getEvents = function() {
|
||||
return _events;
|
||||
};
|
||||
|
||||
this.setTypes = function(types) {
|
||||
return this._types = types;
|
||||
};
|
||||
|
||||
/**
|
||||
* create internal objects for the given events
|
||||
*/
|
||||
this.setupEvents = function(events) {
|
||||
var that = this;
|
||||
var parts = _.partition(events, 'isRegion');
|
||||
var regions = parts[0];
|
||||
events = parts[1];
|
||||
|
||||
$.each(events, function(index, event) {
|
||||
var ve = new VisualEvent(event, that._buildDiv(event));
|
||||
_events.push(ve);
|
||||
});
|
||||
|
||||
$.each(regions, function (index, event) {
|
||||
var vre = new VisualEvent(event, that._buildRegDiv(event));
|
||||
_events.push(vre);
|
||||
});
|
||||
|
||||
_events.sort(function(a, b) {
|
||||
var ao = a.getOptions(), bo = b.getOptions();
|
||||
if (ao.min > bo.min) { return 1; }
|
||||
if (ao.min < bo.min) { return -1; }
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* draw the events to the plot
|
||||
*/
|
||||
this.drawEvents = function() {
|
||||
var that = this;
|
||||
// var o = this._plot.getPlotOffset();
|
||||
|
||||
$.each(_events, function(index, event) {
|
||||
// check event is inside the graph range
|
||||
if (that._insidePlot(event.getOptions().min) && !event.isHidden()) {
|
||||
event.visual().draw();
|
||||
} else {
|
||||
event.visual().getObject().hide();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* update the position of the event-markers (e.g. after scrolling or zooming)
|
||||
*/
|
||||
this.updateEvents = function() {
|
||||
var that = this;
|
||||
var o = this._plot.getPlotOffset(), left, top;
|
||||
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
|
||||
$.each(_events, function(index, event) {
|
||||
top = o.top + that._plot.height() - event.visual().height();
|
||||
left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
|
||||
event.visual().moveTo({ top: top, left: left });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* remove all events from the plot
|
||||
*/
|
||||
this._clearEvents = function() {
|
||||
$.each(_events, function(index, val) {
|
||||
val.visual().clear();
|
||||
});
|
||||
_events = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* create a DOM element for the given event
|
||||
*/
|
||||
this._buildDiv = function(event) {
|
||||
var that = this;
|
||||
|
||||
var container = this._plot.getPlaceholder();
|
||||
var o = this._plot.getPlotOffset();
|
||||
var axes = this._plot.getAxes();
|
||||
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
var yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth;
|
||||
var markerTooltip;
|
||||
|
||||
// determine the y axis used
|
||||
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
|
||||
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
|
||||
|
||||
// map the eventType to a types object
|
||||
var eventTypeId = event.eventType;
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
|
||||
color = '#666';
|
||||
} else {
|
||||
color = this._types[eventTypeId].color;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
|
||||
markerSize = 8; //default marker size
|
||||
} else {
|
||||
markerSize = this._types[eventTypeId].markerSize;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
|
||||
markerShow = true;
|
||||
} else {
|
||||
markerShow = this._types[eventTypeId].markerShow;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
|
||||
markerTooltip = true;
|
||||
} else {
|
||||
markerTooltip = this._types[eventTypeId].markerTooltip;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
|
||||
lineStyle = 'dashed'; //default line style
|
||||
} else {
|
||||
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
|
||||
lineWidth = 1; //default line width
|
||||
} else {
|
||||
lineWidth = this._types[eventTypeId].lineWidth;
|
||||
}
|
||||
|
||||
var topOffset = xaxis.options.eventSectionHeight || 0;
|
||||
topOffset = topOffset / 3;
|
||||
|
||||
top = o.top + this._plot.height() + topOffset;
|
||||
left = xaxis.p2c(event.min) + o.left;
|
||||
|
||||
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||
"position": "absolute",
|
||||
"opacity": 0.8,
|
||||
"left": left + 'px',
|
||||
"top": 8,
|
||||
"width": lineWidth + "px",
|
||||
"height": this._plot.height() + topOffset * 0.8,
|
||||
"border-left-width": lineWidth + "px",
|
||||
"border-left-style": lineStyle,
|
||||
"border-left-color": color,
|
||||
"color": color
|
||||
})
|
||||
.appendTo(container);
|
||||
|
||||
if (markerShow) {
|
||||
var marker = $('<div class="events_marker"></div>').css({
|
||||
"position": "absolute",
|
||||
"left": (-markerSize - Math.round(lineWidth / 2)) + "px",
|
||||
"font-size": 0,
|
||||
"line-height": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"border-left": markerSize+"px solid transparent",
|
||||
"border-right": markerSize+"px solid transparent"
|
||||
});
|
||||
|
||||
marker.appendTo(line);
|
||||
|
||||
if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
|
||||
marker.css({
|
||||
"top": top-markerSize-8 +"px",
|
||||
"border-top": "none",
|
||||
"border-bottom": markerSize+"px solid " + color
|
||||
});
|
||||
} else {
|
||||
marker.css({
|
||||
"top": "0px",
|
||||
"border-top": markerSize+"px solid " + color,
|
||||
"border-bottom": "none"
|
||||
});
|
||||
}
|
||||
|
||||
marker.data({
|
||||
"event": event
|
||||
});
|
||||
|
||||
var mouseenter = function() {
|
||||
createAnnotationToolip(marker, $(this).data("event"), that._plot);
|
||||
};
|
||||
|
||||
if (event.editModel) {
|
||||
createEditPopover(marker, event.editModel, that._plot);
|
||||
}
|
||||
|
||||
var mouseleave = function() {
|
||||
that._plot.clearSelection();
|
||||
};
|
||||
|
||||
if (markerTooltip) {
|
||||
marker.css({ "cursor": "help" });
|
||||
marker.hover(mouseenter, mouseleave);
|
||||
}
|
||||
}
|
||||
|
||||
var drawableEvent = new DrawableEvent(
|
||||
line,
|
||||
function drawFunc(obj) { obj.show(); },
|
||||
function(obj) { obj.remove(); },
|
||||
function(obj, position) {
|
||||
obj.css({
|
||||
top: position.top,
|
||||
left: position.left
|
||||
});
|
||||
},
|
||||
left,
|
||||
top,
|
||||
line.width(),
|
||||
line.height()
|
||||
);
|
||||
|
||||
return drawableEvent;
|
||||
};
|
||||
|
||||
/**
|
||||
* create a DOM element for the given region
|
||||
*/
|
||||
this._buildRegDiv = function (event) {
|
||||
var that = this;
|
||||
|
||||
var container = this._plot.getPlaceholder();
|
||||
var o = this._plot.getPlotOffset();
|
||||
var axes = this._plot.getAxes();
|
||||
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
|
||||
|
||||
// determine the y axis used
|
||||
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
|
||||
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
|
||||
|
||||
// map the eventType to a types object
|
||||
var eventTypeId = event.eventType;
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
|
||||
color = '#666';
|
||||
} else {
|
||||
color = this._types[eventTypeId].color;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
|
||||
markerTooltip = true;
|
||||
} else {
|
||||
markerTooltip = this._types[eventTypeId].markerTooltip;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
|
||||
lineWidth = 1; //default line width
|
||||
} else {
|
||||
lineWidth = this._types[eventTypeId].lineWidth;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
|
||||
lineStyle = 'dashed'; //default line style
|
||||
} else {
|
||||
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
|
||||
}
|
||||
|
||||
var topOffset = 2;
|
||||
top = o.top + this._plot.height() + topOffset;
|
||||
|
||||
var timeFrom = Math.min(event.min, event.timeEnd);
|
||||
var timeTo = Math.max(event.min, event.timeEnd);
|
||||
left = xaxis.p2c(timeFrom) + o.left;
|
||||
var right = xaxis.p2c(timeTo) + o.left;
|
||||
regionWidth = right - left;
|
||||
|
||||
_.each([left, right], function(position) {
|
||||
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||
"position": "absolute",
|
||||
"opacity": 0.8,
|
||||
"left": position + 'px',
|
||||
"top": 8,
|
||||
"width": lineWidth + "px",
|
||||
"height": that._plot.height() + topOffset,
|
||||
"border-left-width": lineWidth + "px",
|
||||
"border-left-style": lineStyle,
|
||||
"border-left-color": color,
|
||||
"color": color
|
||||
});
|
||||
line.appendTo(container);
|
||||
});
|
||||
|
||||
var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
|
||||
"position": "absolute",
|
||||
"opacity": 0.5,
|
||||
"left": left + 'px',
|
||||
"top": top,
|
||||
"width": Math.round(regionWidth + lineWidth) + "px",
|
||||
"height": "0.5rem",
|
||||
"border-left-color": color,
|
||||
"color": color,
|
||||
"background-color": color
|
||||
});
|
||||
region.appendTo(container);
|
||||
|
||||
region.data({
|
||||
"event": event
|
||||
});
|
||||
|
||||
var mouseenter = function () {
|
||||
createAnnotationToolip(region, $(this).data("event"), that._plot);
|
||||
};
|
||||
|
||||
if (event.editModel) {
|
||||
createEditPopover(region, event.editModel, that._plot);
|
||||
}
|
||||
|
||||
var mouseleave = function () {
|
||||
that._plot.clearSelection();
|
||||
};
|
||||
|
||||
if (markerTooltip) {
|
||||
region.css({ "cursor": "help" });
|
||||
region.hover(mouseenter, mouseleave);
|
||||
}
|
||||
|
||||
var drawableEvent = new DrawableEvent(
|
||||
region,
|
||||
function drawFunc(obj) { obj.show(); },
|
||||
function (obj) { obj.remove(); },
|
||||
function (obj, position) {
|
||||
obj.css({
|
||||
top: position.top,
|
||||
left: position.left
|
||||
});
|
||||
},
|
||||
left,
|
||||
top,
|
||||
region.width(),
|
||||
region.height()
|
||||
);
|
||||
|
||||
return drawableEvent;
|
||||
};
|
||||
|
||||
/**
|
||||
* check if the event is inside visible range
|
||||
*/
|
||||
this._insidePlot = function(x) {
|
||||
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
var xc = xaxis.p2c(x);
|
||||
return xc > 0 && xc < xaxis.p2c(xaxis.max);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* initialize the plugin for the given plot
|
||||
*/
|
||||
function init(plot) {
|
||||
/*jshint validthis:true */
|
||||
var that = this;
|
||||
var eventMarkers = new EventMarkers(plot);
|
||||
|
||||
plot.getEvents = function() {
|
||||
return eventMarkers._events;
|
||||
};
|
||||
|
||||
plot.hideEvents = function() {
|
||||
$.each(eventMarkers._events, function(index, event) {
|
||||
event.visual().getObject().hide();
|
||||
});
|
||||
};
|
||||
|
||||
plot.showEvents = function() {
|
||||
plot.hideEvents();
|
||||
$.each(eventMarkers._events, function(index, event) {
|
||||
event.hide();
|
||||
});
|
||||
|
||||
that.eventMarkers.drawEvents();
|
||||
};
|
||||
|
||||
// change events on an existing plot
|
||||
plot.setEvents = function(events) {
|
||||
if (eventMarkers.eventsEnabled) {
|
||||
eventMarkers.setupEvents(events);
|
||||
}
|
||||
};
|
||||
|
||||
plot.hooks.processOptions.push(function(plot, options) {
|
||||
// enable the plugin
|
||||
if (options.events.data != null) {
|
||||
eventMarkers.eventsEnabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
plot.hooks.draw.push(function(plot) {
|
||||
var options = plot.getOptions();
|
||||
|
||||
if (eventMarkers.eventsEnabled) {
|
||||
// check for first run
|
||||
if (eventMarkers.getEvents().length < 1) {
|
||||
eventMarkers.setTypes(options.events.types);
|
||||
eventMarkers.setupEvents(options.events.data);
|
||||
} else {
|
||||
eventMarkers.updateEvents();
|
||||
}
|
||||
}
|
||||
|
||||
eventMarkers.drawEvents();
|
||||
});
|
||||
}
|
||||
|
||||
var defaultOptions = {
|
||||
events: {
|
||||
data: null,
|
||||
types: null,
|
||||
xaxis: 1,
|
||||
position: 'BOTTOM'
|
||||
}
|
||||
};
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: defaultOptions,
|
||||
name: "events",
|
||||
version: "0.2.5"
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,671 @@
|
||||
import angular from 'angular';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import Drop from 'tether-drop';
|
||||
|
||||
/** @ngInject */
|
||||
export function createAnnotationToolip(element, event, plot) {
|
||||
let injector = angular.element(document).injector();
|
||||
let content = document.createElement('div');
|
||||
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
|
||||
|
||||
injector.invoke([
|
||||
'$compile',
|
||||
'$rootScope',
|
||||
function($compile, $rootScope) {
|
||||
let eventManager = plot.getOptions().events.manager;
|
||||
let tmpScope = $rootScope.$new(true);
|
||||
tmpScope.event = event;
|
||||
tmpScope.onEdit = function() {
|
||||
eventManager.editEvent(event);
|
||||
};
|
||||
|
||||
$compile(content)(tmpScope);
|
||||
tmpScope.$digest();
|
||||
tmpScope.$destroy();
|
||||
|
||||
let drop = new Drop({
|
||||
target: element[0],
|
||||
content: content,
|
||||
position: 'bottom center',
|
||||
classes: 'drop-popover drop-popover--annotation',
|
||||
openOn: 'hover',
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'window', pin: true, attachment: 'both' }],
|
||||
},
|
||||
});
|
||||
|
||||
drop.open();
|
||||
|
||||
drop.on('close', function() {
|
||||
setTimeout(function() {
|
||||
drop.destroy();
|
||||
});
|
||||
});
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
let markerElementToAttachTo = null;
|
||||
|
||||
/** @ngInject */
|
||||
export function createEditPopover(element, event, plot) {
|
||||
let eventManager = plot.getOptions().events.manager;
|
||||
if (eventManager.editorOpen) {
|
||||
// update marker element to attach to (needed in case of legend on the right
|
||||
// when there is a double render pass and the inital marker element is removed)
|
||||
markerElementToAttachTo = element;
|
||||
return;
|
||||
}
|
||||
|
||||
// mark as openend
|
||||
eventManager.editorOpened();
|
||||
// set marker elment to attache to
|
||||
markerElementToAttachTo = element;
|
||||
|
||||
// wait for element to be attached and positioned
|
||||
setTimeout(function() {
|
||||
let injector = angular.element(document).injector();
|
||||
let content = document.createElement('div');
|
||||
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
|
||||
|
||||
injector.invoke([
|
||||
'$compile',
|
||||
'$rootScope',
|
||||
function($compile, $rootScope) {
|
||||
let scope = $rootScope.$new(true);
|
||||
let drop;
|
||||
|
||||
scope.event = event;
|
||||
scope.panelCtrl = eventManager.panelCtrl;
|
||||
scope.close = function() {
|
||||
drop.close();
|
||||
};
|
||||
|
||||
$compile(content)(scope);
|
||||
scope.$digest();
|
||||
|
||||
drop = new Drop({
|
||||
target: markerElementToAttachTo[0],
|
||||
content: content,
|
||||
position: 'bottom center',
|
||||
classes: 'drop-popover drop-popover--form',
|
||||
openOn: 'click',
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'window', pin: true, attachment: 'both' }],
|
||||
},
|
||||
});
|
||||
|
||||
drop.open();
|
||||
eventManager.editorOpened();
|
||||
|
||||
drop.on('close', function() {
|
||||
// need timeout here in order call drop.destroy
|
||||
setTimeout(function() {
|
||||
eventManager.editorClosed();
|
||||
scope.$destroy();
|
||||
drop.destroy();
|
||||
});
|
||||
});
|
||||
},
|
||||
]);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/*
|
||||
* jquery.flot.events
|
||||
*
|
||||
* description: Flot plugin for adding events/markers to the plot
|
||||
* version: 0.2.5
|
||||
* authors:
|
||||
* Alexander Wunschik <alex@wunschik.net>
|
||||
* Joel Oughton <joeloughton@gmail.com>
|
||||
* Nicolas Joseph <www.nicolasjoseph.com>
|
||||
*
|
||||
* website: https://github.com/mojoaxel/flot-events
|
||||
*
|
||||
* released under MIT License and GPLv2+
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class that allows for the drawing an remove of some object
|
||||
*/
|
||||
export class DrawableEvent {
|
||||
_object: any;
|
||||
_drawFunc: any;
|
||||
_clearFunc: any;
|
||||
_moveFunc: any;
|
||||
_position: any;
|
||||
_width: any;
|
||||
_height: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
|
||||
this._object = object;
|
||||
this._drawFunc = drawFunc;
|
||||
this._clearFunc = clearFunc;
|
||||
this._moveFunc = moveFunc;
|
||||
this._position = { left: left, top: top };
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
}
|
||||
|
||||
width() {
|
||||
return this._width;
|
||||
}
|
||||
height() {
|
||||
return this._height;
|
||||
}
|
||||
position() {
|
||||
return this._position;
|
||||
}
|
||||
draw() {
|
||||
this._drawFunc(this._object);
|
||||
}
|
||||
clear() {
|
||||
this._clearFunc(this._object);
|
||||
}
|
||||
getObject() {
|
||||
return this._object;
|
||||
}
|
||||
moveTo(position) {
|
||||
this._position = position;
|
||||
this._moveFunc(this._object, this._position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event class that stores options (eventType, min, max, title, description) and the object to draw.
|
||||
*/
|
||||
export class VisualEvent {
|
||||
_parent: any;
|
||||
_options: any;
|
||||
_drawableEvent: any;
|
||||
_hidden: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(options, drawableEvent) {
|
||||
this._options = options;
|
||||
this._drawableEvent = drawableEvent;
|
||||
this._hidden = false;
|
||||
}
|
||||
|
||||
visual() {
|
||||
return this._drawableEvent;
|
||||
}
|
||||
getOptions() {
|
||||
return this._options;
|
||||
}
|
||||
getParent() {
|
||||
return this._parent;
|
||||
}
|
||||
isHidden() {
|
||||
return this._hidden;
|
||||
}
|
||||
hide() {
|
||||
this._hidden = true;
|
||||
}
|
||||
unhide() {
|
||||
this._hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Class that handles the event-markers inside the given plot
|
||||
*/
|
||||
export class EventMarkers {
|
||||
_events: any;
|
||||
_types: any;
|
||||
_plot: any;
|
||||
eventsEnabled: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(plot) {
|
||||
this._events = [];
|
||||
this._types = [];
|
||||
this._plot = plot;
|
||||
this.eventsEnabled = false;
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this._events;
|
||||
}
|
||||
|
||||
setTypes(types) {
|
||||
return (this._types = types);
|
||||
}
|
||||
|
||||
/**
|
||||
* create internal objects for the given events
|
||||
*/
|
||||
setupEvents(events) {
|
||||
let parts = _.partition(events, 'isRegion');
|
||||
let regions = parts[0];
|
||||
events = parts[1];
|
||||
|
||||
$.each(events, (index, event) => {
|
||||
let ve = new VisualEvent(event, this._buildDiv(event));
|
||||
this._events.push(ve);
|
||||
});
|
||||
|
||||
$.each(regions, (index, event) => {
|
||||
let vre = new VisualEvent(event, this._buildRegDiv(event));
|
||||
this._events.push(vre);
|
||||
});
|
||||
|
||||
this._events.sort((a, b) => {
|
||||
let ao = a.getOptions(),
|
||||
bo = b.getOptions();
|
||||
if (ao.min > bo.min) {
|
||||
return 1;
|
||||
}
|
||||
if (ao.min < bo.min) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* draw the events to the plot
|
||||
*/
|
||||
drawEvents() {
|
||||
// var o = this._plot.getPlotOffset();
|
||||
|
||||
$.each(this._events, (index, event) => {
|
||||
// check event is inside the graph range
|
||||
if (this._insidePlot(event.getOptions().min) && !event.isHidden()) {
|
||||
event.visual().draw();
|
||||
} else {
|
||||
event
|
||||
.visual()
|
||||
.getObject()
|
||||
.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* update the position of the event-markers (e.g. after scrolling or zooming)
|
||||
*/
|
||||
updateEvents() {
|
||||
let o = this._plot.getPlotOffset(),
|
||||
left,
|
||||
top;
|
||||
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
|
||||
$.each(this._events, (index, event) => {
|
||||
top = o.top + this._plot.height() - event.visual().height();
|
||||
left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
|
||||
event.visual().moveTo({ top: top, left: left });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* remove all events from the plot
|
||||
*/
|
||||
_clearEvents() {
|
||||
$.each(this._events, (index, val) => {
|
||||
val.visual().clear();
|
||||
});
|
||||
this._events = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* create a DOM element for the given event
|
||||
*/
|
||||
_buildDiv(event) {
|
||||
let that = this;
|
||||
|
||||
let container = this._plot.getPlaceholder();
|
||||
let o = this._plot.getPlotOffset();
|
||||
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
let top, left, color, markerSize, markerShow, lineStyle, lineWidth;
|
||||
let markerTooltip;
|
||||
|
||||
// map the eventType to a types object
|
||||
let eventTypeId = event.eventType;
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
|
||||
color = '#666';
|
||||
} else {
|
||||
color = this._types[eventTypeId].color;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
|
||||
markerSize = 8; //default marker size
|
||||
} else {
|
||||
markerSize = this._types[eventTypeId].markerSize;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
|
||||
markerShow = true;
|
||||
} else {
|
||||
markerShow = this._types[eventTypeId].markerShow;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
|
||||
markerTooltip = true;
|
||||
} else {
|
||||
markerTooltip = this._types[eventTypeId].markerTooltip;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
|
||||
lineStyle = 'dashed'; //default line style
|
||||
} else {
|
||||
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
|
||||
lineWidth = 1; //default line width
|
||||
} else {
|
||||
lineWidth = this._types[eventTypeId].lineWidth;
|
||||
}
|
||||
|
||||
let topOffset = xaxis.options.eventSectionHeight || 0;
|
||||
topOffset = topOffset / 3;
|
||||
|
||||
top = o.top + this._plot.height() + topOffset;
|
||||
left = xaxis.p2c(event.min) + o.left;
|
||||
|
||||
let line = $('<div class="events_line flot-temp-elem"></div>')
|
||||
.css({
|
||||
position: 'absolute',
|
||||
opacity: 0.8,
|
||||
left: left + 'px',
|
||||
top: 8,
|
||||
width: lineWidth + 'px',
|
||||
height: this._plot.height() + topOffset * 0.8,
|
||||
'border-left-width': lineWidth + 'px',
|
||||
'border-left-style': lineStyle,
|
||||
'border-left-color': color,
|
||||
color: color,
|
||||
})
|
||||
.appendTo(container);
|
||||
|
||||
if (markerShow) {
|
||||
let marker = $('<div class="events_marker"></div>').css({
|
||||
position: 'absolute',
|
||||
left: -markerSize - Math.round(lineWidth / 2) + 'px',
|
||||
'font-size': 0,
|
||||
'line-height': 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
'border-left': markerSize + 'px solid transparent',
|
||||
'border-right': markerSize + 'px solid transparent',
|
||||
});
|
||||
|
||||
marker.appendTo(line);
|
||||
|
||||
if (
|
||||
this._types[eventTypeId] &&
|
||||
this._types[eventTypeId].position &&
|
||||
this._types[eventTypeId].position.toUpperCase() === 'BOTTOM'
|
||||
) {
|
||||
marker.css({
|
||||
top: top - markerSize - 8 + 'px',
|
||||
'border-top': 'none',
|
||||
'border-bottom': markerSize + 'px solid ' + color,
|
||||
});
|
||||
} else {
|
||||
marker.css({
|
||||
top: '0px',
|
||||
'border-top': markerSize + 'px solid ' + color,
|
||||
'border-bottom': 'none',
|
||||
});
|
||||
}
|
||||
|
||||
marker.data({
|
||||
event: event,
|
||||
});
|
||||
|
||||
let mouseenter = function() {
|
||||
createAnnotationToolip(marker, $(this).data('event'), that._plot);
|
||||
};
|
||||
|
||||
if (event.editModel) {
|
||||
createEditPopover(marker, event.editModel, that._plot);
|
||||
}
|
||||
|
||||
let mouseleave = function() {
|
||||
that._plot.clearSelection();
|
||||
};
|
||||
|
||||
if (markerTooltip) {
|
||||
marker.css({ cursor: 'help' });
|
||||
marker.hover(mouseenter, mouseleave);
|
||||
}
|
||||
}
|
||||
|
||||
let drawableEvent = new DrawableEvent(
|
||||
line,
|
||||
function drawFunc(obj) {
|
||||
obj.show();
|
||||
},
|
||||
function(obj) {
|
||||
obj.remove();
|
||||
},
|
||||
function(obj, position) {
|
||||
obj.css({
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
});
|
||||
},
|
||||
left,
|
||||
top,
|
||||
line.width(),
|
||||
line.height()
|
||||
);
|
||||
|
||||
return drawableEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a DOM element for the given region
|
||||
*/
|
||||
_buildRegDiv(event) {
|
||||
let that = this;
|
||||
|
||||
let container = this._plot.getPlaceholder();
|
||||
let o = this._plot.getPlotOffset();
|
||||
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
let top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
|
||||
|
||||
// map the eventType to a types object
|
||||
let eventTypeId = event.eventType;
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
|
||||
color = '#666';
|
||||
} else {
|
||||
color = this._types[eventTypeId].color;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
|
||||
markerTooltip = true;
|
||||
} else {
|
||||
markerTooltip = this._types[eventTypeId].markerTooltip;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
|
||||
lineWidth = 1; //default line width
|
||||
} else {
|
||||
lineWidth = this._types[eventTypeId].lineWidth;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
|
||||
lineStyle = 'dashed'; //default line style
|
||||
} else {
|
||||
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
|
||||
}
|
||||
|
||||
let topOffset = 2;
|
||||
top = o.top + this._plot.height() + topOffset;
|
||||
|
||||
let timeFrom = Math.min(event.min, event.timeEnd);
|
||||
let timeTo = Math.max(event.min, event.timeEnd);
|
||||
left = xaxis.p2c(timeFrom) + o.left;
|
||||
let right = xaxis.p2c(timeTo) + o.left;
|
||||
regionWidth = right - left;
|
||||
|
||||
_.each([left, right], position => {
|
||||
let line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||
position: 'absolute',
|
||||
opacity: 0.8,
|
||||
left: position + 'px',
|
||||
top: 8,
|
||||
width: lineWidth + 'px',
|
||||
height: this._plot.height() + topOffset,
|
||||
'border-left-width': lineWidth + 'px',
|
||||
'border-left-style': lineStyle,
|
||||
'border-left-color': color,
|
||||
color: color,
|
||||
});
|
||||
line.appendTo(container);
|
||||
});
|
||||
|
||||
let region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
|
||||
position: 'absolute',
|
||||
opacity: 0.5,
|
||||
left: left + 'px',
|
||||
top: top,
|
||||
width: Math.round(regionWidth + lineWidth) + 'px',
|
||||
height: '0.5rem',
|
||||
'border-left-color': color,
|
||||
color: color,
|
||||
'background-color': color,
|
||||
});
|
||||
region.appendTo(container);
|
||||
|
||||
region.data({
|
||||
event: event,
|
||||
});
|
||||
|
||||
let mouseenter = function() {
|
||||
createAnnotationToolip(region, $(this).data('event'), that._plot);
|
||||
};
|
||||
|
||||
if (event.editModel) {
|
||||
createEditPopover(region, event.editModel, that._plot);
|
||||
}
|
||||
|
||||
let mouseleave = function() {
|
||||
that._plot.clearSelection();
|
||||
};
|
||||
|
||||
if (markerTooltip) {
|
||||
region.css({ cursor: 'help' });
|
||||
region.hover(mouseenter, mouseleave);
|
||||
}
|
||||
|
||||
let drawableEvent = new DrawableEvent(
|
||||
region,
|
||||
function drawFunc(obj) {
|
||||
obj.show();
|
||||
},
|
||||
function(obj) {
|
||||
obj.remove();
|
||||
},
|
||||
function(obj, position) {
|
||||
obj.css({
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
});
|
||||
},
|
||||
left,
|
||||
top,
|
||||
region.width(),
|
||||
region.height()
|
||||
);
|
||||
|
||||
return drawableEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the event is inside visible range
|
||||
*/
|
||||
_insidePlot(x) {
|
||||
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
let xc = xaxis.p2c(x);
|
||||
return xc > 0 && xc < xaxis.p2c(xaxis.max);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize the plugin for the given plot
|
||||
*/
|
||||
|
||||
/** @ngInject */
|
||||
export function init(plot) {
|
||||
/*jshint validthis:true */
|
||||
let that = this;
|
||||
let eventMarkers = new EventMarkers(plot);
|
||||
|
||||
plot.getEvents = function() {
|
||||
return eventMarkers._events;
|
||||
};
|
||||
|
||||
plot.hideEvents = function() {
|
||||
$.each(eventMarkers._events, (index, event) => {
|
||||
event
|
||||
.visual()
|
||||
.getObject()
|
||||
.hide();
|
||||
});
|
||||
};
|
||||
|
||||
plot.showEvents = function() {
|
||||
plot.hideEvents();
|
||||
$.each(eventMarkers._events, (index, event) => {
|
||||
event.hide();
|
||||
});
|
||||
|
||||
that.eventMarkers.drawEvents();
|
||||
};
|
||||
|
||||
// change events on an existing plot
|
||||
plot.setEvents = function(events) {
|
||||
if (eventMarkers.eventsEnabled) {
|
||||
eventMarkers.setupEvents(events);
|
||||
}
|
||||
};
|
||||
|
||||
plot.hooks.processOptions.push(function(plot, options) {
|
||||
// enable the plugin
|
||||
if (options.events.data != null) {
|
||||
eventMarkers.eventsEnabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
plot.hooks.draw.push(function(plot) {
|
||||
let options = plot.getOptions();
|
||||
|
||||
if (eventMarkers.eventsEnabled) {
|
||||
// check for first run
|
||||
if (eventMarkers.getEvents().length < 1) {
|
||||
eventMarkers.setTypes(options.events.types);
|
||||
eventMarkers.setupEvents(options.events.data);
|
||||
} else {
|
||||
eventMarkers.updateEvents();
|
||||
}
|
||||
}
|
||||
|
||||
eventMarkers.drawEvents();
|
||||
});
|
||||
}
|
||||
|
||||
let defaultOptions = {
|
||||
events: {
|
||||
data: null,
|
||||
types: null,
|
||||
xaxis: 1,
|
||||
position: 'BOTTOM',
|
||||
},
|
||||
};
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: defaultOptions,
|
||||
name: 'events',
|
||||
version: '0.2.5',
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import moment from 'moment';
|
||||
import { GraphCtrl } from '../module';
|
||||
|
||||
jest.mock('../graph', () => ({}));
|
||||
|
||||
describe('GraphCtrl', () => {
|
||||
let injector = {
|
||||
get: () => {
|
||||
return {
|
||||
timeRange: () => {
|
||||
return {
|
||||
from: '',
|
||||
to: '',
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
let scope = {
|
||||
$on: () => {},
|
||||
};
|
||||
|
||||
GraphCtrl.prototype.panel = {
|
||||
events: {
|
||||
on: () => {},
|
||||
},
|
||||
gridPos: {
|
||||
w: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let ctx = <any>{};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.ctrl = new GraphCtrl(scope, injector, {});
|
||||
ctx.ctrl.annotationsPromise = Promise.resolve({});
|
||||
ctx.ctrl.updateTimeRange();
|
||||
});
|
||||
|
||||
describe('when time series are outside range', () => {
|
||||
beforeEach(() => {
|
||||
var data = [
|
||||
{
|
||||
target: 'test.cpu1',
|
||||
datapoints: [[45, 1234567890], [60, 1234567899]],
|
||||
},
|
||||
];
|
||||
|
||||
ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
|
||||
ctx.ctrl.onDataReceived(data);
|
||||
});
|
||||
|
||||
it('should set datapointsOutside', () => {
|
||||
expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when time series are inside range', () => {
|
||||
beforeEach(() => {
|
||||
var range = {
|
||||
from: moment()
|
||||
.subtract(1, 'days')
|
||||
.valueOf(),
|
||||
to: moment().valueOf(),
|
||||
};
|
||||
|
||||
var data = [
|
||||
{
|
||||
target: 'test.cpu1',
|
||||
datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
|
||||
},
|
||||
];
|
||||
|
||||
ctx.ctrl.range = range;
|
||||
ctx.ctrl.onDataReceived(data);
|
||||
});
|
||||
|
||||
it('should set datapointsOutside', () => {
|
||||
expect(ctx.ctrl.dataWarning).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('datapointsCount given 2 series', () => {
|
||||
beforeEach(() => {
|
||||
var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
|
||||
ctx.ctrl.onDataReceived(data);
|
||||
});
|
||||
|
||||
it('should set datapointsCount warning', () => {
|
||||
expect(ctx.ctrl.dataWarning.title).toBe('No data points');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
|
||||
|
||||
import moment from 'moment';
|
||||
import { GraphCtrl } from '../module';
|
||||
import helpers from '../../../../../test/specs/helpers';
|
||||
|
||||
describe('GraphCtrl', function() {
|
||||
var ctx = new helpers.ControllerTestContext();
|
||||
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
beforeEach(
|
||||
angularMocks.module(function($compileProvider) {
|
||||
$compileProvider.preAssignBindingsEnabled(true);
|
||||
})
|
||||
);
|
||||
|
||||
beforeEach(ctx.providePhase());
|
||||
beforeEach(ctx.createPanelController(GraphCtrl));
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.annotationsPromise = Promise.resolve({});
|
||||
ctx.ctrl.updateTimeRange();
|
||||
});
|
||||
|
||||
describe('when time series are outside range', function() {
|
||||
beforeEach(function() {
|
||||
var data = [
|
||||
{
|
||||
target: 'test.cpu1',
|
||||
datapoints: [[45, 1234567890], [60, 1234567899]],
|
||||
},
|
||||
];
|
||||
|
||||
ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
|
||||
ctx.ctrl.onDataReceived(data);
|
||||
});
|
||||
|
||||
it('should set datapointsOutside', function() {
|
||||
expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when time series are inside range', function() {
|
||||
beforeEach(function() {
|
||||
var range = {
|
||||
from: moment()
|
||||
.subtract(1, 'days')
|
||||
.valueOf(),
|
||||
to: moment().valueOf(),
|
||||
};
|
||||
|
||||
var data = [
|
||||
{
|
||||
target: 'test.cpu1',
|
||||
datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
|
||||
},
|
||||
];
|
||||
|
||||
ctx.ctrl.range = range;
|
||||
ctx.ctrl.onDataReceived(data);
|
||||
});
|
||||
|
||||
it('should set datapointsOutside', function() {
|
||||
expect(ctx.ctrl.dataWarning).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('datapointsCount given 2 series', function() {
|
||||
beforeEach(function() {
|
||||
var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
|
||||
ctx.ctrl.onDataReceived(data);
|
||||
});
|
||||
|
||||
it('should set datapointsCount warning', function() {
|
||||
expect(ctx.ctrl.dataWarning.title).to.be('No data points');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,10 +56,10 @@
|
||||
<h5 class="section-heading">Coloring</h5>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-6" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label-class="width-6" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-8" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
|
||||
@@ -112,7 +112,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
controller: 'FolderDashboardsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/explore/:initial?', {
|
||||
.when('/explore', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
roles: () => ['Editor', 'Admin'],
|
||||
|
||||
@@ -93,24 +93,14 @@ $headings-color: darken($white, 11%);
|
||||
$abbr-border-color: $gray-3 !default;
|
||||
$text-muted: $text-color-weak;
|
||||
|
||||
$blockquote-small-color: $gray-3 !default;
|
||||
$blockquote-border-color: $gray-4 !default;
|
||||
|
||||
$hr-border-color: rgba(0, 0, 0, 0.1) !default;
|
||||
|
||||
// Components
|
||||
$component-active-color: #fff !default;
|
||||
$component-active-bg: $brand-primary !default;
|
||||
|
||||
// Panel
|
||||
// -------------------------
|
||||
$panel-bg: #212124;
|
||||
$panel-border-color: $dark-1;
|
||||
$panel-border: solid 1px $panel-border-color;
|
||||
$panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191919 10px, #222 20px);
|
||||
$panel-header-hover-bg: $dark-4;
|
||||
$panel-header-menu-hover-bg: $dark-5;
|
||||
$panel-edit-shadow: 0 -30px 30px -30px $black;
|
||||
|
||||
// page header
|
||||
$page-header-bg: linear-gradient(90deg, #292a2d, black);
|
||||
@@ -205,7 +195,6 @@ $input-box-shadow-focus: rgba(102, 175, 233, 0.6);
|
||||
$input-color-placeholder: $gray-1 !default;
|
||||
$input-label-bg: $gray-blue;
|
||||
$input-label-border-color: $dark-3;
|
||||
$input-invalid-border-color: lighten($red, 5%);
|
||||
|
||||
// Search
|
||||
$search-shadow: 0 0 30px 0 $black;
|
||||
@@ -223,7 +212,6 @@ $dropdownBorder: rgba(0, 0, 0, 0.2);
|
||||
$dropdownDividerTop: transparent;
|
||||
$dropdownDividerBottom: #444;
|
||||
$dropdownDivider: $dropdownDividerBottom;
|
||||
$dropdownTitle: $link-color-disabled;
|
||||
|
||||
$dropdownLinkColor: $text-color;
|
||||
$dropdownLinkColorHover: $white;
|
||||
@@ -232,8 +220,6 @@ $dropdownLinkColorActive: $white;
|
||||
$dropdownLinkBackgroundActive: $dark-4;
|
||||
$dropdownLinkBackgroundHover: $dark-4;
|
||||
|
||||
$dropdown-link-color: $gray-3;
|
||||
|
||||
// COMPONENT VARIABLES
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -246,22 +232,13 @@ $horizontalComponentOffset: 180px;
|
||||
|
||||
// Wells
|
||||
// -------------------------
|
||||
$wellBackground: #131517;
|
||||
|
||||
$navbarHeight: 55px;
|
||||
$navbarBackgroundHighlight: $dark-3;
|
||||
$navbarBackground: $panel-bg;
|
||||
$navbarBorder: 1px solid $dark-3;
|
||||
$navbarShadow: 0 0 20px black;
|
||||
|
||||
$navbarText: $gray-4;
|
||||
$navbarLinkColor: $gray-4;
|
||||
$navbarLinkColorHover: $white;
|
||||
$navbarLinkColorActive: $navbarLinkColorHover;
|
||||
$navbarLinkBackgroundHover: transparent;
|
||||
$navbarLinkBackgroundActive: $navbarBackground;
|
||||
$navbarBrandColor: $link-color;
|
||||
$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg;
|
||||
|
||||
$navbarButtonBackground: $navbarBackground;
|
||||
$navbarButtonBackgroundHighlight: $body-bg;
|
||||
@@ -275,20 +252,15 @@ $side-menu-bg-mobile: $side-menu-bg;
|
||||
$side-menu-item-hover-bg: $dark-2;
|
||||
$side-menu-shadow: 0 0 20px black;
|
||||
$side-menu-link-color: $link-color;
|
||||
$breadcrumb-hover-hl: #111;
|
||||
|
||||
// Menu dropdowns
|
||||
// -------------------------
|
||||
$menu-dropdown-bg: $body-bg;
|
||||
$menu-dropdown-hover-bg: $dark-2;
|
||||
$menu-dropdown-border-color: $dark-3;
|
||||
$menu-dropdown-shadow: 5px 5px 20px -5px $black;
|
||||
|
||||
// Breadcrumb
|
||||
// -------------------------
|
||||
$page-nav-bg: $black;
|
||||
$page-nav-shadow: 5px 5px 20px -5px $black;
|
||||
$page-nav-breadcrumb-color: $gray-3;
|
||||
|
||||
// Tabs
|
||||
// -------------------------
|
||||
@@ -296,9 +268,6 @@ $tab-border-color: $dark-4;
|
||||
|
||||
// Pagination
|
||||
// -------------------------
|
||||
$paginationBackground: $body-bg;
|
||||
$paginationBorder: transparent;
|
||||
$paginationActiveBackground: $blue;
|
||||
|
||||
// Form states and alerts
|
||||
// -------------------------
|
||||
@@ -343,10 +312,6 @@ $info-box-color: $gray-4;
|
||||
$footer-link-color: $gray-2;
|
||||
$footer-link-hover: $gray-4;
|
||||
|
||||
// collapse box
|
||||
$collapse-box-body-border: $dark-5;
|
||||
$collapse-box-body-error-border: $red;
|
||||
|
||||
// json-explorer
|
||||
$json-explorer-default-color: $text-color;
|
||||
$json-explorer-string-color: #23d662;
|
||||
@@ -357,7 +322,6 @@ $json-explorer-undefined-color: rgb(239, 143, 190);
|
||||
$json-explorer-function-color: #fd48cb;
|
||||
$json-explorer-rotate-time: 100ms;
|
||||
$json-explorer-toggler-opacity: 0.6;
|
||||
$json-explorer-toggler-color: #45376f;
|
||||
$json-explorer-bracket-color: #9494ff;
|
||||
$json-explorer-key-color: #23a0db;
|
||||
$json-explorer-url-color: #027bff;
|
||||
|
||||
@@ -90,25 +90,15 @@ $headings-color: $text-color;
|
||||
$abbr-border-color: $gray-2 !default;
|
||||
$text-muted: $text-color-weak;
|
||||
|
||||
$blockquote-small-color: $gray-2 !default;
|
||||
$blockquote-border-color: $gray-3 !default;
|
||||
|
||||
$hr-border-color: $dark-3 !default;
|
||||
|
||||
// Components
|
||||
$component-active-color: $white !default;
|
||||
$component-active-bg: $brand-primary !default;
|
||||
|
||||
// Panel
|
||||
// -------------------------
|
||||
|
||||
$panel-bg: $white;
|
||||
$panel-border-color: $gray-5;
|
||||
$panel-border: solid 1px $panel-border-color;
|
||||
$panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px);
|
||||
$panel-header-hover-bg: $gray-6;
|
||||
$panel-header-menu-hover-bg: $gray-4;
|
||||
$panel-edit-shadow: 0 0 30px 20px $black;
|
||||
|
||||
// Page header
|
||||
$page-header-bg: linear-gradient(90deg, $white, $gray-7);
|
||||
@@ -201,7 +191,6 @@ $input-box-shadow-focus: $blue !default;
|
||||
$input-color-placeholder: $gray-4 !default;
|
||||
$input-label-bg: $gray-5;
|
||||
$input-label-border-color: $gray-5;
|
||||
$input-invalid-border-color: lighten($red, 5%);
|
||||
|
||||
// Sidemenu
|
||||
// -------------------------
|
||||
@@ -215,15 +204,10 @@ $side-menu-link-color: $gray-6;
|
||||
// -------------------------
|
||||
$menu-dropdown-bg: $gray-7;
|
||||
$menu-dropdown-hover-bg: $gray-6;
|
||||
$menu-dropdown-border-color: $gray-4;
|
||||
$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
|
||||
|
||||
// Breadcrumb
|
||||
// -------------------------
|
||||
$page-nav-bg: $gray-5;
|
||||
$page-nav-shadow: 5px 5px 20px -5px $gray-4;
|
||||
$page-nav-breadcrumb-color: $black;
|
||||
$breadcrumb-hover-hl: #d9dadd;
|
||||
|
||||
// Tabs
|
||||
// -------------------------
|
||||
@@ -245,7 +229,6 @@ $dropdownBorder: $gray-4;
|
||||
$dropdownDividerTop: $gray-6;
|
||||
$dropdownDividerBottom: $white;
|
||||
$dropdownDivider: $dropdownDividerTop;
|
||||
$dropdownTitle: $gray-3;
|
||||
|
||||
$dropdownLinkColor: $dark-3;
|
||||
$dropdownLinkColorHover: $link-color;
|
||||
@@ -271,24 +254,16 @@ $horizontalComponentOffset: 180px;
|
||||
|
||||
// Wells
|
||||
// -------------------------
|
||||
$wellBackground: $gray-3;
|
||||
|
||||
// Navbar
|
||||
// -------------------------
|
||||
|
||||
$navbarHeight: 52px;
|
||||
$navbarBackgroundHighlight: $white;
|
||||
$navbarBackground: $white;
|
||||
$navbarBorder: 1px solid $gray-4;
|
||||
$navbarShadow: 0 0 3px #c1c1c1;
|
||||
|
||||
$navbarText: #444;
|
||||
$navbarLinkColor: #444;
|
||||
$navbarLinkColorHover: #000;
|
||||
$navbarLinkColorActive: #333;
|
||||
$navbarLinkBackgroundHover: transparent;
|
||||
$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%);
|
||||
$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%);
|
||||
|
||||
$navbarBrandColor: $navbarLinkColor;
|
||||
|
||||
@@ -299,9 +274,6 @@ $navbar-button-border: $gray-4;
|
||||
|
||||
// Pagination
|
||||
// -------------------------
|
||||
$paginationBackground: $gray-2;
|
||||
$paginationBorder: transparent;
|
||||
$paginationActiveBackground: $blue;
|
||||
|
||||
// Form states and alerts
|
||||
// -------------------------
|
||||
@@ -346,8 +318,6 @@ $footer-link-color: $gray-3;
|
||||
$footer-link-hover: $dark-5;
|
||||
|
||||
// collapse box
|
||||
$collapse-box-body-border: $gray-4;
|
||||
$collapse-box-body-error-border: $red;
|
||||
|
||||
// json explorer
|
||||
$json-explorer-default-color: black;
|
||||
@@ -359,7 +329,6 @@ $json-explorer-undefined-color: rgb(202, 11, 105);
|
||||
$json-explorer-function-color: #ff20ed;
|
||||
$json-explorer-rotate-time: 100ms;
|
||||
$json-explorer-toggler-opacity: 0.6;
|
||||
$json-explorer-toggler-color: #45376f;
|
||||
$json-explorer-bracket-color: blue;
|
||||
$json-explorer-key-color: #00008b;
|
||||
$json-explorer-url-color: blue;
|
||||
|
||||
@@ -3,13 +3,7 @@
|
||||
// Quickly modify global styling by enabling or disabling optional features.
|
||||
|
||||
$enable-flex: true !default;
|
||||
$enable-rounded: true !default;
|
||||
$enable-shadows: false !default;
|
||||
$enable-gradients: false !default;
|
||||
$enable-transitions: false !default;
|
||||
$enable-hover-media-query: false !default;
|
||||
$enable-grid-classes: true !default;
|
||||
$enable-print-styles: true !default;
|
||||
|
||||
// Spacing
|
||||
//
|
||||
@@ -53,9 +47,9 @@ $enable-flex: true;
|
||||
// Typography
|
||||
// -------------------------
|
||||
|
||||
$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif;
|
||||
$font-family-serif: Georgia, "Times New Roman", Times, serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$font-family-serif: Georgia, 'Times New Roman', Times, serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
$font-family-base: $font-family-sans-serif !default;
|
||||
|
||||
$font-size-root: 14px !default;
|
||||
@@ -90,16 +84,12 @@ $lead-font-size: 1.25rem !default;
|
||||
$lead-font-weight: 300 !default;
|
||||
|
||||
$headings-margin-bottom: ($spacer / 2) !default;
|
||||
$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$headings-font-weight: 400 !default;
|
||||
$headings-line-height: 1.1 !default;
|
||||
|
||||
$blockquote-font-size: ($font-size-base * 1.25) !default;
|
||||
$blockquote-border-width: 0.25rem !default;
|
||||
|
||||
$hr-border-width: $border-width !default;
|
||||
$dt-font-weight: bold !default;
|
||||
$list-inline-padding: 5px !default;
|
||||
|
||||
// Components
|
||||
//
|
||||
@@ -112,9 +102,6 @@ $border-radius: 3px !default;
|
||||
$border-radius-lg: 5px !default;
|
||||
$border-radius-sm: 2px!default;
|
||||
|
||||
$caret-width: 0.3em !default;
|
||||
$caret-width-lg: $caret-width !default;
|
||||
|
||||
// Page
|
||||
|
||||
$page-sidebar-width: 11rem;
|
||||
@@ -130,7 +117,6 @@ $link-hover-decoration: none !default;
|
||||
// Customizes the `.table` component with basic values, each used across all table variations.
|
||||
|
||||
$table-cell-padding: 4px 10px !default;
|
||||
$table-sm-cell-padding: 0.3rem !default;
|
||||
|
||||
// Forms
|
||||
$input-padding-x: 10px !default;
|
||||
@@ -139,31 +125,18 @@ $input-line-height: 18px !default;
|
||||
|
||||
$input-btn-border-width: 1px;
|
||||
$input-border-radius: 0 $border-radius $border-radius 0 !default;
|
||||
$input-border-radius-lg: 0 $border-radius-lg $border-radius-lg 0 !default;
|
||||
$input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
|
||||
|
||||
$label-border-radius: $border-radius 0 0 $border-radius !default;
|
||||
$label-border-radius-lg: $border-radius-lg 0 0 $border-radius-lg !default;
|
||||
$label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
|
||||
|
||||
$input-padding-x-sm: 7px !default;
|
||||
$input-padding-y-sm: 4px !default;
|
||||
|
||||
$input-padding-x-lg: 20px !default;
|
||||
$input-padding-y-lg: 10px !default;
|
||||
|
||||
$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2))
|
||||
!default;
|
||||
$input-height-lg: (
|
||||
($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)
|
||||
)
|
||||
!default;
|
||||
$input-height-sm: (
|
||||
($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)
|
||||
)
|
||||
!default;
|
||||
$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default;
|
||||
|
||||
$form-group-margin-bottom: $spacer-y !default;
|
||||
$gf-form-margin: 0.2rem;
|
||||
|
||||
$cursor-disabled: not-allowed !default;
|
||||
@@ -221,9 +194,9 @@ $panel-padding: 0px 10px 5px 10px;
|
||||
$tabs-padding: 10px 15px 9px;
|
||||
|
||||
$external-services: (
|
||||
github: (bgColor: #464646, borderColor: #393939, icon: ""),
|
||||
google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""),
|
||||
grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""),
|
||||
oauth: (bgColor: inherit, borderColor: #393939, icon: "")
|
||||
github: (bgColor: #464646, borderColor: #393939, icon: ''),
|
||||
google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
|
||||
grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''),
|
||||
oauth: (bgColor: inherit, borderColor: #393939, icon: '')
|
||||
)
|
||||
!default;
|
||||
|
||||
@@ -24,7 +24,7 @@ small {
|
||||
font-size: 85%;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
em {
|
||||
font-style: italic;
|
||||
@@ -249,7 +249,7 @@ dd {
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
dt {
|
||||
font-weight: bold;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
dd {
|
||||
margin-left: $line-height-base / 2;
|
||||
@@ -376,7 +376,7 @@ a.external-link {
|
||||
padding: $spacer*0.5 $spacer;
|
||||
}
|
||||
th {
|
||||
font-weight: normal;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
background: $table-bg-accent;
|
||||
}
|
||||
}
|
||||
@@ -415,3 +415,7 @@ a.external-link {
|
||||
color: $yellow;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ div.flot-text {
|
||||
height: 100%;
|
||||
|
||||
&--solo {
|
||||
margin: 0;
|
||||
.panel-container {
|
||||
border: none;
|
||||
z-index: $zindex-sidemenu + 1;
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.datasource-picker {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.timepicker {
|
||||
display: flex;
|
||||
|
||||
@@ -93,3 +97,40 @@
|
||||
.query-row-tools {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.explore {
|
||||
.logs {
|
||||
.logs-entries {
|
||||
display: grid;
|
||||
grid-column-gap: 1rem;
|
||||
grid-row-gap: 0.1rem;
|
||||
grid-template-columns: 4px minmax(100px, max-content) 1fr;
|
||||
font-family: $font-family-monospace;
|
||||
}
|
||||
|
||||
.logs-row-match-highlight {
|
||||
background-color: lighten($blue, 20%);
|
||||
}
|
||||
|
||||
.logs-row-level {
|
||||
background-color: transparent;
|
||||
margin: 6px 0;
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logs-row-level-crit,
|
||||
.logs-row-level-error,
|
||||
.logs-row-level-err {
|
||||
background-color: $red;
|
||||
}
|
||||
|
||||
.logs-row-level-warn {
|
||||
background-color: $orange;
|
||||
}
|
||||
|
||||
.logs-row-level-info {
|
||||
background-color: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -91,6 +91,6 @@ func (c *Client) AddDebugHandlers() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Handlers.Send.PushFrontNamed(request.NamedHandler{Name: "awssdk.client.LogRequest", Fn: logRequest})
|
||||
c.Handlers.Send.PushBackNamed(request.NamedHandler{Name: "awssdk.client.LogResponse", Fn: logResponse})
|
||||
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
|
||||
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
|
||||
}
|
||||
|
||||
+87
-15
@@ -44,12 +44,22 @@ func (reader *teeReaderCloser) Close() error {
|
||||
return reader.Source.Close()
|
||||
}
|
||||
|
||||
// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will include the HTTP request body if the LogLevel of the
|
||||
// request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPRequestHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequest",
|
||||
Fn: logRequest,
|
||||
}
|
||||
|
||||
func logRequest(r *request.Request) {
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
bodySeekable := aws.IsReaderSeekable(r.Body)
|
||||
dumpedBody, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
|
||||
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg, r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,7 +73,28 @@ func logRequest(r *request.Request) {
|
||||
r.ResetBody()
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg, r.ClientInfo.ServiceName, r.Operation.Name, string(dumpedBody)))
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will only log the HTTP request's headers. The request payload
|
||||
// will not be read.
|
||||
var LogHTTPRequestHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequestHeader",
|
||||
Fn: logRequestHeader,
|
||||
}
|
||||
|
||||
func logRequestHeader(r *request.Request) {
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
const logRespMsg = `DEBUG: Response %s/%s Details:
|
||||
@@ -76,27 +107,44 @@ const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
|
||||
// received from a service. Will include the HTTP response body if the LogLevel
|
||||
// of the request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPResponseHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponse",
|
||||
Fn: logResponse,
|
||||
}
|
||||
|
||||
func logResponse(r *request.Request) {
|
||||
lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
|
||||
r.HTTPResponse.Body = &teeReaderCloser{
|
||||
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
|
||||
Source: r.HTTPResponse.Body,
|
||||
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
if logBody {
|
||||
r.HTTPResponse.Body = &teeReaderCloser{
|
||||
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
|
||||
Source: r.HTTPResponse.Body,
|
||||
}
|
||||
}
|
||||
|
||||
handlerFn := func(req *request.Request) {
|
||||
body, err := httputil.DumpResponse(req.HTTPResponse, false)
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, false)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(lw.buf)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
lw.Logger.Log(fmt.Sprintf(logRespMsg, req.ClientInfo.ServiceName, req.Operation.Name, string(body)))
|
||||
if req.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody) {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
|
||||
|
||||
if logBody {
|
||||
b, err := ioutil.ReadAll(lw.buf)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(string(b))
|
||||
}
|
||||
}
|
||||
@@ -110,3 +158,27 @@ func logResponse(r *request.Request) {
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
|
||||
// response received from a service. Will only log the HTTP response's headers.
|
||||
// The response payload will not be read.
|
||||
var LogHTTPResponseHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponseHeader",
|
||||
Fn: logResponseHeader,
|
||||
}
|
||||
|
||||
func logResponseHeader(r *request.Request) {
|
||||
if r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := httputil.DumpResponse(r.HTTPResponse, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ package metadata
|
||||
// ClientInfo wraps immutable data from the client.Client structure.
|
||||
type ClientInfo struct {
|
||||
ServiceName string
|
||||
ServiceID string
|
||||
APIVersion string
|
||||
Endpoint string
|
||||
SigningName string
|
||||
|
||||
+15
-3
@@ -178,7 +178,8 @@ func (e *Expiry) IsExpired() bool {
|
||||
type Credentials struct {
|
||||
creds Value
|
||||
forceRefresh bool
|
||||
m sync.Mutex
|
||||
|
||||
m sync.RWMutex
|
||||
|
||||
provider Provider
|
||||
}
|
||||
@@ -201,6 +202,17 @@ func NewCredentials(provider Provider) *Credentials {
|
||||
// If Credentials.Expire() was called the credentials Value will be force
|
||||
// expired, and the next call to Get() will cause them to be refreshed.
|
||||
func (c *Credentials) Get() (Value, error) {
|
||||
// Check the cached credentials first with just the read lock.
|
||||
c.m.RLock()
|
||||
if !c.isExpired() {
|
||||
creds := c.creds
|
||||
c.m.RUnlock()
|
||||
return creds, nil
|
||||
}
|
||||
c.m.RUnlock()
|
||||
|
||||
// Credentials are expired need to retrieve the credentials taking the full
|
||||
// lock.
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
|
||||
@@ -234,8 +246,8 @@ func (c *Credentials) Expire() {
|
||||
// If the Credentials were forced to be expired with Expire() this will
|
||||
// reflect that override.
|
||||
func (c *Credentials) IsExpired() bool {
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
c.m.RLock()
|
||||
defer c.m.RUnlock()
|
||||
|
||||
return c.isExpired()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user