mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'develop' into panel-edit-in-react
This commit is contained in:
commit
7f46b75330
@ -1,7 +1,7 @@
|
||||
[run]
|
||||
init_cmds = [
|
||||
["go", "run", "build.go", "-dev", "build-server"],
|
||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||
["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
|
||||
]
|
||||
watch_all = true
|
||||
follow_symlinks = true
|
||||
@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"]
|
||||
build_delay = 1500
|
||||
cmds = [
|
||||
["go", "run", "build.go", "-dev", "build-server"],
|
||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||
["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
|
||||
]
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
build-all:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -241,7 +241,7 @@ jobs:
|
||||
|
||||
build-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -273,7 +273,7 @@ jobs:
|
||||
|
||||
build-all-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -359,6 +359,9 @@ jobs:
|
||||
- run:
|
||||
name: deploy to gcp
|
||||
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
|
||||
- run:
|
||||
name: Deploy to Grafana.com
|
||||
command: './scripts/build/publish.sh --enterprise'
|
||||
|
||||
deploy-master:
|
||||
docker:
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -2,6 +2,7 @@
|
||||
|
||||
### New Features
|
||||
|
||||
* **Alerting**: Introduce alert debouncing with the `FOR` setting. [#7886](https://github.com/grafana/grafana/issues/7886) & [#6202](https://github.com/grafana/grafana/issues/6202)
|
||||
* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
|
||||
* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
|
||||
* **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
|
||||
@ -9,30 +10,38 @@
|
||||
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
|
||||
* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
|
||||
* **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550)
|
||||
* **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930)
|
||||
* **OAuth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller](https://github.com/Nick-Triller)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
|
||||
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
|
||||
* **Cloudwatch**: Enable using variables in the stats field [#13810](https://github.com/grafana/grafana/issues/13810), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
|
||||
* **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367)
|
||||
* **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977)
|
||||
* **Graph**: Render dots when no connecting line can be made [#13605](https://github.com/grafana/grafana/issues/13605), thx [@jsferrei](https://github.com/jsferrei)
|
||||
* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
|
||||
* **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia)
|
||||
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
|
||||
* **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov)
|
||||
* **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
||||
* **Alerting**: Increase Telegram captions length limit [#13876](https://github.com/grafana/grafana/pull/13876), thx [@skgsergio](https://github.com/skgsergio)
|
||||
* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
|
||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
||||
* **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo)
|
||||
* **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat)
|
||||
* **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry)
|
||||
* **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425)
|
||||
* **Render**: Support full height screenshots using phantomjs render script [#13352](https://github.com/grafana/grafana/pull/13352), thx [@amuraru](https://github.com/amuraru)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
|
||||
|
||||
# 5.3.5 (unreleased)
|
||||
|
||||
* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
|
||||
|
||||
# 5.3.4 (2018-11-13)
|
||||
|
||||
* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)
|
||||
|
2
Makefile
2
Makefile
@ -25,7 +25,7 @@ build: build-go build-js
|
||||
|
||||
build-docker-dev:
|
||||
@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest
|
||||
cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
cd packaging/docker && docker build --tag grafana/grafana:dev .
|
||||
|
||||
|
2
build.go
2
build.go
@ -128,6 +128,8 @@ func main() {
|
||||
if goos == linux {
|
||||
createLinuxPackages()
|
||||
}
|
||||
case "pkg-archive":
|
||||
grunt(gruntBuildArg("package")...)
|
||||
|
||||
case "pkg-rpm":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
|
511
devenv/dev-dashboards/panel_tests_graph_time_regions.json
Normal file
511
devenv/dev-dashboards/panel_tests_graph_time_regions.json
Normal file
@ -0,0 +1,511 @@
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "gray",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 255, 255, 0.03)",
|
||||
"from": "08:30",
|
||||
"fromDayOfWeek": 1,
|
||||
"line": false,
|
||||
"lineColor": "rgba(255, 255, 255, 0.2)",
|
||||
"op": "time",
|
||||
"to": "16:45",
|
||||
"toDayOfWeek": 5
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "Business Hours",
|
||||
"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": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "red",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 255, 255, 0.03)",
|
||||
"from": "20:00",
|
||||
"fromDayOfWeek": 7,
|
||||
"line": false,
|
||||
"lineColor": "rgba(255, 255, 255, 0.2)",
|
||||
"op": "time",
|
||||
"to": "23:00",
|
||||
"toDayOfWeek": 7
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "Sunday's 20-23",
|
||||
"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": {
|
||||
"A-series": "#d683ce"
|
||||
},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 3,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 0.5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 0, 0, 0.22)",
|
||||
"from": "",
|
||||
"fromDayOfWeek": 1,
|
||||
"line": true,
|
||||
"lineColor": "rgba(255, 0, 0, 0.32)",
|
||||
"op": "time",
|
||||
"to": "",
|
||||
"toDayOfWeek": 1
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 127, 0, 0.22)",
|
||||
"fromDayOfWeek": 2,
|
||||
"line": true,
|
||||
"lineColor": "rgba(255, 127, 0, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 2
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 255, 0, 0.22)",
|
||||
"fromDayOfWeek": 3,
|
||||
"line": true,
|
||||
"lineColor": "rgba(255, 255, 0, 0.22)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 3
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(0, 255, 0, 0.22)",
|
||||
"fromDayOfWeek": 4,
|
||||
"line": true,
|
||||
"lineColor": "rgba(0, 255, 0, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 4
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(0, 0, 255, 0.22)",
|
||||
"fromDayOfWeek": 5,
|
||||
"line": true,
|
||||
"lineColor": "rgba(0, 0, 255, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 5
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(75, 0, 130, 0.22)",
|
||||
"fromDayOfWeek": 6,
|
||||
"line": true,
|
||||
"lineColor": "rgba(75, 0, 130, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 6
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(148, 0, 211, 0.22)",
|
||||
"fromDayOfWeek": 7,
|
||||
"line": true,
|
||||
"lineColor": "rgba(148, 0, 211, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 7
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "Each day of week",
|
||||
"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": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"id": 5,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "red",
|
||||
"fill": false,
|
||||
"from": "05:00",
|
||||
"line": true,
|
||||
"op": "time"
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "05:00",
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30d",
|
||||
"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 - Graph (Time Regions)",
|
||||
"uid": "XMjIZPmik",
|
||||
"version": 1
|
||||
}
|
@ -1,250 +1,546 @@
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
60
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always OK",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 60
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always OK",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": "125",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"executionErrorState": "alerting",
|
||||
"for": "0m",
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
1
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"15m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"for": "5m",
|
||||
"frequency": "1m",
|
||||
"handler": 1,
|
||||
"name": "TestData - No data",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 5,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "no_data_points",
|
||||
"stringInput": "",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "No data",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"15m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"for": "1m",
|
||||
"frequency": "1m",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Alerting with For",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 7
|
||||
},
|
||||
"id": 6,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting with For",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"revision": 2,
|
||||
"title": "Alerting with TestData",
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": 255.625,
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
60
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always OK",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 60,
|
||||
"op": "gt",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"colorMode": "critical"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always OK",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": "125",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
}
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
@ -274,14 +570,8 @@
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 13,
|
||||
"version": 4,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
||||
"timezone": "browser",
|
||||
"title": "Alerting with TestData",
|
||||
"uid": "7MeksYbmk",
|
||||
"version": 1
|
||||
}
|
@ -9,7 +9,7 @@ services:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
|
||||
db:
|
||||
image: mysql
|
||||
image: mysql:5.6
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_DATABASE: grafana
|
||||
|
@ -39,6 +39,7 @@ local alertDashboardTemplate = {
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"for": "1m",
|
||||
"name": "bulk alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
|
@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of
|
||||
|
||||
## Rule Config
|
||||
|
||||
{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
|
||||
|
||||
|
||||
Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table**
|
||||
panels as well in a future release.
|
||||
@ -48,6 +48,16 @@ panels as well in a future release.
|
||||
|
||||
Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule.
|
||||
|
||||
### For
|
||||
|
||||
> This setting is available in Grafana 5.4 and above.
|
||||
|
||||
If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications.
|
||||
|
||||
Typically, it's always a good idea to use this setting since its often worse to get false positive than wait a few minutes before the alert notification triggers.
|
||||
|
||||
{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
|
||||
|
||||
### Conditions
|
||||
|
||||
Currently the only condition type that exists is a `Query` condition that allows you to
|
||||
@ -57,11 +67,11 @@ specify a query letter, time range and an aggregation function.
|
||||
### Query condition example
|
||||
|
||||
```sql
|
||||
avg() OF query(A, 5m, now) IS BELOW 14
|
||||
avg() OF query(A, 15m, now) IS BELOW 14
|
||||
```
|
||||
|
||||
- `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
|
||||
- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
|
||||
- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
|
||||
- `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold.
|
||||
|
||||
The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
|
||||
|
@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
disable_login_form ⁼ true
|
||||
disable_login_form = true
|
||||
```
|
||||
|
||||
### Automatic OAuth login
|
||||
|
||||
Set to true to attempt login with OAuth automatically, skipping the login screen.
|
||||
This setting is ignored if multiple OAuth providers are configured.
|
||||
Defaults to `false`.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
oauth_auto_login = true
|
||||
```
|
||||
|
||||
### Hide sign-out menu
|
||||
|
@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to
|
||||
Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when
|
||||
the graph crosses a particular threshold.
|
||||
|
||||
### Time Regions
|
||||
|
||||
> Only available in Grafana v5.4 and above.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}}
|
||||
|
||||
Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours.
|
||||
|
||||
## Time Range
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v51/graph-time-range.png" max-width= "900px" >}}
|
||||
|
@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then
|
||||
. "$DEFAULT"
|
||||
fi
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function checkUser() {
|
||||
if [ `id -u` -ne 0 ]; then
|
||||
|
@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
--packaging=deb \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||
|
@ -80,6 +80,7 @@ fi
|
||||
exec grafana-server \
|
||||
--homepath="$GF_PATHS_HOME" \
|
||||
--config="$GF_PATHS_CONFIG" \
|
||||
--packaging=docker \
|
||||
"$@" \
|
||||
cfg:default.log.mode="console" \
|
||||
cfg:default.paths.data="$GF_PATHS_DATA" \
|
||||
|
@ -60,7 +60,7 @@ fi
|
||||
# overwrite settings from default file
|
||||
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function isRunning() {
|
||||
status -p $PID_FILE $NAME > /dev/null 2>&1
|
||||
|
@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
--packaging=rpm \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||
|
@ -295,7 +295,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
|
||||
return Error(500, "", err)
|
||||
}
|
||||
|
||||
var response m.AlertStateType = m.AlertStatePending
|
||||
var response m.AlertStateType = m.AlertStateUnknown
|
||||
pausedState := "un-paused"
|
||||
if cmd.Paused {
|
||||
response = m.AlertStatePaused
|
||||
|
@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
|
||||
viewData.Settings["loginError"] = loginError
|
||||
}
|
||||
|
||||
if tryOAuthAutoLogin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
if !tryLoginUsingRememberCookie(c) {
|
||||
c.HTML(200, ViewIndex, viewData)
|
||||
return
|
||||
@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
|
||||
func tryOAuthAutoLogin(c *m.ReqContext) bool {
|
||||
if !setting.OAuthAutoLogin {
|
||||
return false
|
||||
}
|
||||
oauthInfos := setting.OAuthService.OAuthInfos
|
||||
if len(oauthInfos) != 1 {
|
||||
log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.")
|
||||
return false
|
||||
}
|
||||
for key := range setting.OAuthService.OAuthInfos {
|
||||
redirectUrl := setting.AppSubUrl + "/login/" + key
|
||||
log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
|
||||
c.Redirect(redirectUrl, 307)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
|
||||
// Check auto-login.
|
||||
uname := c.GetCookie(setting.CookieUserName)
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
extensions "github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
@ -39,6 +39,7 @@ var buildstamp string
|
||||
var configFile = flag.String("config", "", "path to config file")
|
||||
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
|
||||
var pidFile = flag.String("pidfile", "", "path to pid file")
|
||||
var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed")
|
||||
|
||||
func main() {
|
||||
v := flag.Bool("v", false, "prints current version and exits")
|
||||
@ -79,6 +80,7 @@ func main() {
|
||||
setting.BuildStamp = buildstampInt64
|
||||
setting.BuildBranch = buildBranch
|
||||
setting.IsEnterprise = extensions.IsEnterprise
|
||||
setting.Packaging = validPackaging(*packaging)
|
||||
|
||||
metrics.SetBuildInformation(version, commit, buildBranch)
|
||||
|
||||
@ -95,6 +97,16 @@ func main() {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func validPackaging(packaging string) string {
|
||||
validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"}
|
||||
for _, vt := range validTypes {
|
||||
if packaging == vt {
|
||||
return packaging
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
sighupChan := make(chan os.Signal, 1)
|
||||
|
@ -313,7 +313,7 @@ func init() {
|
||||
|
||||
// SetBuildInformation sets the build information for this binary
|
||||
func SetBuildInformation(version, revision, branch string) {
|
||||
// We export this info twice for backwards compability.
|
||||
// We export this info twice for backwards compatibility.
|
||||
// Once this have been released for some time we should be able to remote `M_Grafana_Version`
|
||||
// The reason we added a new one is that its common practice in the prometheus community
|
||||
// to name this metric `*_build_info` so its easy to do aggregation on all programs.
|
||||
@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) {
|
||||
|
||||
metrics := map[string]interface{}{}
|
||||
report := map[string]interface{}{
|
||||
"version": version,
|
||||
"metrics": metrics,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"edition": getEdition(),
|
||||
"version": version,
|
||||
"metrics": metrics,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"edition": getEdition(),
|
||||
"packaging": setting.Packaging,
|
||||
}
|
||||
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) {
|
||||
}
|
||||
metrics["stats.ds.other.count"] = dsOtherCount
|
||||
|
||||
metrics["stats.packaging."+setting.Packaging+".count"] = 1
|
||||
|
||||
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
|
||||
if err := bus.Dispatch(&dsAccessStats); err != nil {
|
||||
metricsLogger.Error("Failed to get datasource access stats", "error", err)
|
||||
|
@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) {
|
||||
setting.BasicAuthEnabled = true
|
||||
setting.LdapEnabled = true
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.Packaging = "deb"
|
||||
|
||||
wg.Add(1)
|
||||
sendUsageStats(oauthProviders)
|
||||
@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) {
|
||||
So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
|
||||
So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
|
||||
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
|
||||
|
||||
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -19,6 +19,7 @@ const (
|
||||
AlertStateAlerting AlertStateType = "alerting"
|
||||
AlertStateOK AlertStateType = "ok"
|
||||
AlertStatePending AlertStateType = "pending"
|
||||
AlertStateUnknown AlertStateType = "unknown"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -39,7 +40,12 @@ var (
|
||||
)
|
||||
|
||||
func (s AlertStateType) IsValid() bool {
|
||||
return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
|
||||
return s == AlertStateOK ||
|
||||
s == AlertStateNoData ||
|
||||
s == AlertStatePaused ||
|
||||
s == AlertStatePending ||
|
||||
s == AlertStateAlerting ||
|
||||
s == AlertStateUnknown
|
||||
}
|
||||
|
||||
func (s NoDataOption) IsValid() bool {
|
||||
@ -66,12 +72,13 @@ type Alert struct {
|
||||
PanelId int64
|
||||
Name string
|
||||
Message string
|
||||
Severity string
|
||||
Severity string //Unused
|
||||
State AlertStateType
|
||||
Handler int64
|
||||
Handler int64 //Unused
|
||||
Silenced bool
|
||||
ExecutionError string
|
||||
Frequency int64
|
||||
For time.Duration
|
||||
|
||||
EvalData *simplejson.Json
|
||||
NewStateDate time.Time
|
||||
|
@ -68,8 +68,13 @@ func (c *EvalContext) GetStateModel() *StateDescription {
|
||||
Color: "#D63232",
|
||||
Text: "Alerting",
|
||||
}
|
||||
case m.AlertStateUnknown:
|
||||
return &StateDescription{
|
||||
Color: "#888888",
|
||||
Text: "Unknown",
|
||||
}
|
||||
default:
|
||||
panic("Unknown rule state " + c.Rule.State)
|
||||
panic("Unknown rule state for alert " + c.Rule.State)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
|
||||
// GetNewState returns the new state from the alert rule evaluation
|
||||
func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
ns := getNewStateInternal(c)
|
||||
if ns != m.AlertStateAlerting || c.Rule.For == 0 {
|
||||
return ns
|
||||
}
|
||||
|
||||
since := time.Since(c.Rule.LastStateChange)
|
||||
if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For {
|
||||
return m.AlertStateAlerting
|
||||
}
|
||||
|
||||
if c.PrevAlertState == m.AlertStateAlerting {
|
||||
return m.AlertStateAlerting
|
||||
}
|
||||
|
||||
return m.AlertStatePending
|
||||
}
|
||||
|
||||
func getNewStateInternal(c *EvalContext) m.AlertStateType {
|
||||
if c.Error != nil {
|
||||
c.log.Error("Alert Rule Result Error",
|
||||
"ruleId", c.Rule.Id,
|
||||
@ -125,11 +149,13 @@ func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
return c.PrevAlertState
|
||||
}
|
||||
return c.Rule.ExecutionErrorState.ToAlertState()
|
||||
}
|
||||
|
||||
} else if c.Firing {
|
||||
if c.Firing {
|
||||
return m.AlertStateAlerting
|
||||
}
|
||||
|
||||
} else if c.NoDataFound {
|
||||
if c.NoDataFound {
|
||||
c.log.Info("Alert Rule returned no data",
|
||||
"ruleId", c.Rule.Id,
|
||||
"name", c.Rule.Name,
|
||||
|
@ -2,11 +2,11 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
|
||||
@ -31,71 +31,176 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertingEvalContext(t *testing.T) {
|
||||
Convey("Should compute and replace properly new rule state", t, func() {
|
||||
func TestGetStateFromEvalContext(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
expected models.AlertStateType
|
||||
applyFn func(ec *EvalContext)
|
||||
}{
|
||||
{
|
||||
name: "ok -> alerting",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.Firing = true
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> error(alerting)",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> pending. since its been firing for less than FOR",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
|
||||
ec.Rule.For = time.Minute * 5
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> alerting. since its been firing for more than FOR and prev state is pending",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "alerting -> alerting. should not update regardless of FOR",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateAlerting
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> ok. should not update regardless of FOR",
|
||||
expected: models.AlertStateOK,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> error(keep_last)",
|
||||
expected: models.AlertStateOK,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> error(keep_last)",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> no_data(alerting)",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ec.NoDataFound = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> no_data(keep_last)",
|
||||
expected: models.AlertStateOK,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Rule.NoDataState = models.NoDataKeepState
|
||||
ec.NoDataFound = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> no_data(keep_last)",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.NoDataState = models.NoDataKeepState
|
||||
ec.NoDataFound = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> no_data(alerting) with for duration have not passed",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ec.NoDataFound = true
|
||||
ec.Rule.For = time.Minute * 5
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> no_data(alerting) should set alerting since time passed FOR",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ec.NoDataFound = true
|
||||
ec.Rule.For = time.Minute * 2
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> error(alerting) with for duration have not passed ",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.For = time.Minute * 5
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> error(alerting) should set alerting since time passed FOR",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.For = time.Minute * 2
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
|
||||
dummieError := fmt.Errorf("dummie error")
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Firing = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
})
|
||||
tc.applyFn(ctx)
|
||||
have := ctx.GetNewState()
|
||||
if have != tc.expected {
|
||||
t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package alerting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -115,6 +115,15 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
return nil, ValidationError{Reason: "Could not parse frequency"}
|
||||
}
|
||||
|
||||
rawFor := jsonAlert.Get("for").MustString()
|
||||
var forValue time.Duration
|
||||
if rawFor != "" {
|
||||
forValue, err = time.ParseDuration(rawFor)
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Could not parse for"}
|
||||
}
|
||||
}
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgID,
|
||||
@ -124,6 +133,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||
Message: jsonAlert.Get("message").MustString(),
|
||||
Frequency: frequency,
|
||||
For: forValue,
|
||||
}
|
||||
|
||||
for _, condition := range jsonAlert.Get("conditions").MustArray() {
|
||||
|
@ -3,6 +3,7 @@ package alerting
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
|
||||
json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Extractor should not modify the original json", func() {
|
||||
@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(alerts[1].PanelId, ShouldEqual, 4)
|
||||
})
|
||||
|
||||
Convey("should extract for param", func() {
|
||||
So(alerts[0].For, ShouldEqual, time.Minute*2)
|
||||
So(alerts[1].For, ShouldEqual, time.Duration(0))
|
||||
})
|
||||
|
||||
Convey("should extract name and desc", func() {
|
||||
So(alerts[0].Name, ShouldEqual, "name1")
|
||||
So(alerts[0].Message, ShouldEqual, "desc1")
|
||||
@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Panels missing id should return error", func() {
|
||||
panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
|
||||
panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(panelWithoutId)
|
||||
@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Panel with id set to zero should return error", func() {
|
||||
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
|
||||
panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(panelWithIdZero)
|
||||
@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Parse alerts from dashboard without rows", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
||||
json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
|
||||
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should be able to extract collapsed panels", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
|
||||
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
|
||||
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
|
@ -1,13 +1,60 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestWhenAlertManagerShouldNotify(t *testing.T) {
|
||||
tcs := []struct {
|
||||
prevState m.AlertStateType
|
||||
newState m.AlertStateType
|
||||
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
prevState: m.AlertStatePending,
|
||||
newState: m.AlertStateOK,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
prevState: m.AlertStateAlerting,
|
||||
newState: m.AlertStateOK,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
prevState: m.AlertStateOK,
|
||||
newState: m.AlertStatePending,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStatePending,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
am := &AlertmanagerNotifier{log: log.New("test.logger")}
|
||||
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: tc.prevState,
|
||||
})
|
||||
|
||||
evalContext.Rule.State = tc.newState
|
||||
|
||||
res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{})
|
||||
if res != tc.expect {
|
||||
t.Errorf("got %v expected %v", res, tc.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertmanagerNotifier(t *testing.T) {
|
||||
Convey("Alertmanager notifier tests", t, func() {
|
||||
|
||||
|
@ -67,6 +67,16 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
|
||||
}
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify when we become OK from pending
|
||||
if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
|
||||
return false
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStatePending,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStatePending,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
prevState: m.AlertStateAlerting,
|
||||
frequency: time.Minute * 10,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
@ -132,6 +125,27 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
prevState: m.AlertStateOK,
|
||||
state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "unknown -> ok",
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStateOK,
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "unknown -> pending",
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStatePending,
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "unknown -> alerting",
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStateAlerting,
|
||||
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
@ -141,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
State: tc.prevState,
|
||||
})
|
||||
|
||||
if tc.state == nil {
|
||||
tc.state = &m.AlertNotificationState{}
|
||||
}
|
||||
|
||||
evalContext.Rule.State = tc.newState
|
||||
nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}
|
||||
|
||||
|
@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
// when two servers are raising. This makes sure that the server
|
||||
// with the last state change always sends a notification.
|
||||
evalContext.Rule.StateChanges = cmd.Result.StateChanges
|
||||
|
||||
// Update the last state change of the alert rule in memory
|
||||
evalContext.Rule.LastStateChange = time.Now()
|
||||
}
|
||||
|
||||
// save annotation
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
@ -18,6 +19,8 @@ type Rule struct {
|
||||
Frequency int64
|
||||
Name string
|
||||
Message string
|
||||
LastStateChange time.Time
|
||||
For time.Duration
|
||||
NoDataState m.NoDataOption
|
||||
ExecutionErrorState m.ExecutionErrorOption
|
||||
State m.AlertStateType
|
||||
@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
model.Message = ruleDef.Message
|
||||
model.Frequency = ruleDef.Frequency
|
||||
model.State = ruleDef.State
|
||||
model.LastStateChange = ruleDef.NewStateDate
|
||||
model.For = ruleDef.For
|
||||
model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
|
||||
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
|
||||
model.StateChanges = ruleDef.StateChanges
|
||||
|
@ -23,6 +23,7 @@
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"for": "2m",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
@ -193,7 +193,8 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
|
||||
if alertToUpdate.ContainsUpdates(alert) {
|
||||
alert.Updated = timeNow()
|
||||
alert.State = alertToUpdate.State
|
||||
sess.MustCols("message")
|
||||
sess.MustCols("message", "for")
|
||||
|
||||
_, err := sess.ID(alert.Id).Update(alert)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -204,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
|
||||
} else {
|
||||
alert.Updated = timeNow()
|
||||
alert.Created = timeNow()
|
||||
alert.State = m.AlertStatePending
|
||||
alert.State = m.AlertStateUnknown
|
||||
alert.NewStateDate = timeNow()
|
||||
|
||||
_, err := sess.Insert(alert)
|
||||
@ -299,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
|
||||
params = append(params, string(m.AlertStatePaused))
|
||||
params = append(params, timeNow())
|
||||
} else {
|
||||
params = append(params, string(m.AlertStatePending))
|
||||
params = append(params, string(m.AlertStateUnknown))
|
||||
params = append(params, timeNow())
|
||||
}
|
||||
|
||||
@ -323,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
|
||||
if cmd.Paused {
|
||||
newState = string(m.AlertStatePaused)
|
||||
} else {
|
||||
newState = string(m.AlertStatePending)
|
||||
newState = string(m.AlertStateUnknown)
|
||||
}
|
||||
|
||||
res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())
|
||||
|
@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
So(alert.DashboardId, ShouldEqual, testDash.Id)
|
||||
So(alert.PanelId, ShouldEqual, 1)
|
||||
So(alert.Name, ShouldEqual, "Alerting title")
|
||||
So(alert.State, ShouldEqual, "pending")
|
||||
So(alert.State, ShouldEqual, m.AlertStateUnknown)
|
||||
So(alert.NewStateDate, ShouldNotBeNil)
|
||||
So(alert.EvalData, ShouldNotBeNil)
|
||||
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
|
||||
@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
So(query.Result[0].Name, ShouldEqual, "Name")
|
||||
|
||||
Convey("Alert state should not be updated", func() {
|
||||
So(query.Result[0].State, ShouldEqual, "pending")
|
||||
So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) {
|
||||
mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state))
|
||||
mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id",
|
||||
NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0]))
|
||||
|
||||
mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
|
||||
Name: "for", Type: DB_BigInt, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
@ -57,6 +57,9 @@ var (
|
||||
IsEnterprise bool
|
||||
ApplicationName string
|
||||
|
||||
// packaging
|
||||
Packaging = "unknown"
|
||||
|
||||
// Paths
|
||||
HomePath string
|
||||
PluginsPath string
|
||||
@ -112,6 +115,7 @@ var (
|
||||
ExternalUserMngLinkUrl string
|
||||
ExternalUserMngLinkName string
|
||||
ExternalUserMngInfo string
|
||||
OAuthAutoLogin bool
|
||||
ViewersCanEdit bool
|
||||
|
||||
// Http auth
|
||||
@ -626,6 +630,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
auth := iniFile.Section("auth")
|
||||
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
|
||||
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
|
||||
OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
|
||||
SignoutRedirectUrl = auth.Key("signout_redirect_url").String()
|
||||
|
||||
// anonymous access
|
||||
|
@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
|
||||
|
||||
func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
@ -47,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
|
||||
}
|
||||
|
||||
return &BasicUserInfo{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
Login: data.Email,
|
||||
|
@ -46,6 +46,7 @@ func init() {
|
||||
"AWS/Billing": {"EstimatedCharges"},
|
||||
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
||||
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
||||
"AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
|
||||
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
|
||||
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
|
||||
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
|
||||
@ -121,6 +122,7 @@ func init() {
|
||||
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||
"AWS/CloudSearch": {},
|
||||
"AWS/CloudHSM": {"Region", "ClusterId", "HsmId"},
|
||||
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
|
||||
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
||||
"AWS/DX": {"ConnectionId"},
|
||||
|
@ -36,17 +36,13 @@ export class Switch extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gf-form">
|
||||
{label && (
|
||||
<label htmlFor={labelId} className={labelClassName}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<label htmlFor={labelId} className="gf-form-switch-container">
|
||||
{label && <label className={labelClassName}>{label}</label>}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<label htmlFor={labelId} />
|
||||
<span className="gf-form-switch__slider" />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -64,10 +64,10 @@
|
||||
|
||||
<div class="search-results" ng-show="ctrl.sections.length > 0">
|
||||
<div class="search-results-filter-row">
|
||||
<gf-form-switch
|
||||
<gf-form-checkbox
|
||||
on-change="ctrl.onSelectAllChanged()"
|
||||
checked="ctrl.selectAllChecked"
|
||||
switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox"
|
||||
switch-class="gf-form-switch--transparent"
|
||||
/>
|
||||
<div class="search-results-filter-row__filters">
|
||||
<div class="gf-form-select-wrapper" ng-show="!(ctrl.canMove || ctrl.canDelete)">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<div ng-repeat="section in ctrl.results" class="search-section">
|
||||
<div class="search-section__header pointer" ng-hide="section.hideHeader" ng-class="{'selected': section.selected}" ng-click="ctrl.toggleFolderExpand(section)">
|
||||
<div ng-click="ctrl.toggleSelection(section, $event)">
|
||||
<gf-form-switch
|
||||
<gf-form-checkbox
|
||||
ng-show="ctrl.editable"
|
||||
on-change="ctrl.selectionChanged($event)"
|
||||
checked="section.checked"
|
||||
switch-class="gf-form-switch--transparent gf-form-switch--search-result__section">
|
||||
</gf-form-switch>
|
||||
</gf-form-checkbox>
|
||||
</div>
|
||||
<i class="search-section__header__icon" ng-class="section.icon"></i>
|
||||
<span class="search-section__header__text">{{::section.title}}</span>
|
||||
@ -22,12 +22,12 @@
|
||||
<div ng-if="section.expanded">
|
||||
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
|
||||
<div ng-click="ctrl.toggleSelection(item, $event)">
|
||||
<gf-form-switch
|
||||
<gf-form-checkbox
|
||||
ng-show="ctrl.editable"
|
||||
on-change="ctrl.selectionChanged()"
|
||||
checked="item.checked"
|
||||
switch-class="gf-form-switch--transparent gf-form-switch--search-result__item">
|
||||
</gf-form-switch>
|
||||
</gf-form-checkbox>
|
||||
</div>
|
||||
<span class="search-item__icon">
|
||||
<i class="gicon mini gicon-dashboard-list"></i>
|
||||
|
@ -1,16 +1,33 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer" ng-show="ctrl.label">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-switch-container">
|
||||
<div class="gf-form-label {{ctrl.labelClass}}" ng-show="ctrl.label">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show">
|
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
|
||||
<span class="gf-form-switch__slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
|
||||
const checkboxTemplate = `
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-switch-container">
|
||||
<div class="gf-form-label {{ctrl.labelClass}}" ng-show="ctrl.label">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form-checkbox {{ctrl.switchClass}}" ng-if="ctrl.show">
|
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
|
||||
<span class="gf-form-switch__checkbox"></span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show">
|
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
|
||||
<label for="check-{{ctrl.id}}" data-on="Yes" data-off="No"></label>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export class SwitchCtrl {
|
||||
@ -51,4 +68,23 @@ export function switchDirective() {
|
||||
};
|
||||
}
|
||||
|
||||
export function checkboxDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: SwitchCtrl,
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
checked: '=',
|
||||
label: '@',
|
||||
labelClass: '@',
|
||||
tooltip: '@',
|
||||
switchClass: '@',
|
||||
onChange: '&',
|
||||
},
|
||||
template: checkboxTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('gfFormSwitch', switchDirective);
|
||||
coreModule.directive('gfFormCheckbox', checkboxDirective);
|
||||
|
@ -31,6 +31,7 @@ export interface LogSearchMatch {
|
||||
}
|
||||
|
||||
export interface LogRow {
|
||||
duplicates?: number;
|
||||
entry: string;
|
||||
key: string; // timestamp + labels
|
||||
labels: string;
|
||||
@ -71,6 +72,53 @@ export interface LogsStreamLabels {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export enum LogsDedupStrategy {
|
||||
none = 'none',
|
||||
exact = 'exact',
|
||||
numbers = 'numbers',
|
||||
signature = 'signature',
|
||||
}
|
||||
|
||||
const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g;
|
||||
function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean {
|
||||
switch (strategy) {
|
||||
case LogsDedupStrategy.exact:
|
||||
// Exact still strips dates
|
||||
return row.entry.replace(isoDateRegexp, '') === other.entry.replace(isoDateRegexp, '');
|
||||
|
||||
case LogsDedupStrategy.numbers:
|
||||
return row.entry.replace(/\d/g, '') === other.entry.replace(/\d/g, '');
|
||||
|
||||
case LogsDedupStrategy.signature:
|
||||
return row.entry.replace(/\w/g, '') === other.entry.replace(/\w/g, '');
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): LogsModel {
|
||||
if (strategy === LogsDedupStrategy.none) {
|
||||
return logs;
|
||||
}
|
||||
|
||||
const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
|
||||
const previous = result[result.length - 1];
|
||||
if (index > 0 && isDuplicateRow(row, previous, strategy)) {
|
||||
previous.duplicates++;
|
||||
} else {
|
||||
row.duplicates = 0;
|
||||
result.push(row);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...logs,
|
||||
rows: dedupedRows,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
|
||||
// Graph time series by log level
|
||||
const seriesByLevel = {};
|
||||
|
@ -26,7 +26,7 @@ export class Analytics {
|
||||
|
||||
init() {
|
||||
this.$rootScope.$on('$viewContentLoaded', () => {
|
||||
const track = { page: this.$location.url() };
|
||||
const track = { location: this.$location.url() };
|
||||
const ga = (window as any).ga || this.gaInit();
|
||||
ga('set', track);
|
||||
ga('send', 'pageview');
|
||||
|
@ -32,8 +32,8 @@ export class KeybindingSrv {
|
||||
|
||||
this.setupGlobal();
|
||||
appEvents.on('show-modal', () => (this.modalOpen = true));
|
||||
$rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
|
||||
$rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
|
||||
appEvents.on('timepickerOpen', () => (this.timepickerOpen = true));
|
||||
appEvents.on('timepickerClosed', () => (this.timepickerOpen = false));
|
||||
}
|
||||
|
||||
setupGlobal() {
|
||||
|
108
public/app/core/specs/logs_model.test.ts
Normal file
108
public/app/core/specs/logs_model.test.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
|
||||
|
||||
describe('dedupLogRows()', () => {
|
||||
test('should return rows as is when dedup is set to none', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows);
|
||||
});
|
||||
|
||||
test('should dedup on exact matches', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
|
||||
{
|
||||
duplicates: 1,
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should dedup on number matches', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([
|
||||
{
|
||||
duplicates: 1,
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should dedup on signature matches', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([
|
||||
{
|
||||
duplicates: 3,
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -7,6 +7,7 @@ export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
|
||||
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
|
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
|
||||
export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
const colors = [
|
||||
|
@ -29,6 +29,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
{ text: 'Alerting', value: 'alerting' },
|
||||
{ text: 'No Data', value: 'no_data' },
|
||||
{ text: 'Paused', value: 'paused' },
|
||||
{ text: 'Pending', value: 'pending' },
|
||||
];
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -169,6 +169,7 @@ export class AlertTabCtrl {
|
||||
alert.frequency = alert.frequency || '1m';
|
||||
alert.handler = alert.handler || 1;
|
||||
alert.notifications = alert.notifications || [];
|
||||
alert.for = alert.for || '0m';
|
||||
|
||||
const defaultName = this.panel.title + ' alert';
|
||||
alert.name = alert.name || defaultName;
|
||||
@ -217,7 +218,7 @@ export class AlertTabCtrl {
|
||||
buildDefaultCondition() {
|
||||
return {
|
||||
type: 'query',
|
||||
query: { params: ['A', '15m', 'now'] },
|
||||
query: { params: ['A', '5m', 'now'] },
|
||||
reducer: { type: 'avg', params: [] },
|
||||
evaluator: { type: 'gt', params: [null] },
|
||||
operator: { type: 'and' },
|
||||
@ -354,6 +355,7 @@ export class AlertTabCtrl {
|
||||
enable() {
|
||||
this.panel.alert = {};
|
||||
this.initModel();
|
||||
this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
|
||||
}
|
||||
|
||||
evaluatorParamsChanged() {
|
||||
|
@ -81,6 +81,12 @@ exports[`Render should render alert rules 1`] = `
|
||||
>
|
||||
Paused
|
||||
</option>
|
||||
<option
|
||||
key="pending"
|
||||
value="pending"
|
||||
>
|
||||
Pending
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,6 +236,12 @@ exports[`Render should render component 1`] = `
|
||||
>
|
||||
Paused
|
||||
</option>
|
||||
<option
|
||||
key="pending"
|
||||
value="pending"
|
||||
>
|
||||
Pending
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,147 +1,159 @@
|
||||
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
<span class="gf-form-label">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.frequency"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending.
|
||||
Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<ol class="alert-rule-list" >
|
||||
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
|
||||
|
@ -99,6 +99,13 @@ function getStateDisplayModel(state) {
|
||||
stateClass: 'alert-state-warning',
|
||||
};
|
||||
}
|
||||
case 'unknown': {
|
||||
return {
|
||||
text: 'UNKNOWN',
|
||||
iconClass: 'fa fa-question',
|
||||
stateClass: 'alert-state-paused',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw { message: 'Unknown alert state' };
|
||||
|
@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
|
||||
if (event.alertId) {
|
||||
const stateModel = alertDef.getStateDisplayModel(event.newState);
|
||||
titleStateClass = stateModel.stateClass;
|
||||
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||
title = `<i class="${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||
text = alertDef.getAlertAnnotationInfo(event);
|
||||
if (event.text) {
|
||||
text = text + '<br />' + event.text;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
OK_COLOR,
|
||||
ALERTING_COLOR,
|
||||
NO_DATA_COLOR,
|
||||
PENDING_COLOR,
|
||||
DEFAULT_ANNOTATION_COLOR,
|
||||
REGION_FILL_ALPHA,
|
||||
} from 'app/core/utils/colors';
|
||||
@ -71,6 +72,11 @@ export class EventManager {
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
$__pending: {
|
||||
color: PENDING_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
$__editing: {
|
||||
color: DEFAULT_ANNOTATION_COLOR,
|
||||
position: 'BOTTOM',
|
||||
|
@ -77,6 +77,10 @@ export class DashboardSrv {
|
||||
postSave(clone, data) {
|
||||
this.dash.version = data.version;
|
||||
|
||||
// important that these happens before location redirect below
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
||||
|
||||
const newUrl = locationUtil.stripBaseFromUrl(data.url);
|
||||
const currentPath = this.$location.path();
|
||||
|
||||
@ -84,9 +88,6 @@ export class DashboardSrv {
|
||||
this.$location.url(newUrl).replace();
|
||||
}
|
||||
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
||||
|
||||
return this.dash;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ export class PanelHeaderCorner extends PureComponent<Props> {
|
||||
<div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
|
||||
{panel.links &&
|
||||
panel.links.length > 0 && (
|
||||
<ul>
|
||||
<ul className="text-left">
|
||||
{panel.links.map((link, idx) => {
|
||||
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
|
||||
return (
|
||||
@ -74,7 +74,7 @@ export class PanelHeaderCorner extends PureComponent<Props> {
|
||||
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
|
||||
<Tooltip
|
||||
content={this.getInfoContent}
|
||||
className="absolute"
|
||||
className="popper__manager--block"
|
||||
refClassName={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
|
||||
>
|
||||
<i className="fa" />
|
||||
|
@ -30,8 +30,8 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="revision in ctrl.revisions">
|
||||
<td class="filter-table__switch-cell" bs-tooltip="!revision.checked && ctrl.canCompare ? 'You can only compare 2 versions at a time' : ''" data-placement="right">
|
||||
<gf-form-switch switch-class="gf-form-switch--table-cell" checked="revision.checked" on-change="ctrl.revisionSelectionChanged()" ng-disabled="!revision.checked && ctrl.canCompare">
|
||||
</gf-form-switch>
|
||||
<gf-form-checkbox switch-class="gf-form-switch--table-cell" checked="revision.checked" on-change="ctrl.revisionSelectionChanged()" ng-disabled="!revision.checked && ctrl.canCompare">
|
||||
</gf-form-checkbox>
|
||||
</td>
|
||||
<td class="text-center">{{revision.version}}</td>
|
||||
<td>{{revision.createdDateString}}</td>
|
||||
|
@ -142,7 +142,7 @@ export class PanelModel {
|
||||
setViewMode(fullscreen: boolean, isEditing: boolean) {
|
||||
this.fullscreen = fullscreen;
|
||||
this.isEditing = isEditing;
|
||||
this.events.emit('panel-size-changed');
|
||||
this.events.emit('view-mode-changed');
|
||||
}
|
||||
|
||||
updateGridPos(newPos: GridPos) {
|
||||
|
@ -126,8 +126,7 @@ export class DashboardViewState {
|
||||
|
||||
if (!panel.fullscreen) {
|
||||
this.enterFullscreen(panel);
|
||||
} else {
|
||||
// already in fullscreen view just update the view mode
|
||||
} else if (this.dashboard.meta.isEditing !== this.state.edit) {
|
||||
this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
|
||||
}
|
||||
} else if (this.fullscreenPanel) {
|
||||
|
@ -3,8 +3,9 @@ import { hot } from 'react-hot-loader';
|
||||
import Select from 'react-select';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { DataSource } from 'app/types/datasources';
|
||||
import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore';
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { RawTimeRange, DataQuery } from 'app/types/series';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import store from 'app/core/store';
|
||||
@ -16,7 +17,9 @@ import PickerOption from 'app/core/components/Picker/PickerOption';
|
||||
import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
|
||||
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import Panel from './Panel';
|
||||
import QueryRows from './QueryRows';
|
||||
import Graph from './Graph';
|
||||
import Logs from './Logs';
|
||||
@ -24,7 +27,6 @@ import Table from './Table';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import TimePicker from './TimePicker';
|
||||
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
import { DataSource } from 'app/types/datasources';
|
||||
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
@ -77,7 +79,7 @@ function updateHistory(history: HistoryItem[], datasourceId: string, queries: st
|
||||
}
|
||||
|
||||
interface ExploreProps {
|
||||
datasourceSrv: any;
|
||||
datasourceSrv: DatasourceSrv;
|
||||
onChangeSplit: (split: boolean, state?: ExploreState) => void;
|
||||
onSaveState: (key: string, state: ExploreState) => void;
|
||||
position: string;
|
||||
@ -92,6 +94,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
/**
|
||||
* Current query expressions of the rows including their modifications, used for running queries.
|
||||
* Not kept in component state to prevent edit-render roundtrips.
|
||||
* TODO: make this generic (other datasources might not have string representations of current query state)
|
||||
*/
|
||||
queryExpressions: string[];
|
||||
/**
|
||||
@ -125,6 +128,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
range: initialRange,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingStartPage: false,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
@ -164,7 +168,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
}
|
||||
}
|
||||
|
||||
async setDatasource(datasource: DataSource) {
|
||||
async setDatasource(datasource: any, origin?: DataSource) {
|
||||
const supportsGraph = datasource.meta.metrics;
|
||||
const supportsLogs = datasource.meta.logs;
|
||||
const supportsTable = datasource.meta.metrics;
|
||||
@ -193,12 +197,33 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
datasource.init();
|
||||
}
|
||||
|
||||
// Keep queries but reset edit state
|
||||
// Check if queries can be imported from previously selected datasource
|
||||
let queryExpressions = this.queryExpressions;
|
||||
if (origin) {
|
||||
if (origin.meta.id === datasource.meta.id) {
|
||||
// Keep same queries if same type of datasource
|
||||
queryExpressions = [...this.queryExpressions];
|
||||
} else if (datasource.importQueries) {
|
||||
// Datasource-specific importers, wrapping to satisfy interface
|
||||
const wrappedQueries: DataQuery[] = this.queryExpressions.map((query, index) => ({
|
||||
refId: String(index),
|
||||
expr: query,
|
||||
}));
|
||||
const modifiedQueries: DataQuery[] = await datasource.importQueries(wrappedQueries, origin.meta);
|
||||
queryExpressions = modifiedQueries.map(({ expr }) => expr);
|
||||
} else {
|
||||
// Default is blank queries
|
||||
queryExpressions = this.queryExpressions.map(() => '');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset edit state with new queries
|
||||
const nextQueries = this.state.queries.map((q, i) => ({
|
||||
...q,
|
||||
key: generateQueryKey(i),
|
||||
query: this.queryExpressions[i],
|
||||
query: queryExpressions[i],
|
||||
}));
|
||||
this.queryExpressions = queryExpressions;
|
||||
|
||||
// Custom components
|
||||
const StartPage = datasource.pluginExports.ExploreStartPage;
|
||||
@ -215,6 +240,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
datasourceLoading: false,
|
||||
datasourceName: datasource.name,
|
||||
queries: nextQueries,
|
||||
showingStartPage: Boolean(StartPage),
|
||||
},
|
||||
() => {
|
||||
if (datasourceError === null) {
|
||||
@ -258,6 +284,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
};
|
||||
|
||||
onChangeDatasource = async option => {
|
||||
const origin = this.state.datasource;
|
||||
this.setState({
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
@ -266,7 +293,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
});
|
||||
const datasourceName = option.value;
|
||||
const datasource = await this.props.datasourceSrv.get(datasourceName);
|
||||
this.setDatasource(datasource);
|
||||
this.setDatasource(datasource as any, origin);
|
||||
};
|
||||
|
||||
onChangeQuery = (value: string, index: number, override?: boolean) => {
|
||||
@ -305,10 +332,11 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onClickClear = () => {
|
||||
this.queryExpressions = [''];
|
||||
this.setState(
|
||||
{
|
||||
prevState => ({
|
||||
queries: ensureQueries(),
|
||||
queryTransactions: [],
|
||||
},
|
||||
showingStartPage: Boolean(prevState.StartPage),
|
||||
}),
|
||||
this.saveState
|
||||
);
|
||||
};
|
||||
@ -539,6 +567,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
|
||||
return {
|
||||
queryTransactions: nextQueryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
});
|
||||
|
||||
@ -765,16 +794,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
range,
|
||||
showingGraph,
|
||||
showingLogs,
|
||||
showingStartPage,
|
||||
showingTable,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
} = 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 graphHeight = showingGraph && showingTable ? '200px' : '400px';
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined;
|
||||
const graphRangeIntervals = getIntervals(graphRange, datasource, this.el ? this.el.offsetWidth : 0);
|
||||
@ -799,8 +825,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
)
|
||||
: undefined;
|
||||
const loading = queryTransactions.some(qt => !qt.done);
|
||||
const showStartPages = StartPage && queryTransactions.length === 0;
|
||||
const viewModeCount = [supportsGraph, supportsLogs, supportsTable].filter(m => m).length;
|
||||
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
@ -889,55 +913,47 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
/>
|
||||
<main className="m-t-2">
|
||||
<ErrorBoundary>
|
||||
{showStartPages && <StartPage onClickQuery={this.onClickQuery} />}
|
||||
{!showStartPages && (
|
||||
{showingStartPage && <StartPage onClickQuery={this.onClickQuery} />}
|
||||
{!showingStartPage && (
|
||||
<>
|
||||
{viewModeCount > 1 && (
|
||||
<div className="result-options">
|
||||
{supportsGraph ? (
|
||||
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{supportsTable ? (
|
||||
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
) : null}
|
||||
{supportsLogs ? (
|
||||
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||
Logs
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supportsGraph &&
|
||||
showingGraph && (
|
||||
{supportsGraph && (
|
||||
<Panel
|
||||
label="Graph"
|
||||
isOpen={showingGraph}
|
||||
loading={graphLoading}
|
||||
onToggle={this.onClickGraphButton}
|
||||
>
|
||||
<Graph
|
||||
data={graphResult}
|
||||
height={graphHeight}
|
||||
loading={graphLoading}
|
||||
id={`explore-graph-${position}`}
|
||||
onChangeTime={this.onChangeTime}
|
||||
range={graphRange}
|
||||
split={split}
|
||||
/>
|
||||
)}
|
||||
{supportsTable && showingTable ? (
|
||||
<div className="panel-container m-t-2">
|
||||
</Panel>
|
||||
)}
|
||||
{supportsTable && (
|
||||
<Panel
|
||||
label="Table"
|
||||
loading={tableLoading}
|
||||
isOpen={showingTable}
|
||||
onToggle={this.onClickTableButton}
|
||||
>
|
||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
||||
</div>
|
||||
) : null}
|
||||
{supportsLogs && showingLogs ? (
|
||||
<Logs
|
||||
data={logsResult}
|
||||
loading={logsLoading}
|
||||
position={position}
|
||||
onChangeTime={this.onChangeTime}
|
||||
range={range}
|
||||
/>
|
||||
) : null}
|
||||
</Panel>
|
||||
)}
|
||||
{supportsLogs && (
|
||||
<Panel label="Logs" loading={logsLoading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
|
||||
<Logs
|
||||
data={logsResult}
|
||||
loading={logsLoading}
|
||||
position={position}
|
||||
onChangeTime={this.onChangeTime}
|
||||
range={range}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
@ -77,7 +77,6 @@ interface GraphProps {
|
||||
data: any[];
|
||||
height?: string; // e.g., '200px'
|
||||
id?: string;
|
||||
loading?: boolean;
|
||||
range: RawTimeRange;
|
||||
split?: boolean;
|
||||
size?: { width: number; height: number };
|
||||
@ -188,12 +187,11 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { height = '100px', id = 'graph', loading = false } = this.props;
|
||||
const { height = '100px', id = 'graph' } = this.props;
|
||||
const data = this.getGraphData();
|
||||
|
||||
return (
|
||||
<div className="panel-container">
|
||||
{loading && <div className="explore-panel__loader" />}
|
||||
<>
|
||||
{this.props.data &&
|
||||
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
||||
!this.state.showAllTimeSeries && (
|
||||
@ -207,7 +205,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
)}
|
||||
<div id={id} className="explore-graph" style={{ height }} />
|
||||
<Legend data={data} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React, { Fragment, PureComponent } from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { LogsModel } from 'app/core/logs_model';
|
||||
import { LogsDedupStrategy, LogsModel, dedupLogRows } from 'app/core/logs_model';
|
||||
import { findHighlightChunksInText } from 'app/core/utils/text';
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
|
||||
@ -32,6 +32,7 @@ interface LogsProps {
|
||||
}
|
||||
|
||||
interface LogsState {
|
||||
dedup: LogsDedupStrategy;
|
||||
showLabels: boolean;
|
||||
showLocalTime: boolean;
|
||||
showUtc: boolean;
|
||||
@ -39,11 +40,21 @@ interface LogsState {
|
||||
|
||||
export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
state = {
|
||||
dedup: LogsDedupStrategy.none,
|
||||
showLabels: true,
|
||||
showLocalTime: true,
|
||||
showUtc: false,
|
||||
};
|
||||
|
||||
onChangeDedup = (dedup: LogsDedupStrategy) => {
|
||||
this.setState(prevState => {
|
||||
if (prevState.dedup === dedup) {
|
||||
return { dedup: LogsDedupStrategy.none };
|
||||
}
|
||||
return { dedup };
|
||||
});
|
||||
};
|
||||
|
||||
onChangeLabels = (event: React.SyntheticEvent) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this.setState({
|
||||
@ -67,9 +78,18 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
|
||||
render() {
|
||||
const { className = '', data, loading = false, position, range } = this.props;
|
||||
const { showLabels, showLocalTime, showUtc } = this.state;
|
||||
const { dedup, showLabels, showLocalTime, showUtc } = this.state;
|
||||
const hasData = data && data.rows && data.rows.length > 0;
|
||||
const cssColumnSizes = ['4px'];
|
||||
const dedupedData = dedupLogRows(data, dedup);
|
||||
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
|
||||
const meta = [...data.meta];
|
||||
if (dedup !== LogsDedupStrategy.none) {
|
||||
meta.push({
|
||||
label: 'Dedup count',
|
||||
value: String(dedupCount),
|
||||
});
|
||||
}
|
||||
const cssColumnSizes = ['3px']; // Log-level indicator line
|
||||
if (showUtc) {
|
||||
cssColumnSizes.push('minmax(100px, max-content)');
|
||||
}
|
||||
@ -97,15 +117,39 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="panel-container logs-options">
|
||||
<div className="logs-options">
|
||||
<div className="logs-controls">
|
||||
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} small />
|
||||
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} small />
|
||||
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} small />
|
||||
<Switch
|
||||
label="Dedup: off"
|
||||
checked={dedup === LogsDedupStrategy.none}
|
||||
onChange={() => this.onChangeDedup(LogsDedupStrategy.none)}
|
||||
small
|
||||
/>
|
||||
<Switch
|
||||
label="Dedup: exact"
|
||||
checked={dedup === LogsDedupStrategy.exact}
|
||||
onChange={() => this.onChangeDedup(LogsDedupStrategy.exact)}
|
||||
small
|
||||
/>
|
||||
<Switch
|
||||
label="Dedup: numbers"
|
||||
checked={dedup === LogsDedupStrategy.numbers}
|
||||
onChange={() => this.onChangeDedup(LogsDedupStrategy.numbers)}
|
||||
small
|
||||
/>
|
||||
<Switch
|
||||
label="Dedup: signature"
|
||||
checked={dedup === LogsDedupStrategy.signature}
|
||||
onChange={() => this.onChangeDedup(LogsDedupStrategy.signature)}
|
||||
small
|
||||
/>
|
||||
{hasData &&
|
||||
data.meta && (
|
||||
meta && (
|
||||
<div className="logs-meta">
|
||||
{data.meta.map(item => (
|
||||
{meta.map(item => (
|
||||
<div className="logs-meta-item" key={item.label}>
|
||||
<span className="logs-meta-item__label">{item.label}:</span>
|
||||
<span className="logs-meta-item__value">{item.value}</span>
|
||||
@ -116,33 +160,38 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-container">
|
||||
{loading && <div className="explore-panel__loader" />}
|
||||
<div className="logs-entries" style={logEntriesStyle}>
|
||||
{hasData &&
|
||||
data.rows.map(row => (
|
||||
<Fragment key={row.key}>
|
||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
|
||||
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
||||
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
||||
{showLabels && (
|
||||
<div className="max-width" title={row.labels}>
|
||||
{row.labels}
|
||||
<div className="logs-entries" style={logEntriesStyle}>
|
||||
{hasData &&
|
||||
dedupedData.rows.map(row => (
|
||||
<Fragment key={row.key}>
|
||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
|
||||
{row.duplicates > 0 && (
|
||||
<div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
|
||||
{Array.apply(null, { length: row.duplicates }).map(index => (
|
||||
<div className="logs-row-level__duplicate" key={`${index}`} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Highlighter
|
||||
textToHighlight={row.entry}
|
||||
searchWords={row.searchWords}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName="logs-row-match-highlight"
|
||||
/>
|
||||
</div>
|
||||
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
||||
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
||||
{showLabels && (
|
||||
<div className="max-width" title={row.labels}>
|
||||
{row.labels}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{!loading && !hasData && 'No data was returned.'}
|
||||
)}
|
||||
<div>
|
||||
<Highlighter
|
||||
textToHighlight={row.entry}
|
||||
searchWords={row.searchWords}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName="logs-row-match-highlight"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{!loading && !hasData && 'No data was returned.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
34
public/app/features/explore/Panel.tsx
Normal file
34
public/app/features/explore/Panel.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
label: string;
|
||||
loading?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export default class Panel extends PureComponent<Props> {
|
||||
onClickToggle = () => this.props.onToggle(!this.props.isOpen);
|
||||
|
||||
render() {
|
||||
const { isOpen, loading } = this.props;
|
||||
const iconClass = isOpen ? 'fa fa-caret-up' : 'fa fa-caret-down';
|
||||
const loaderClass = loading ? 'explore-panel__loader explore-panel__loader--active' : 'explore-panel__loader';
|
||||
return (
|
||||
<div className="explore-panel panel-container">
|
||||
<div className="explore-panel__header" onClick={this.onClickToggle}>
|
||||
<div className="explore-panel__header-buttons">
|
||||
<span className={iconClass} />
|
||||
</div>
|
||||
<div className="explore-panel__header-label">{this.props.label}</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="explore-panel__body">
|
||||
<div className={loaderClass} />
|
||||
{this.props.children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="panel-container"
|
||||
>
|
||||
<Fragment>
|
||||
<div
|
||||
className="explore-graph"
|
||||
id="graph"
|
||||
@ -456,13 +454,11 @@ exports[`Render should render component 1`] = `
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Render should render component with disclaimer 1`] = `
|
||||
<div
|
||||
className="panel-container"
|
||||
>
|
||||
<Fragment>
|
||||
<div
|
||||
className="time-series-disclaimer"
|
||||
>
|
||||
@ -952,13 +948,11 @@ exports[`Render should render component with disclaimer 1`] = `
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Render should show query return no time series 1`] = `
|
||||
<div
|
||||
className="panel-container"
|
||||
>
|
||||
<Fragment>
|
||||
<div
|
||||
className="explore-graph"
|
||||
id="graph"
|
||||
@ -971,5 +965,5 @@ exports[`Render should show query return no time series 1`] = `
|
||||
<Legend
|
||||
data={Array []}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
@ -82,7 +82,6 @@ export class MetricsTabCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
this.datasourceInstance = option.datasource;
|
||||
this.setDatasource(option.datasource);
|
||||
this.updateDatasourceOptions();
|
||||
}
|
||||
@ -102,6 +101,7 @@ export class MetricsTabCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
this.datasourceInstance = datasource;
|
||||
this.panel.datasource = datasource.value;
|
||||
this.panel.refresh();
|
||||
}
|
||||
|
@ -48,6 +48,16 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
let lastAlertState;
|
||||
let hasAlertRule;
|
||||
|
||||
function mouseEnter() {
|
||||
panelContainer.toggleClass('panel-hover-highlight', true);
|
||||
ctrl.dashboard.setPanelFocus(ctrl.panel.id);
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
panelContainer.toggleClass('panel-hover-highlight', false);
|
||||
ctrl.dashboard.setPanelFocus(0);
|
||||
}
|
||||
|
||||
function resizeScrollableContent() {
|
||||
if (panelScrollbar) {
|
||||
panelScrollbar.update();
|
||||
@ -98,6 +108,19 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
});
|
||||
});
|
||||
|
||||
ctrl.events.on('view-mode-changed', () => {
|
||||
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
|
||||
setTimeout(() => {
|
||||
// then recalc style
|
||||
ctrl.calculatePanelHeight();
|
||||
// then wait another cycle (this might not be needed)
|
||||
$timeout(() => {
|
||||
ctrl.render();
|
||||
resizeScrollableContent();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// set initial height
|
||||
ctrl.calculatePanelHeight();
|
||||
|
||||
@ -119,7 +142,11 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
panelContainer.removeClass('panel-alert-state--' + lastAlertState);
|
||||
}
|
||||
|
||||
if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') {
|
||||
if (
|
||||
ctrl.alertState.state === 'ok' ||
|
||||
ctrl.alertState.state === 'alerting' ||
|
||||
ctrl.alertState.state === 'pending'
|
||||
) {
|
||||
panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state);
|
||||
}
|
||||
|
||||
@ -170,6 +197,9 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
scope.$apply(ctrl.openInspector.bind(ctrl));
|
||||
});
|
||||
|
||||
elem.on('mouseenter', mouseEnter);
|
||||
elem.on('mouseleave', mouseLeave);
|
||||
|
||||
scope.$on('$destroy', () => {
|
||||
elem.off();
|
||||
cornerInfoElem.off();
|
||||
|
@ -19,7 +19,7 @@ export class DatasourceSrv {
|
||||
this.datasources = {};
|
||||
}
|
||||
|
||||
get(name?): Promise<DataSourceApi> {
|
||||
get(name?: string): Promise<DataSourceApi> {
|
||||
if (!name) {
|
||||
return this.get(config.defaultDatasource);
|
||||
}
|
||||
@ -37,7 +37,7 @@ export class DatasourceSrv {
|
||||
return this.loadDatasource(name);
|
||||
}
|
||||
|
||||
loadDatasource(name) {
|
||||
loadDatasource(name: string): Promise<DataSourceApi> {
|
||||
const dsConfig = config.datasources[name];
|
||||
if (!dsConfig) {
|
||||
return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
|
||||
|
@ -118,7 +118,7 @@ export class DataSourceEditCtrl {
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
this.datasourceSrv.get(this.current.name).then(datasource => {
|
||||
return this.datasourceSrv.get(this.current.name).then(datasource => {
|
||||
if (!datasource.testDatasource) {
|
||||
return;
|
||||
}
|
||||
@ -126,7 +126,7 @@ export class DataSourceEditCtrl {
|
||||
this.testing = { done: false, status: 'error' };
|
||||
|
||||
// make test call in no backend cache context
|
||||
this.backendSrv
|
||||
return this.backendSrv
|
||||
.withNoBackendCache(() => {
|
||||
return datasource
|
||||
.testDatasource()
|
||||
@ -161,8 +161,8 @@ export class DataSourceEditCtrl {
|
||||
return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => {
|
||||
this.current = result.datasource;
|
||||
this.updateNav();
|
||||
this.updateFrontendSettings().then(() => {
|
||||
this.testDatasource();
|
||||
return this.updateFrontendSettings().then(() => {
|
||||
return this.testDatasource();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
@ -71,15 +71,21 @@
|
||||
<h3 class="page-heading">Auth</h3>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-10" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-checkbox class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-10" switch-class="max-width-6"></gf-form-checkbox>
|
||||
<gf-form-checkbox class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth
|
||||
headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11"
|
||||
switch-class="max-width-6"></gf-form-checkbox>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-10" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-10"
|
||||
checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-checkbox>
|
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for
|
||||
verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11"
|
||||
switch-class="max-width-6"></gf-form-checkbox>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verify" label-class="width-10" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verify" label-class="width-10"
|
||||
checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -124,7 +124,11 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
</button>
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
<i className="fa fa-rocket" /> {headerTooltip}
|
||||
<a className="text-link empty-list-cta__pro-tip-link" href="asd" target="_blank">
|
||||
<a
|
||||
className="text-link empty-list-cta__pro-tip-link"
|
||||
href="http://docs.grafana.org/auth/enhanced_ldap/"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
|
@ -74,7 +74,7 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
</td>
|
||||
<td>{member.login}</td>
|
||||
<td>{member.email}</td>
|
||||
{syncEnabled ? this.renderLabels(member.labels) : null}
|
||||
{syncEnabled && this.renderLabels(member.labels)}
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
|
||||
</td>
|
||||
@ -132,7 +132,7 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
<th />
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
{syncEnabled ? <th /> : ''}
|
||||
{syncEnabled && <th />}
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -96,7 +96,7 @@ exports[`Render should render component 1`] = `
|
||||
Sync LDAP or OAuth groups with your Grafana teams.
|
||||
<a
|
||||
className="text-link empty-list-cta__pro-tip-link"
|
||||
href="asd"
|
||||
href="http://docs.grafana.org/auth/enhanced_ldap/"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
|
@ -1,10 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
|
||||
import { PluginMeta, DataQuery } from 'app/types';
|
||||
|
||||
import LanguageProvider from './language_provider';
|
||||
import { mergeStreamsToLogs } from './result_transformer';
|
||||
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
|
||||
|
||||
export const DEFAULT_LIMIT = 1000;
|
||||
|
||||
@ -111,6 +112,10 @@ export default class LoggingDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]> {
|
||||
return this.languageProvider.importQueries(queries, originMeta.id);
|
||||
}
|
||||
|
||||
metadataRequest(url) {
|
||||
// HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
|
||||
const apiUrl = url.replace('v1', 'prom');
|
||||
|
@ -0,0 +1,74 @@
|
||||
import Plain from 'slate-plain-serializer';
|
||||
|
||||
import LanguageProvider from './language_provider';
|
||||
|
||||
describe('Language completion provider', () => {
|
||||
const datasource = {
|
||||
metadataRequest: () => ({ data: { data: [] } }),
|
||||
};
|
||||
|
||||
it('returns default suggestions on emtpty context', () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions.length).toEqual(0);
|
||||
});
|
||||
|
||||
describe('label suggestions', () => {
|
||||
it('returns default label suggestions on label context', () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('{}');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 1,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
value: valueWithSelection,
|
||||
});
|
||||
expect(result.context).toBe('context-labels');
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query imports', () => {
|
||||
const datasource = {
|
||||
metadataRequest: () => ({ data: { data: [] } }),
|
||||
};
|
||||
|
||||
it('returns empty queries for unknown origin datasource', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown');
|
||||
expect(result).toEqual([{ refId: 'bar', expr: '' }]);
|
||||
});
|
||||
|
||||
describe('prometheus query imports', () => {
|
||||
it('returns empty query from metric-only query', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const result = await instance.importPrometheusQuery('foo');
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
it('returns empty query from selector query if label is not available', async () => {
|
||||
const datasourceWithLabels = {
|
||||
metadataRequest: url => (url === '/api/prom/label' ? { data: { data: ['other'] } } : { data: { data: [] } }),
|
||||
};
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
const result = await instance.importPrometheusQuery('{foo="bar"}');
|
||||
expect(result).toEqual('{}');
|
||||
});
|
||||
|
||||
it('returns selector query from selector query with common labels', async () => {
|
||||
const datasourceWithLabels = {
|
||||
metadataRequest: url => (url === '/api/prom/label' ? { data: { data: ['foo'] } } : { data: { data: [] } }),
|
||||
};
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
||||
expect(result).toEqual('{foo="bar"}');
|
||||
});
|
||||
});
|
||||
});
|
@ -8,9 +8,9 @@ import {
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
} from 'app/types/explore';
|
||||
|
||||
import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils';
|
||||
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
||||
import PromqlSyntax from 'app/plugins/datasource/prometheus/promql';
|
||||
import { DataQuery } from 'app/types';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
@ -158,6 +158,56 @@ export default class LoggingLanguageProvider extends LanguageProvider {
|
||||
return { context, refresher, suggestions };
|
||||
}
|
||||
|
||||
async importQueries(queries: DataQuery[], datasourceType: string): Promise<DataQuery[]> {
|
||||
if (datasourceType === 'prometheus') {
|
||||
return Promise.all(
|
||||
queries.map(async query => {
|
||||
const expr = await this.importPrometheusQuery(query.expr);
|
||||
return {
|
||||
...query,
|
||||
expr,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
return queries.map(query => ({
|
||||
...query,
|
||||
expr: '',
|
||||
}));
|
||||
}
|
||||
|
||||
async importPrometheusQuery(query: string): Promise<string> {
|
||||
// Consider only first selector in query
|
||||
const selectorMatch = query.match(selectorRegexp);
|
||||
if (selectorMatch) {
|
||||
const selector = selectorMatch[0];
|
||||
const labels = {};
|
||||
selector.replace(labelRegexp, (_, key, operator, value) => {
|
||||
labels[key] = { value, operator };
|
||||
return '';
|
||||
});
|
||||
|
||||
// Keep only labels that exist on origin and target datasource
|
||||
await this.start(); // fetches all existing label keys
|
||||
const commonLabels = {};
|
||||
for (const key in labels) {
|
||||
const existingKeys = this.labelKeys[EMPTY_SELECTOR];
|
||||
if (existingKeys.indexOf(key) > -1) {
|
||||
// Should we check for label value equality here?
|
||||
commonLabels[key] = labels[key];
|
||||
}
|
||||
}
|
||||
const labelKeys = Object.keys(commonLabels).sort();
|
||||
const cleanSelector = labelKeys
|
||||
.map(key => `${key}${commonLabels[key].operator}${commonLabels[key].value}`)
|
||||
.join(',');
|
||||
|
||||
return ['{', cleanSelector, '}'].join('');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async fetchLogLabels() {
|
||||
const url = '/api/prom/label';
|
||||
try {
|
||||
|
@ -10,6 +10,7 @@ import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import addLabelToQuery from './add_label_to_query';
|
||||
import { getQueryHints } from './query_hints';
|
||||
import { expandRecordingRules } from './language_utils';
|
||||
|
||||
export function alignRange(start, end, step) {
|
||||
const alignedEnd = Math.ceil(end / step) * step;
|
||||
@ -468,11 +469,8 @@ export class PrometheusDatasource {
|
||||
return `sum(${query.trim()}) by ($1)`;
|
||||
}
|
||||
case 'EXPAND_RULES': {
|
||||
const mapping = action.mapping;
|
||||
if (mapping) {
|
||||
const ruleNames = Object.keys(mapping);
|
||||
const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\()`, 'ig');
|
||||
return query.replace(rulesRegex, (match, pre, name, post) => mapping[name]);
|
||||
if (action.mapping) {
|
||||
return expandRecordingRules(query, action.mapping);
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -78,9 +78,24 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput {
|
||||
provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
|
||||
// Local text properties
|
||||
const empty = value.document.text.length === 0;
|
||||
const selectedLines = value.document.getTextsAtRangeAsArray(value.selection);
|
||||
const currentLine = selectedLines.length === 1 ? selectedLines[0] : null;
|
||||
const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null;
|
||||
|
||||
// Syntax spans have 3 classes by default. More indicate a recognized token
|
||||
const tokenRecognized = wrapperClasses.length > 3;
|
||||
// Non-empty prefix, but not inside known token
|
||||
const prefixUnrecognized = prefix && !tokenRecognized;
|
||||
// Prevent suggestions in `function(|suffix)`
|
||||
const noSuffix = !nextCharacter || nextCharacter === ')';
|
||||
// Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it
|
||||
const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix;
|
||||
// About to type next operand if preceded by binary operator
|
||||
const isNextOperand = text.match(/[+\-*/^%]/);
|
||||
|
||||
// Determine candidates by CSS context
|
||||
if (_.includes(wrapperClasses, 'context-range')) {
|
||||
// Suggestions for metric[|]
|
||||
@ -89,14 +104,14 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
|
||||
return this.getLabelCompletionItems.apply(this, arguments);
|
||||
} else if (_.includes(wrapperClasses, 'context-aggregation')) {
|
||||
// Suggestions for sum(metric) by (|)
|
||||
return this.getAggregationCompletionItems.apply(this, arguments);
|
||||
} else if (
|
||||
// Show default suggestions in a couple of scenarios
|
||||
(prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token
|
||||
(prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace
|
||||
text.match(/[+\-*/^%]/) // Anything after binary operator
|
||||
) {
|
||||
} else if (empty) {
|
||||
// Suggestions for empty query field
|
||||
return this.getEmptyCompletionItems(context || {});
|
||||
} else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) {
|
||||
// Show term suggestions in a couple of scenarios
|
||||
return this.getTermCompletionItems();
|
||||
}
|
||||
|
||||
return {
|
||||
@ -106,8 +121,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
|
||||
getEmptyCompletionItems(context: any): TypeaheadOutput {
|
||||
const { history } = context;
|
||||
const { metrics } = this;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
let suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
if (history && history.length > 0) {
|
||||
const historyItems = _.chain(history)
|
||||
@ -126,13 +140,23 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
});
|
||||
}
|
||||
|
||||
const termCompletionItems = this.getTermCompletionItems();
|
||||
suggestions = [...suggestions, ...termCompletionItems.suggestions];
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
getTermCompletionItems(): TypeaheadOutput {
|
||||
const { metrics } = this;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
label: 'Functions',
|
||||
items: FUNCTIONS.map(setFunctionKind),
|
||||
});
|
||||
|
||||
if (metrics) {
|
||||
if (metrics && metrics.length > 0) {
|
||||
suggestions.push({
|
||||
label: 'Metrics',
|
||||
items: metrics.map(wrapLabel),
|
||||
|
@ -24,8 +24,8 @@ export function processLabels(labels, withName = false) {
|
||||
}
|
||||
|
||||
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
|
||||
const selectorRegexp = /\{[^}]*?\}/;
|
||||
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
|
||||
export const selectorRegexp = /\{[^}]*?\}/;
|
||||
export const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
|
||||
export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
|
||||
if (!query.match(selectorRegexp)) {
|
||||
// Special matcher for metrics
|
||||
@ -83,3 +83,9 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
|
||||
|
||||
return { labelKeys, selector: selectorString };
|
||||
}
|
||||
|
||||
export function expandRecordingRules(query: string, mapping: { [name: string]: string }): string {
|
||||
const ruleNames = Object.keys(mapping);
|
||||
const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\(|\\[|\\{)`, 'ig');
|
||||
return query.replace(rulesRegex, (match, pre, name, post) => `${pre}${mapping[name]}${post}`);
|
||||
}
|
||||
|
@ -7,18 +7,47 @@ describe('Language completion provider', () => {
|
||||
metadataRequest: () => ({ data: { data: [] } }),
|
||||
};
|
||||
|
||||
it('returns default suggestions on emtpty context', () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions.length).toEqual(2);
|
||||
describe('empty query suggestions', () => {
|
||||
it('returns default suggestions on emtpty context', () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('');
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns default suggestions with metrics on emtpty context when metrics were provided', () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const value = Plain.deserialize('');
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
},
|
||||
{
|
||||
label: 'Metrics',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('range suggestions', () => {
|
||||
it('returns range suggestions in range context', () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] });
|
||||
const value = Plain.deserialize('1');
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '1',
|
||||
prefix: '1',
|
||||
value,
|
||||
wrapperClasses: ['context-range'],
|
||||
});
|
||||
expect(result.context).toBe('context-range');
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toEqual([
|
||||
@ -31,20 +60,54 @@ describe('Language completion provider', () => {
|
||||
});
|
||||
|
||||
describe('metric suggestions', () => {
|
||||
it('returns metrics suggestions by default', () => {
|
||||
it('returns metrics and function suggestions in an unknown context', () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] });
|
||||
const value = Plain.deserialize('a');
|
||||
const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions.length).toEqual(2);
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
},
|
||||
{
|
||||
label: 'Metrics',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns default suggestions after a binary operator', () => {
|
||||
it('returns metrics and function suggestions after a binary operator', () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] });
|
||||
const value = Plain.deserialize('*');
|
||||
const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions.length).toEqual(2);
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
},
|
||||
{
|
||||
label: 'Metrics',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no suggestions at the beginning of a non-empty function', () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const value = Plain.deserialize('sum(up)');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 4,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
value: valueWithSelection,
|
||||
wrapperClasses: [],
|
||||
});
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { parseSelector } from '../language_utils';
|
||||
import { expandRecordingRules, parseSelector } from '../language_utils';
|
||||
|
||||
describe('parseSelector()', () => {
|
||||
let parsed;
|
||||
@ -62,3 +62,25 @@ describe('parseSelector()', () => {
|
||||
expect(parsed.selector).toBe('{__name__="bar:metric:1m"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandRecordingRules()', () => {
|
||||
it('returns query w/o recording rules as is', () => {
|
||||
expect(expandRecordingRules('metric', {})).toBe('metric');
|
||||
expect(expandRecordingRules('metric + metric', {})).toBe('metric + metric');
|
||||
expect(expandRecordingRules('metric{}', {})).toBe('metric{}');
|
||||
});
|
||||
|
||||
it('does not modify recording rules name in label values', () => {
|
||||
expect(expandRecordingRules('{__name__="metric"} + bar', { metric: 'foo', bar: 'super' })).toBe(
|
||||
'{__name__="metric"} + super'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns query with expanded recording rules', () => {
|
||||
expect(expandRecordingRules('metric', { metric: 'foo' })).toBe('foo');
|
||||
expect(expandRecordingRules('metric + metric', { metric: 'foo' })).toBe('foo + foo');
|
||||
expect(expandRecordingRules('metric{}', { metric: 'foo' })).toBe('foo{}');
|
||||
expect(expandRecordingRules('metric[]', { metric: 'foo' })).toBe('foo[]');
|
||||
expect(expandRecordingRules('metric + foo', { metric: 'foo', foo: 'bar' })).toBe('foo + bar');
|
||||
});
|
||||
});
|
||||
|
@ -50,5 +50,6 @@
|
||||
<gf-form-switch class="gf-form" label="No data" label-class="width-10" checked="ctrl.stateFilter['no_data']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Execution error" label-class="width-10" checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Pending" label-class="width-10" checked="ctrl.stateFilter['pending']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ import { tickStep } from 'app/core/utils/ticks';
|
||||
import { appEvents, coreModule, updateLegendValues } from 'app/core/core';
|
||||
import GraphTooltip from './graph_tooltip';
|
||||
import { ThresholdManager } from './threshold_manager';
|
||||
import { TimeRegionManager } from './time_region_manager';
|
||||
import { EventManager } from 'app/features/annotations/all';
|
||||
import { convertToHistogramData } from './histogram';
|
||||
import { alignYLevel } from './align_yaxes';
|
||||
@ -38,6 +39,7 @@ class GraphElement {
|
||||
panelWidth: number;
|
||||
eventManager: EventManager;
|
||||
thresholdManager: ThresholdManager;
|
||||
timeRegionManager: TimeRegionManager;
|
||||
legendElem: HTMLElement;
|
||||
|
||||
constructor(private scope, private elem, private timeSrv) {
|
||||
@ -49,6 +51,7 @@ class GraphElement {
|
||||
this.panelWidth = 0;
|
||||
this.eventManager = new EventManager(this.ctrl);
|
||||
this.thresholdManager = new ThresholdManager(this.ctrl);
|
||||
this.timeRegionManager = new TimeRegionManager(this.ctrl);
|
||||
this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
|
||||
return this.sortedSeries;
|
||||
});
|
||||
@ -125,6 +128,7 @@ class GraphElement {
|
||||
|
||||
onPanelTeardown() {
|
||||
this.thresholdManager = null;
|
||||
this.timeRegionManager = null;
|
||||
|
||||
if (this.plot) {
|
||||
this.plot.destroy();
|
||||
@ -215,6 +219,7 @@ class GraphElement {
|
||||
}
|
||||
|
||||
this.thresholdManager.draw(plot);
|
||||
this.timeRegionManager.draw(plot);
|
||||
}
|
||||
|
||||
processOffsetHook(plot, gridMargin) {
|
||||
@ -293,6 +298,7 @@ class GraphElement {
|
||||
this.prepareXAxis(options, this.panel);
|
||||
this.configureYAxisOptions(this.data, options);
|
||||
this.thresholdManager.addFlotOptions(options, this.panel);
|
||||
this.timeRegionManager.addFlotOptions(options, this.panel);
|
||||
this.eventManager.addFlotEvents(this.annotations, options);
|
||||
|
||||
this.sortedSeries = this.sortSeries(this.data, this.panel);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import './graph';
|
||||
import './series_overrides_ctrl';
|
||||
import './thresholds_form';
|
||||
import './time_regions_form';
|
||||
|
||||
import template from './template';
|
||||
import _ from 'lodash';
|
||||
@ -111,6 +112,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
// other style overrides
|
||||
seriesOverrides: [],
|
||||
thresholds: [],
|
||||
timeRegions: [],
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
@ -133,9 +135,9 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
|
||||
this.addEditorTab('Axes', axesEditorComponent, 2);
|
||||
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
|
||||
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
|
||||
|
||||
// if (config.alertingEnabled) {
|
||||
// this.addEditorTab('Alert', alertTab, 5);
|
||||
|
262
public/app/plugins/panel/graph/specs/time_region_manager.test.ts
Normal file
262
public/app/plugins/panel/graph/specs/time_region_manager.test.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import { TimeRegionManager, colorModes } from '../time_region_manager';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('TimeRegionManager', () => {
|
||||
function plotOptionsScenario(desc, func) {
|
||||
describe(desc, () => {
|
||||
const ctx: any = {
|
||||
panel: {
|
||||
timeRegions: [],
|
||||
},
|
||||
options: {
|
||||
grid: { markings: [] },
|
||||
},
|
||||
panelCtrl: {
|
||||
range: {},
|
||||
dashboard: {
|
||||
isTimezoneUtc: () => false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
ctx.setup = (regions, from, to) => {
|
||||
ctx.panel.timeRegions = regions;
|
||||
ctx.panelCtrl.range.from = from;
|
||||
ctx.panelCtrl.range.to = to;
|
||||
const manager = new TimeRegionManager(ctx.panelCtrl);
|
||||
manager.addFlotOptions(ctx.options, ctx.panel);
|
||||
};
|
||||
|
||||
ctx.printScenario = () => {
|
||||
console.log(
|
||||
`Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`,
|
||||
ctx.panelCtrl.range.from._isUTC
|
||||
);
|
||||
ctx.options.grid.markings.forEach((m, i) => {
|
||||
console.log(
|
||||
`Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
func(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
describe('When creating plot markings using local time', () => {
|
||||
plotOptionsScenario('for day of week region', ctx => {
|
||||
const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }];
|
||||
const from = moment('2018-01-01T00:00:00+01:00');
|
||||
const to = moment('2018-01-01T23:59:00+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add fill', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format());
|
||||
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||
});
|
||||
|
||||
it('should add line before', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-01T01:00:00+01:00').format());
|
||||
expect(markings[1].color).toBe(colorModes.red.color.line);
|
||||
});
|
||||
|
||||
it('should add line after', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-02T00:59:59+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format());
|
||||
expect(markings[2].color).toBe(colorModes.red.color.line);
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for time from region', ctx => {
|
||||
const regions = [{ from: '05:00', fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-01-01T00:00+01:00');
|
||||
const to = moment('2018-01-03T23:59+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at 05:00 each day', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T06:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-01T06:00:00+01:00').format());
|
||||
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-02T06:00:00+01:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-02T06:00:00+01:00').format());
|
||||
expect(markings[1].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-03T06:00:00+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-03T06:00:00+01:00').format());
|
||||
expect(markings[2].color).toBe(colorModes.red.color.fill);
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for time to region', ctx => {
|
||||
const regions = [{ to: '05:00', fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-02-01T00:00+01:00');
|
||||
const to = moment('2018-02-03T23:59+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at 05:00 each day', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-02-01T06:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-02-01T06:00:00+01:00').format());
|
||||
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-02-02T06:00:00+01:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-02-02T06:00:00+01:00').format());
|
||||
expect(markings[1].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-02-03T06:00:00+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-02-03T06:00:00+01:00').format());
|
||||
expect(markings[2].color).toBe(colorModes.red.color.fill);
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for day of week from/to region', ctx => {
|
||||
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-01-01T18:45:05+01:00');
|
||||
const to = moment('2018-01-22T08:27:00+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at each sunday', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format());
|
||||
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format());
|
||||
expect(markings[1].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format());
|
||||
expect(markings[2].color).toBe(colorModes.red.color.fill);
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for day of week from region', ctx => {
|
||||
const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-01-01T18:45:05+01:00');
|
||||
const to = moment('2018-01-22T08:27:00+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at each sunday', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format());
|
||||
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format());
|
||||
expect(markings[1].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format());
|
||||
expect(markings[2].color).toBe(colorModes.red.color.fill);
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for day of week to region', ctx => {
|
||||
const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-01-01T18:45:05+01:00');
|
||||
const to = moment('2018-01-22T08:27:00+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at each sunday', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format());
|
||||
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format());
|
||||
expect(markings[1].color).toBe(colorModes.red.color.fill);
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format());
|
||||
expect(markings[2].color).toBe(colorModes.red.color.fill);
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => {
|
||||
const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-03-17T06:00:00+01:00');
|
||||
const to = moment('2018-04-03T06:00:00+02:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at each sunday between 20:00 and 23:00', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-03-18T21:00:00+01:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-03-19T00:00:00+01:00').format());
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-03-25T22:00:00+02:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-03-26T01:00:00+02:00').format());
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-04-01T22:00:00+02:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-04-02T01:00:00+02:00').format());
|
||||
});
|
||||
});
|
||||
|
||||
plotOptionsScenario('for each day of week with winter time', ctx => {
|
||||
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
|
||||
const from = moment('2018-10-20T14:50:11+02:00');
|
||||
const to = moment('2018-11-07T12:56:23+01:00');
|
||||
ctx.setup(regions, from, to);
|
||||
|
||||
it('should add 3 markings', () => {
|
||||
expect(ctx.options.grid.markings.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add one fill at each sunday', () => {
|
||||
const markings = ctx.options.grid.markings;
|
||||
|
||||
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-10-21T02:00:00+02:00').format());
|
||||
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-10-22T01:59:59+02:00').format());
|
||||
|
||||
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-10-28T02:00:00+02:00').format());
|
||||
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-10-29T00:59:59+01:00').format());
|
||||
|
||||
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-11-04T01:00:00+01:00').format());
|
||||
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-11-05T00:59:59+01:00').format());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -14,6 +14,11 @@
|
||||
Thresholds <span class="muted">({{ctrl.panel.thresholds.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 3}">
|
||||
<a ng-click="ctrl.subTabIndex = 3">
|
||||
Time regions <span class="muted">({{ctrl.panel.timeRegions.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
@ -132,4 +137,8 @@
|
||||
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 3">
|
||||
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
77
public/app/plugins/panel/graph/thresholds_form.html
Normal file
77
public/app/plugins/panel/graph/thresholds_form.html
Normal file
@ -0,0 +1,77 @@
|
||||
<div class="gf-form-group">
|
||||
<h5>Thresholds</h5>
|
||||
<p class="muted" ng-show="ctrl.disabled">
|
||||
Visual thresholds options <strong>disabled.</strong>
|
||||
Visit the Alert tab update your thresholds. <br>
|
||||
To re-enable thresholds, the alert rule must be deleted from this panel.
|
||||
</p>
|
||||
<div ng-class="{'thresholds-form-disabled': ctrl.disabled}">
|
||||
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.thresholds">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">T{{$index+1}}</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="threshold.op"
|
||||
ng-options="f for f in ['gt', 'lt']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled"></select>
|
||||
</div>
|
||||
<input type="number" ng-model="threshold.value" class="gf-form-input width-8"
|
||||
ng-change="ctrl.render()" placeholder="value" ng-disabled="ctrl.disabled">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Color</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="threshold.colorMode"
|
||||
ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Fill" checked="threshold.fill"
|
||||
on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
|
||||
|
||||
<div class="gf-form" ng-if="threshold.fill && threshold.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Fill color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="threshold.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Line" checked="threshold.line"
|
||||
on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
|
||||
|
||||
<div class="gf-form" ng-if="threshold.line && threshold.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Line color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="threshold.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Y-Axis</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="threshold.yaxis"
|
||||
ng-init="threshold.yaxis = threshold.yaxis === 'left' || threshold.yaxis === 'right' ? threshold.yaxis : 'left'"
|
||||
ng-options="f for f in ['left', 'right']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" ng-click="ctrl.removeThreshold($index)" ng-disabled="ctrl.disabled">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.addThreshold()" ng-disabled="ctrl.disabled">
|
||||
<i class="fa fa-plus"></i> Add Threshold
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -58,90 +58,10 @@ export class ThresholdFormCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
const template = `
|
||||
<div class="gf-form-group">
|
||||
<h5>Thresholds</h5>
|
||||
<p class="muted" ng-show="ctrl.disabled">
|
||||
Visual thresholds options <strong>disabled.</strong>
|
||||
Visit the Alert tab update your thresholds. <br>
|
||||
To re-enable thresholds, the alert rule must be deleted from this panel.
|
||||
</p>
|
||||
<div ng-class="{'thresholds-form-disabled': ctrl.disabled}">
|
||||
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.thresholds">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">T{{$index+1}}</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="threshold.op"
|
||||
ng-options="f for f in ['gt', 'lt']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled"></select>
|
||||
</div>
|
||||
<input type="number" ng-model="threshold.value" class="gf-form-input width-8"
|
||||
ng-change="ctrl.render()" placeholder="value" ng-disabled="ctrl.disabled">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Color</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="threshold.colorMode"
|
||||
ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Fill" checked="threshold.fill"
|
||||
on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
|
||||
|
||||
<div class="gf-form" ng-if="threshold.fill && threshold.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Fill color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="threshold.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Line" checked="threshold.line"
|
||||
on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
|
||||
|
||||
<div class="gf-form" ng-if="threshold.line && threshold.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Line color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="threshold.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Y-Axis</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="threshold.yaxis"
|
||||
ng-init="threshold.yaxis = threshold.yaxis === 'left' || threshold.yaxis === 'right' ? threshold.yaxis : 'left'"
|
||||
ng-options="f for f in ['left', 'right']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" ng-click="ctrl.removeThreshold($index)" ng-disabled="ctrl.disabled">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.addThreshold()" ng-disabled="ctrl.disabled">
|
||||
<i class="fa fa-plus"></i> Add Threshold
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
coreModule.directive('graphThresholdForm', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
templateUrl: 'public/app/plugins/panel/graph/thresholds_form.html',
|
||||
controller: ThresholdFormCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
|
248
public/app/plugins/panel/graph/time_region_manager.ts
Normal file
248
public/app/plugins/panel/graph/time_region_manager.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const colorModes = {
|
||||
gray: {
|
||||
themeDependent: true,
|
||||
title: 'Gray',
|
||||
darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' },
|
||||
lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' },
|
||||
},
|
||||
red: {
|
||||
title: 'Red',
|
||||
color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' },
|
||||
},
|
||||
green: {
|
||||
title: 'Green',
|
||||
color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' },
|
||||
},
|
||||
blue: {
|
||||
title: 'Blue',
|
||||
color: { fill: 'rgba(11, 125, 238, 0.12)', line: 'rgba(11, 125, 238, 0.60)' },
|
||||
},
|
||||
yellow: {
|
||||
title: 'Yellow',
|
||||
color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' },
|
||||
},
|
||||
custom: { title: 'Custom' },
|
||||
};
|
||||
|
||||
export function getColorModes() {
|
||||
return _.map(Object.keys(colorModes), key => {
|
||||
return {
|
||||
key: key,
|
||||
value: colorModes[key].title,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getColor(timeRegion) {
|
||||
if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) {
|
||||
timeRegion.colorMode = 'red';
|
||||
}
|
||||
|
||||
if (timeRegion.colorMode === 'custom') {
|
||||
return {
|
||||
fill: timeRegion.fillColor,
|
||||
line: timeRegion.lineColor,
|
||||
};
|
||||
}
|
||||
|
||||
const colorMode = colorModes[timeRegion.colorMode];
|
||||
if (colorMode.themeDependent === true) {
|
||||
return config.bootData.user.lightTheme ? colorMode.lightColor : colorMode.darkColor;
|
||||
}
|
||||
|
||||
return colorMode.color;
|
||||
}
|
||||
|
||||
export class TimeRegionManager {
|
||||
plot: any;
|
||||
timeRegions: any;
|
||||
|
||||
constructor(private panelCtrl) {}
|
||||
|
||||
draw(plot) {
|
||||
this.timeRegions = this.panelCtrl.panel.timeRegions;
|
||||
this.plot = plot;
|
||||
}
|
||||
|
||||
addFlotOptions(options, panel) {
|
||||
if (!panel.timeRegions || panel.timeRegions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tRange = { from: moment(this.panelCtrl.range.from).utc(), to: moment(this.panelCtrl.range.to).utc() };
|
||||
|
||||
let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor;
|
||||
|
||||
const timeRegionsCopy = panel.timeRegions.map(a => ({ ...a }));
|
||||
|
||||
for (i = 0; i < timeRegionsCopy.length; i++) {
|
||||
timeRegion = timeRegionsCopy[i];
|
||||
|
||||
if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hRange = {
|
||||
from: this.parseTimeRange(timeRegion.from),
|
||||
to: this.parseTimeRange(timeRegion.to),
|
||||
};
|
||||
|
||||
if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) {
|
||||
timeRegion.fromDayOfWeek = timeRegion.toDayOfWeek;
|
||||
}
|
||||
|
||||
if (!timeRegion.toDayOfWeek && timeRegion.fromDayOfWeek) {
|
||||
timeRegion.toDayOfWeek = timeRegion.fromDayOfWeek;
|
||||
}
|
||||
|
||||
if (timeRegion.fromDayOfWeek) {
|
||||
hRange.from.dayOfWeek = Number(timeRegion.fromDayOfWeek);
|
||||
}
|
||||
|
||||
if (timeRegion.toDayOfWeek) {
|
||||
hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek);
|
||||
}
|
||||
|
||||
if (!hRange.from.h && hRange.to.h) {
|
||||
hRange.from = hRange.to;
|
||||
}
|
||||
|
||||
if (hRange.from.h && !hRange.to.h) {
|
||||
hRange.to = hRange.from;
|
||||
}
|
||||
|
||||
if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) {
|
||||
hRange.from.h = 0;
|
||||
hRange.from.m = 0;
|
||||
hRange.from.s = 0;
|
||||
}
|
||||
|
||||
if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) {
|
||||
hRange.to.h = 23;
|
||||
hRange.to.m = 59;
|
||||
hRange.to.s = 59;
|
||||
}
|
||||
|
||||
if (!hRange.from || !hRange.to) {
|
||||
continue;
|
||||
}
|
||||
|
||||
regions = [];
|
||||
|
||||
if (
|
||||
hRange.from.h >= tRange.from.hour() &&
|
||||
hRange.from.h <= tRange.from.hour() &&
|
||||
hRange.from.m >= tRange.from.minute() &&
|
||||
hRange.from.m <= tRange.from.minute() &&
|
||||
hRange.to.h >= tRange.to.hour() &&
|
||||
hRange.to.h <= tRange.to.hour() &&
|
||||
hRange.to.m >= tRange.to.minute() &&
|
||||
hRange.to.m <= tRange.to.minute()
|
||||
) {
|
||||
regions.push({ from: tRange.from.valueOf(), to: tRange.to.startOf('hour').valueOf() });
|
||||
} else {
|
||||
fromStart = moment(tRange.from);
|
||||
fromStart.set('hour', 0);
|
||||
fromStart.set('minute', 0);
|
||||
fromStart.set('second', 0);
|
||||
fromStart.add(hRange.from.h, 'hours');
|
||||
fromStart.add(hRange.from.m, 'minutes');
|
||||
fromStart.add(hRange.from.s, 'seconds');
|
||||
|
||||
while (fromStart.unix() <= tRange.to.unix()) {
|
||||
while (hRange.from.dayOfWeek && hRange.from.dayOfWeek !== fromStart.isoWeekday()) {
|
||||
fromStart.add(24, 'hours');
|
||||
}
|
||||
|
||||
if (fromStart.unix() > tRange.to.unix()) {
|
||||
break;
|
||||
}
|
||||
|
||||
fromEnd = moment(fromStart);
|
||||
|
||||
if (hRange.from.h <= hRange.to.h) {
|
||||
fromEnd.add(hRange.to.h - hRange.from.h, 'hours');
|
||||
} else if (hRange.from.h + hRange.to.h < 23) {
|
||||
fromEnd.add(hRange.to.h, 'hours');
|
||||
} else {
|
||||
fromEnd.add(24 - hRange.from.h, 'hours');
|
||||
}
|
||||
|
||||
fromEnd.set('minute', hRange.to.m);
|
||||
fromEnd.set('second', hRange.to.s);
|
||||
|
||||
while (hRange.to.dayOfWeek && hRange.to.dayOfWeek !== fromEnd.isoWeekday()) {
|
||||
fromEnd.add(24, 'hours');
|
||||
}
|
||||
|
||||
const outsideRange =
|
||||
(fromStart.unix() < tRange.from.unix() && fromEnd.unix() < tRange.from.unix()) ||
|
||||
(fromStart.unix() > tRange.to.unix() && fromEnd.unix() > tRange.to.unix());
|
||||
|
||||
if (!outsideRange) {
|
||||
regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() });
|
||||
}
|
||||
|
||||
fromStart.add(24, 'hours');
|
||||
}
|
||||
}
|
||||
|
||||
timeRegionColor = getColor(timeRegion);
|
||||
|
||||
for (let j = 0; j < regions.length; j++) {
|
||||
const r = regions[j];
|
||||
if (timeRegion.fill) {
|
||||
options.grid.markings.push({
|
||||
xaxis: { from: r.from, to: r.to },
|
||||
color: timeRegionColor.fill,
|
||||
});
|
||||
}
|
||||
|
||||
if (timeRegion.line) {
|
||||
options.grid.markings.push({
|
||||
xaxis: { from: r.from, to: r.from },
|
||||
color: timeRegionColor.line,
|
||||
});
|
||||
options.grid.markings.push({
|
||||
xaxis: { from: r.to, to: r.to },
|
||||
color: timeRegionColor.line,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseTimeRange(str) {
|
||||
const timeRegex = /^([\d]+):?(\d{2})?/;
|
||||
const result = { h: null, m: null };
|
||||
const match = timeRegex.exec(str);
|
||||
|
||||
if (!match) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (match.length > 1) {
|
||||
result.h = Number(match[1]);
|
||||
result.m = 0;
|
||||
|
||||
if (match.length > 2 && match[2] !== undefined) {
|
||||
result.m = Number(match[2]);
|
||||
}
|
||||
|
||||
if (result.h > 23) {
|
||||
result.h = 23;
|
||||
}
|
||||
|
||||
if (result.m > 59) {
|
||||
result.m = 59;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
64
public/app/plugins/panel/graph/time_regions_form.html
Normal file
64
public/app/plugins/panel/graph/time_regions_form.html
Normal file
@ -0,0 +1,64 @@
|
||||
<div class="gf-form-group">
|
||||
<h5>Time regions <tip>All configured time regions refers to UTC time</tip></h5>
|
||||
<div class="gf-form-inline" ng-repeat="timeRegion in ctrl.panel.timeRegions">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">T{{$index+1}}</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">From</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input width-6" ng-model="timeRegion.fromDayOfWeek" ng-options="f.d as f.value for f in [{d: undefined, value: 'Any'}, {d:1, value: 'Mon'}, {d:2, value: 'Tue'}, {d:3, value: 'Wed'}, {d:4, value: 'Thu'}, {d:5, value: 'Fri'}, {d:6, value: 'Sat'}, {d:7, value: 'Sun'}]"
|
||||
ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
<input type="text" ng-maxlength="5" ng-model="timeRegion.from" class="gf-form-input width-5" ng-change="ctrl.render()" placeholder="hh:mm">
|
||||
<label class="gf-form-label">To</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input width-6" ng-model="timeRegion.toDayOfWeek" ng-options="f.d as f.value for f in [{d: undefined, value: 'Any'}, {d:1, value: 'Mon'}, {d:2, value: 'Tue'}, {d:3, value: 'Wed'}, {d:4, value: 'Thu'}, {d:5, value: 'Fri'}, {d:6, value: 'Sat'}, {d:7, value: 'Sun'}]"
|
||||
ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
<input type="text" ng-maxlength="5" ng-model="timeRegion.to" class="gf-form-input width-5" ng-change="ctrl.render()" placeholder="hh:mm"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Color</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="timeRegion.colorMode" ng-options="f.key as f.value for f in ctrl.colorModes" ng-change="ctrl.render()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Fill" checked="timeRegion.fill" on-change="ctrl.render()"></gf-form-switch>
|
||||
|
||||
<div class="gf-form" ng-if="timeRegion.fill && timeRegion.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Fill color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="timeRegion.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Line" checked="timeRegion.line" on-change="ctrl.render()"></gf-form-switch>
|
||||
|
||||
<div class="gf-form" ng-if="timeRegion.line && timeRegion.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Line color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="timeRegion.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" ng-click="ctrl.removeTimeRegion($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.addTimeRegion()">
|
||||
<i class="fa fa-plus"></i> Add time region
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
73
public/app/plugins/panel/graph/time_regions_form.ts
Normal file
73
public/app/plugins/panel/graph/time_regions_form.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { getColorModes } from './time_region_manager';
|
||||
|
||||
export class TimeRegionFormCtrl {
|
||||
panelCtrl: any;
|
||||
panel: any;
|
||||
disabled: boolean;
|
||||
colorModes: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
this.panel = this.panelCtrl.panel;
|
||||
|
||||
const unbindDestroy = $scope.$on('$destroy', () => {
|
||||
this.panelCtrl.editingTimeRegions = false;
|
||||
this.panelCtrl.render();
|
||||
unbindDestroy();
|
||||
});
|
||||
|
||||
this.colorModes = getColorModes();
|
||||
this.panelCtrl.editingTimeRegions = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
addTimeRegion() {
|
||||
this.panel.timeRegions.push({
|
||||
op: 'time',
|
||||
fromDayOfWeek: undefined,
|
||||
from: undefined,
|
||||
toDayOfWeek: undefined,
|
||||
to: undefined,
|
||||
colorMode: 'background6',
|
||||
fill: true,
|
||||
line: false,
|
||||
});
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
removeTimeRegion(index) {
|
||||
this.panel.timeRegions.splice(index, 1);
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
onFillColorChange(index) {
|
||||
return newColor => {
|
||||
this.panel.timeRegions[index].fillColor = newColor;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
|
||||
onLineColorChange(index) {
|
||||
return newColor => {
|
||||
this.panel.timeRegions[index].lineColor = newColor;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('graphTimeRegionForm', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/plugins/panel/graph/time_regions_form.html',
|
||||
controller: TimeRegionFormCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
panelCtrl: '=',
|
||||
},
|
||||
};
|
||||
});
|
@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private linkSrv) {
|
||||
constructor($scope, $injector, private linkSrv, private $sanitize) {
|
||||
super($scope, $injector);
|
||||
_.defaults(this.panel, this.panelDefaults);
|
||||
|
||||
@ -398,14 +398,15 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
const $location = this.$location;
|
||||
const linkSrv = this.linkSrv;
|
||||
const $timeout = this.$timeout;
|
||||
const $sanitize = this.$sanitize;
|
||||
const panel = ctrl.panel;
|
||||
const templateSrv = this.templateSrv;
|
||||
let data, linkInfo;
|
||||
const $panelContainer = elem.find('.panel-container');
|
||||
elem = elem.find('.singlestat-panel');
|
||||
|
||||
function applyColoringThresholds(value, valueString) {
|
||||
const color = getColorForValue(data, value);
|
||||
function applyColoringThresholds(valueString) {
|
||||
const color = getColorForValue(data, data.value);
|
||||
if (color) {
|
||||
return '<span style="color:' + color + '">' + valueString + '</span>';
|
||||
}
|
||||
@ -413,8 +414,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
return valueString;
|
||||
}
|
||||
|
||||
function getSpan(className, fontSize, value) {
|
||||
value = templateSrv.replace(value, data.scopedVars);
|
||||
function getSpan(className, fontSize, applyColoring, value) {
|
||||
value = $sanitize(templateSrv.replace(value, data.scopedVars));
|
||||
value = applyColoring ? applyColoringThresholds(value) : value;
|
||||
return '<span class="' + className + '" style="font-size:' + fontSize + '">' + value + '</span>';
|
||||
}
|
||||
|
||||
@ -422,25 +424,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
let body = '<div class="singlestat-panel-value-container">';
|
||||
|
||||
if (panel.prefix) {
|
||||
let prefix = panel.prefix;
|
||||
if (panel.colorPrefix) {
|
||||
prefix = applyColoringThresholds(data.value, panel.prefix);
|
||||
}
|
||||
body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix);
|
||||
body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.colorPrefix, panel.prefix);
|
||||
}
|
||||
|
||||
let value = data.valueFormatted;
|
||||
if (panel.colorValue) {
|
||||
value = applyColoringThresholds(data.value, value);
|
||||
}
|
||||
body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
|
||||
body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.valueFormatted);
|
||||
|
||||
if (panel.postfix) {
|
||||
let postfix = panel.postfix;
|
||||
if (panel.colorPostfix) {
|
||||
postfix = applyColoringThresholds(data.value, panel.postfix);
|
||||
}
|
||||
body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix);
|
||||
body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix);
|
||||
}
|
||||
|
||||
body += '</div>';
|
||||
|
@ -14,6 +14,8 @@ describe('SingleStatCtrl', () => {
|
||||
get: () => {},
|
||||
};
|
||||
|
||||
const $sanitize = {};
|
||||
|
||||
SingleStatCtrl.prototype.panel = {
|
||||
events: {
|
||||
on: () => {},
|
||||
@ -31,7 +33,7 @@ describe('SingleStatCtrl', () => {
|
||||
describe(desc, () => {
|
||||
ctx.setup = setupFunc => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl = new SingleStatCtrl($scope, $injector, {});
|
||||
ctx.ctrl = new SingleStatCtrl($scope, $injector, {}, $sanitize);
|
||||
setupFunc();
|
||||
ctx.ctrl.onDataReceived(ctx.data);
|
||||
ctx.data = ctx.ctrl.data;
|
||||
|
@ -21,8 +21,6 @@ export interface DataSource {
|
||||
withCredentials: boolean;
|
||||
meta?: PluginMeta;
|
||||
pluginExports?: PluginExports;
|
||||
init?: () => void;
|
||||
testDatasource?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export interface DataSourceSelectItem {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user