Merge branch 'master' into react-query-editor
This commit is contained in:
@@ -367,7 +367,7 @@ jobs:
|
|||||||
command: './scripts/build/publish.sh --enterprise'
|
command: './scripts/build/publish.sh --enterprise'
|
||||||
- run:
|
- run:
|
||||||
name: Load GPG private key
|
name: Load GPG private key
|
||||||
comand: './scripts/build/load-signing-key.sh'
|
command: './scripts/build/load-signing-key.sh'
|
||||||
- run:
|
- run:
|
||||||
name: Update Debian repository
|
name: Update Debian repository
|
||||||
command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
|
command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
|
||||||
@@ -430,7 +430,7 @@ jobs:
|
|||||||
command: './scripts/build/publish.sh'
|
command: './scripts/build/publish.sh'
|
||||||
- run:
|
- run:
|
||||||
name: Load GPG private key
|
name: Load GPG private key
|
||||||
comand: './scripts/build/load-signing-key.sh'
|
command: './scripts/build/load-signing-key.sh'
|
||||||
- run:
|
- run:
|
||||||
name: Update Debian repository
|
name: Update Debian repository
|
||||||
command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
|
command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
|
||||||
|
|||||||
+19
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
|
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
|
||||||
|
* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
|
||||||
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
|
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
|
||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
@@ -11,17 +12,33 @@
|
|||||||
* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
|
* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
|
||||||
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
|
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
|
||||||
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
|
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
|
||||||
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
|
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK]req(https://github.com/IntegersOfK)
|
||||||
* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
|
* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
|
||||||
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
|
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
|
||||||
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
||||||
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
|
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
|
||||||
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
|
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
|
||||||
* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
|
* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
|
||||||
* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach)
|
* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
||||||
|
* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
|
||||||
|
|
||||||
|
# 5.4.3 (2019-01-14)
|
||||||
|
|
||||||
|
### Tech
|
||||||
|
|
||||||
|
* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach)
|
||||||
|
* **Backend**: Upgrade to golang 1.11.4 [#14580](https://github.com/grafana/grafana/issues/14580)
|
||||||
|
* **MySQL** only update session in mysql database when required [#14540](https://github.com/grafana/grafana/pull/14540)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
* **Alerting** Invalid frequency causes division by zero in alert scheduler [#14810](https://github.com/grafana/grafana/issues/14810)
|
||||||
|
* **Dashboard** Dashboard links do not update when time range changes [#14493](https://github.com/grafana/grafana/issues/14493)
|
||||||
|
* **Limits** Support more than 1000 datasources per org [#13883](https://github.com/grafana/grafana/issues/13883)
|
||||||
|
* **Backend** fix signed in user for orgId=0 result should return active org id [#14574](https://github.com/grafana/grafana/pull/14574)
|
||||||
|
* **Provisioning** Adds orgId to user dto for provisioned dashboards [#14678](https://github.com/grafana/grafana/pull/14678)
|
||||||
|
|
||||||
# 5.4.2 (2018-12-13)
|
# 5.4.2 (2018-12-13)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ providers:
|
|||||||
- name: 'gdev dashboards'
|
- name: 'gdev dashboards'
|
||||||
folder: 'gdev dashboards'
|
folder: 'gdev dashboards'
|
||||||
type: file
|
type: file
|
||||||
|
updateIntervalSeconds: 15
|
||||||
options:
|
options:
|
||||||
path: devenv/dev-dashboards
|
path: devenv/dev-dashboards
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,510 @@
|
|||||||
|
{
|
||||||
|
"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)",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"gnetId": null,
|
"gnetId": null,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"iteration": 1542304484522,
|
"iteration": 1545263815779,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"icon": "external link",
|
"icon": "external link",
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -168,6 +169,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -270,6 +272,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -372,6 +375,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -474,6 +478,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -576,6 +581,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2249,6 +2255,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2366,6 +2373,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2483,6 +2491,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2600,6 +2609,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2717,6 +2727,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2834,6 +2845,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -2951,6 +2963,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3068,6 +3081,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3185,6 +3199,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3302,6 +3317,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3419,6 +3435,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3536,6 +3553,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3667,6 +3685,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3780,6 +3799,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -3893,6 +3913,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4006,6 +4027,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4119,6 +4141,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4232,6 +4255,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4345,6 +4369,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4458,6 +4483,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4571,6 +4597,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4684,6 +4711,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4797,6 +4825,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -4910,6 +4939,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -5008,6 +5038,512 @@
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 4
|
"y": 4
|
||||||
},
|
},
|
||||||
|
"id": 60,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "$version_one",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 63,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * 1000"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * 1000 (version one) - interval auto",
|
||||||
|
"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": "$version_two",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 64,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * 1000"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * 1000 (version two) - interval auto",
|
||||||
|
"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": "$version_one",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 13
|
||||||
|
},
|
||||||
|
"id": 65,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "@value",
|
||||||
|
"hide": true,
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"settings": {},
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "4",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "var2",
|
||||||
|
"pipelineAgg": "3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * params.var2"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * avg (version one) - interval auto",
|
||||||
|
"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": "$version_two",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 13
|
||||||
|
},
|
||||||
|
"id": 66,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "@value",
|
||||||
|
"hide": true,
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"settings": {},
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "4",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "var2",
|
||||||
|
"pipelineAgg": "3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * params.var2"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * avg (version two) - interval auto",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Basic date histogram with bucket script aggregation",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsed": true,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
"id": 54,
|
"id": 54,
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
@@ -5042,6 +5578,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -5193,6 +5730,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@@ -5328,8 +5866,8 @@
|
|||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
"text": "gdev-elasticsearch-v2-metrics",
|
"text": "gdev-elasticsearch-v5-metrics",
|
||||||
"value": "gdev-elasticsearch-v2-metrics"
|
"value": "gdev-elasticsearch-v5-metrics"
|
||||||
},
|
},
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"label": "Version One",
|
"label": "Version One",
|
||||||
@@ -5343,8 +5881,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
"text": "gdev-elasticsearch-v5-metrics",
|
"text": "gdev-elasticsearch-v6-metrics",
|
||||||
"value": "gdev-elasticsearch-v5-metrics"
|
"value": "gdev-elasticsearch-v6-metrics"
|
||||||
},
|
},
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"label": "Version Two",
|
"label": "Version Two",
|
||||||
@@ -5359,7 +5897,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"from": "now-3h",
|
"from": "now-1h",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {
|
||||||
@@ -5390,5 +5928,5 @@
|
|||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Datasource tests - Elasticsearch comparison",
|
"title": "Datasource tests - Elasticsearch comparison",
|
||||||
"uid": "fuFWehBmk",
|
"uid": "fuFWehBmk",
|
||||||
"version": 10
|
"version": 4
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
+++
|
+++
|
||||||
title = "Explore"
|
title = "Explore"
|
||||||
|
keywords = ["explore", "loki", "logs"]
|
||||||
type = "docs"
|
type = "docs"
|
||||||
[menu.docs]
|
[menu.docs]
|
||||||
name = "Explore"
|
name = "Explore"
|
||||||
@@ -8,7 +9,11 @@ parent = "features"
|
|||||||
weight = 5
|
weight = 5
|
||||||
+++
|
+++
|
||||||
|
|
||||||
# Introduction
|
# Explore
|
||||||
|
|
||||||
|
> Explore is only available in Grafana 6.0 and above.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
One of the major new features of Grafana 6.0 is the new query-focused Explore workflow for troubleshooting and/or for data exploration.
|
One of the major new features of Grafana 6.0 is the new query-focused Explore workflow for troubleshooting and/or for data exploration.
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions
|
|||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||||
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"role": "Viewer",
|
"role": "Viewer",
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"stable": "5.4.2",
|
"stable": "5.4.3",
|
||||||
"testing": "5.4.2"
|
"testing": "5.4.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import 'vendor/spectrum';
|
import '../../vendor/spectrum';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
color: string;
|
color: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface Props {
|
|||||||
autoHide?: boolean;
|
autoHide?: boolean;
|
||||||
autoHideTimeout?: number;
|
autoHideTimeout?: number;
|
||||||
autoHideDuration?: number;
|
autoHideDuration?: number;
|
||||||
|
autoMaxHeight?: string;
|
||||||
hideTracksWhenNotNeeded?: boolean;
|
hideTracksWhenNotNeeded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,11 +19,12 @@ export class CustomScrollbar extends PureComponent<Props> {
|
|||||||
autoHide: true,
|
autoHide: true,
|
||||||
autoHideTimeout: 200,
|
autoHideTimeout: 200,
|
||||||
autoHideDuration: 200,
|
autoHideDuration: 200,
|
||||||
|
autoMaxHeight: '100%',
|
||||||
hideTracksWhenNotNeeded: false,
|
hideTracksWhenNotNeeded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { customClassName, children, ...scrollProps } = this.props;
|
const { customClassName, children, autoMaxHeight } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scrollbars
|
<Scrollbars
|
||||||
@@ -31,13 +33,12 @@ export class CustomScrollbar extends PureComponent<Props> {
|
|||||||
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||||
autoHeightMin={'0'}
|
autoHeightMin={'0'}
|
||||||
autoHeightMax={'100%'}
|
autoHeightMax={autoMaxHeight}
|
||||||
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
|
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
|
||||||
renderTrackVertical={props => <div {...props} className="track-vertical" />}
|
renderTrackVertical={props => <div {...props} className="track-vertical" />}
|
||||||
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
||||||
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
||||||
renderView={props => <div {...props} className="view" />}
|
renderView={props => <div {...props} className="view" />}
|
||||||
{...scrollProps}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Scrollbars>
|
</Scrollbars>
|
||||||
|
|||||||
-4
@@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"display": "none",
|
"display": "none",
|
||||||
"height": 6,
|
"height": 6,
|
||||||
"opacity": 0,
|
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"transition": "opacity 200ms",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
|||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"display": "none",
|
"display": "none",
|
||||||
"opacity": 0,
|
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"transition": "opacity 200ms",
|
|
||||||
"width": 6,
|
"width": 6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ export class Portal extends PureComponent<Props> {
|
|||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
const {
|
const { className, root = document.body } = this.props;
|
||||||
className,
|
|
||||||
root = document.body
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (className) {
|
if (className) {
|
||||||
this.node.classList.add(className);
|
this.node.classList.add(className);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ interface AsyncProps {
|
|||||||
export const MenuList = (props: any) => {
|
export const MenuList = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<components.MenuList {...props}>
|
<components.MenuList {...props}>
|
||||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
<CustomScrollbar autoHide={false} autoMaxHeight="inherit">{props.children}</CustomScrollbar>
|
||||||
</components.MenuList>
|
</components.MenuList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SelectOption from './SelectOption';
|
|||||||
import { OptionProps } from 'react-select/lib/components/Option';
|
import { OptionProps } from 'react-select/lib/components/Option';
|
||||||
|
|
||||||
const model: OptionProps<any> = {
|
const model: OptionProps<any> = {
|
||||||
|
data: jest.fn(),
|
||||||
cx: jest.fn(),
|
cx: jest.fn(),
|
||||||
clearValue: jest.fn(),
|
clearValue: jest.fn(),
|
||||||
getStyles: jest.fn(),
|
getStyles: jest.fn(),
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import React, { PureComponent } from 'react';
|
|||||||
import { GroupProps } from 'react-select/lib/components/Group';
|
import { GroupProps } from 'react-select/lib/components/Group';
|
||||||
|
|
||||||
interface ExtendedGroupProps extends GroupProps<any> {
|
interface ExtendedGroupProps extends GroupProps<any> {
|
||||||
data: any;
|
data: {
|
||||||
|
label: string;
|
||||||
|
expanded: boolean;
|
||||||
|
options: any[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -15,8 +19,10 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.selectProps) {
|
if (this.props.data.expanded) {
|
||||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
this.setState({ expanded: true });
|
||||||
|
} else if (this.props.selectProps && this.props.selectProps.value) {
|
||||||
|
const { value } = this.props.selectProps.value;
|
||||||
|
|
||||||
if (value && this.props.options.some(option => option.value === value)) {
|
if (value && this.props.options.some(option => option.value === value)) {
|
||||||
this.setState({ expanded: true });
|
this.setState({ expanded: true });
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
.gf-form-select-box__menu-list {
|
.gf-form-select-box__menu-list {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-filter .gf-form-select-box__menu {
|
.tag-filter .gf-form-select-box__menu {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||||
import { BasicGaugeColor } from '../../types';
|
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
@@ -15,49 +14,160 @@ const setup = (propOverrides?: object) => {
|
|||||||
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should add a base threshold if missing', () => {
|
||||||
|
const instance = setup();
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Add threshold', () => {
|
describe('Add threshold', () => {
|
||||||
it('should add threshold', () => {
|
it('should not add threshold at index 0', () => {
|
||||||
const instance = setup();
|
const instance = setup();
|
||||||
|
|
||||||
instance.onAddThreshold(0);
|
instance.onAddThreshold(0);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
|
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add another threshold above a first', () => {
|
it('should add threshold', () => {
|
||||||
const instance = setup({
|
const instance = setup();
|
||||||
thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
instance.onAddThreshold(1);
|
instance.onAddThreshold(1);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 1, value: 75, color: 'rgb(170, 95, 61)' },
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
{ index: 0, value: 50, color: 'rgb(127, 115, 64)' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add another threshold above a first', () => {
|
||||||
|
const instance = setup({
|
||||||
|
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onAddThreshold(2);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add another threshold between first and second index', () => {
|
||||||
|
const instance = setup({
|
||||||
|
thresholds: [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onAddThreshold(2);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 3, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 2, value: 62.5, color: '#EF843C' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Remove threshold', () => {
|
||||||
|
it('should not remove threshold at index 0', () => {
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
const instance = setup({ thresholds });
|
||||||
|
|
||||||
|
instance.onRemoveThreshold(thresholds[0]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual(thresholds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove threshold', () => {
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
const instance = setup({
|
||||||
|
thresholds,
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onRemoveThreshold(thresholds[1]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('change threshold value', () => {
|
describe('change threshold value', () => {
|
||||||
it('should update value and resort rows', () => {
|
it('should not change threshold at index 0', () => {
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
const instance = setup({ thresholds });
|
||||||
|
|
||||||
|
const mockEvent = { target: { value: 12 } };
|
||||||
|
|
||||||
|
instance.onChangeThresholdValue(mockEvent, thresholds[0]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual(thresholds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update value', () => {
|
||||||
const instance = setup();
|
const instance = setup();
|
||||||
const mockThresholds = [
|
const thresholds = [
|
||||||
{ index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
];
|
];
|
||||||
|
|
||||||
instance.state = {
|
instance.state = {
|
||||||
baseColor: BasicGaugeColor.Green,
|
thresholds,
|
||||||
thresholds: mockThresholds,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockEvent = { target: { value: 78 } };
|
const mockEvent = { target: { value: 78 } };
|
||||||
|
|
||||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
|
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
{ index: 1, value: 78, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on blur threshold value', () => {
|
||||||
|
it('should resort rows and update indexes', () => {
|
||||||
|
const instance = setup();
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 78, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
|
||||||
|
instance.state = {
|
||||||
|
thresholds,
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.onBlur();
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 2, value: 78, color: '#EAB839' },
|
||||||
|
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import tinycolor, { ColorInput } from 'tinycolor2';
|
// import tinycolor, { ColorInput } from 'tinycolor2';
|
||||||
|
|
||||||
import { Threshold, BasicGaugeColor } from '../../types';
|
import { Threshold } from '../../types';
|
||||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
import { colors } from '../../utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
@@ -12,50 +13,43 @@ export interface Props {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
baseColor: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
|
const thresholds: Threshold[] =
|
||||||
|
props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
|
||||||
|
this.state = { thresholds };
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddThreshold = (index: number) => {
|
onAddThreshold = (index: number) => {
|
||||||
const maxValue = 100; // hardcoded for now before we add the base threshold
|
|
||||||
const minValue = 0; // hardcoded for now before we add the base threshold
|
|
||||||
const { thresholds } = this.state;
|
const { thresholds } = this.state;
|
||||||
|
const maxValue = 100;
|
||||||
|
const minValue = 0;
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newThresholds = thresholds.map(threshold => {
|
const newThresholds = thresholds.map(threshold => {
|
||||||
if (threshold.index >= index) {
|
if (threshold.index >= index) {
|
||||||
threshold = {
|
const index = threshold.index + 1;
|
||||||
...threshold,
|
threshold = { ...threshold, index };
|
||||||
index: threshold.index + 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return threshold;
|
return threshold;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setting value to a value between the previous thresholds
|
// Setting value to a value between the previous thresholds
|
||||||
let value;
|
const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
|
||||||
|
const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
|
||||||
|
const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
|
||||||
|
const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
|
||||||
|
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
|
||||||
|
|
||||||
if (index === 0 && thresholds.length === 0) {
|
// Set a color
|
||||||
value = maxValue - (maxValue - minValue) / 2;
|
const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
|
||||||
} else if (index === 0 && thresholds.length > 0) {
|
|
||||||
value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
|
|
||||||
} else if (index > newThresholds[newThresholds.length - 1].index) {
|
|
||||||
value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a color that lies between the previous thresholds
|
|
||||||
let color;
|
|
||||||
if (index === 0 && thresholds.length === 0) {
|
|
||||||
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
|
|
||||||
} else {
|
|
||||||
color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
@@ -73,18 +67,40 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onRemoveThreshold = (threshold: Threshold) => {
|
onRemoveThreshold = (threshold: Threshold) => {
|
||||||
|
if (threshold.index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
|
prevState => {
|
||||||
|
const newThresholds = prevState.thresholds.map(t => {
|
||||||
|
if (t.index > threshold.index) {
|
||||||
|
const index = t.index - 1;
|
||||||
|
t = { ...t, index };
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
thresholds: newThresholds.filter(t => t !== threshold),
|
||||||
|
};
|
||||||
|
},
|
||||||
() => this.updateGauge()
|
() => this.updateGauge()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
||||||
|
if (threshold.index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { thresholds } = this.state;
|
const { thresholds } = this.state;
|
||||||
|
const parsedValue = parseInt(event.target.value, 10);
|
||||||
|
const value = isNaN(parsedValue) ? null : parsedValue;
|
||||||
|
|
||||||
const newThresholds = thresholds.map(t => {
|
const newThresholds = thresholds.map(t => {
|
||||||
if (t === threshold) {
|
if (t === threshold) {
|
||||||
t = { ...t, value: event.target.value };
|
t = { ...t, value: value as number };
|
||||||
}
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
@@ -114,7 +130,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
|
this.setState(prevState => {
|
||||||
|
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||||
|
let index = sortThresholds.length - 1;
|
||||||
|
sortThresholds.forEach(t => {
|
||||||
|
t.index = index--;
|
||||||
|
});
|
||||||
|
return { thresholds: sortThresholds };
|
||||||
|
});
|
||||||
|
|
||||||
this.updateGauge();
|
this.updateGauge();
|
||||||
};
|
};
|
||||||
@@ -129,92 +152,53 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderThresholds() {
|
renderInput = (threshold: Threshold) => {
|
||||||
const { thresholds } = this.state;
|
const value = threshold.index === 0 ? 'Base' : threshold.value;
|
||||||
|
|
||||||
return thresholds.map((threshold, index) => {
|
|
||||||
return (
|
|
||||||
<div className="threshold-row" key={`${threshold.index}-${index}`}>
|
|
||||||
<div className="threshold-row-inner">
|
|
||||||
<div className="threshold-row-color">
|
|
||||||
{threshold.color && (
|
|
||||||
<div className="threshold-row-color-inner">
|
|
||||||
<ColorPicker
|
|
||||||
color={threshold.color}
|
|
||||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="threshold-row-input"
|
|
||||||
type="text"
|
|
||||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
|
||||||
value={threshold.value}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
<div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
|
|
||||||
<i className="fa fa-times" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator() {
|
|
||||||
const { thresholds } = this.state;
|
|
||||||
|
|
||||||
return thresholds.map((t, i) => {
|
|
||||||
return (
|
|
||||||
<div key={`${t.value}-${i}`} className="indicator-section">
|
|
||||||
<div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
|
|
||||||
<div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBaseIndicator() {
|
|
||||||
return (
|
return (
|
||||||
<div className="indicator-section" style={{ height: '100%' }}>
|
<div className="thresholds-row-input-inner">
|
||||||
<div
|
<span className="thresholds-row-input-inner-arrow" />
|
||||||
onClick={() => this.onAddThreshold(0)}
|
<div className="thresholds-row-input-inner-color">
|
||||||
style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
|
{threshold.color && (
|
||||||
/>
|
<div className="thresholds-row-input-inner-color-colorpicker">
|
||||||
|
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="thresholds-row-input-inner-value">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||||
|
value={value}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
readOnly={threshold.index === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{threshold.index > 0 && (
|
||||||
|
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderBase() {
|
|
||||||
const baseColor = BasicGaugeColor.Green;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="threshold-row threshold-row-base">
|
|
||||||
<div className="threshold-row-inner threshold-row-inner--base">
|
|
||||||
<div className="threshold-row-color">
|
|
||||||
<div className="threshold-row-color-inner">
|
|
||||||
<ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="threshold-row-label">Base</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { thresholds } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelOptionsGroup title="Thresholds">
|
<PanelOptionsGroup title="Thresholds">
|
||||||
<div className="thresholds">
|
<div className="thresholds">
|
||||||
<div className="color-indicators">
|
{thresholds.map((threshold, index) => {
|
||||||
{this.renderIndicator()}
|
return (
|
||||||
{this.renderBaseIndicator()}
|
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||||
</div>
|
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
|
||||||
<div className="threshold-rows">
|
<i className="fa fa-plus" />
|
||||||
{this.renderThresholds()}
|
</div>
|
||||||
{this.renderBase()}
|
<div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
|
||||||
</div>
|
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</PanelOptionsGroup>
|
</PanelOptionsGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,46 +1,93 @@
|
|||||||
.thresholds {
|
.thresholds {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-rows {
|
.thresholds-row:last-child > .thresholds-row-color-indicator {
|
||||||
margin-left: 5px;
|
border-bottom-left-radius: $border-radius;
|
||||||
|
border-bottom-right-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-row {
|
.thresholds-row-add-button {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
color: $green;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
background-color: $green;
|
||||||
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 3px;
|
justify-content: center;
|
||||||
padding: 5px;
|
cursor: pointer;
|
||||||
|
|
||||||
&::before {
|
|
||||||
font-family: 'FontAwesome';
|
|
||||||
content: '\f0d9';
|
|
||||||
color: $input-label-border-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-row-inner {
|
.thresholds-row-add-button > i {
|
||||||
border: 1px solid $input-label-border-color;
|
color: $white;
|
||||||
border-radius: $border-radius;
|
}
|
||||||
|
|
||||||
|
.thresholds-row-color-indicator {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input {
|
||||||
|
margin-top: 49px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
justify-content: center;
|
||||||
height: 37px;
|
flex-direction: row;
|
||||||
|
height: 42px;
|
||||||
&--base {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-row-color {
|
.thresholds-row-input-inner > div {
|
||||||
width: 36px;
|
border-left: 1px solid $input-label-border-color;
|
||||||
border-right: 1px solid $input-label-border-color;
|
border-top: 1px solid $input-label-border-color;
|
||||||
|
border-bottom: 1px solid $input-label-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner > *:nth-child(2) {
|
||||||
|
border-top-left-radius: $border-radius;
|
||||||
|
border-bottom-left-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner > *:last-child {
|
||||||
|
border-top-right-radius: $border-radius;
|
||||||
|
border-bottom-right-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-arrow {
|
||||||
|
align-self: center;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid transparent;
|
||||||
|
border-right: 6px solid $input-label-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-value > input {
|
||||||
|
height: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-color {
|
||||||
|
width: 42px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: $input-bg;
|
background-color: $input-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-row-color-inner {
|
.thresholds-row-input-inner-color-colorpicker {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -48,56 +95,12 @@
|
|||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-row-input {
|
.thresholds-row-input-inner-remove {
|
||||||
padding: 8px 10px;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold-row-label {
|
|
||||||
background-color: $input-label-bg;
|
|
||||||
padding: 5px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold-row-add-label {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
padding: 5px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold-row-remove {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 37px;
|
height: 42px;
|
||||||
width: 37px;
|
width: 42px;
|
||||||
|
background-color: $input-label-border-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.threshold-row-add {
|
|
||||||
border-right: $border-width solid $input-label-border-color;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
background-color: $green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold-row-label {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-section {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-indicators {
|
|
||||||
width: 15px;
|
|
||||||
border-bottom-left-radius: $border-radius;
|
|
||||||
border-bottom-right-radius: $border-radius;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@
|
|||||||
@import 'Select/Select';
|
@import 'Select/Select';
|
||||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||||
|
@import 'ColorPicker/ColorPicker';
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
@import 'vendor/spectrum';
|
||||||
@import 'components/index';
|
@import 'components/index';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { TimeSeries, LoadingState } from './series';
|
import { TimeSeries, LoadingState } from './series';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
|
|
||||||
|
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
||||||
|
|
||||||
export interface PanelProps<T = any> {
|
export interface PanelProps<T = any> {
|
||||||
timeSeries: TimeSeries[];
|
timeSeries: TimeSeries[];
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
@@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
|
|||||||
renderCounter: number;
|
renderCounter: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
onInterpolate: InterpolateFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelOptionsProps<T = any> {
|
export interface PanelOptionsProps<T = any> {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error)
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger = log.New("imguploader")
|
||||||
|
)
|
||||||
|
|
||||||
func NewImageUploader() (ImageUploader, error) {
|
func NewImageUploader() (ImageUploader, error) {
|
||||||
|
|
||||||
switch setting.ImageUploadProvider {
|
switch setting.ImageUploadProvider {
|
||||||
@@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if setting.ImageUploadProvider != "" {
|
if setting.ImageUploadProvider != "" {
|
||||||
log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
logger.Error("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NopImageUploader{}, nil
|
return NopImageUploader{}, nil
|
||||||
|
|||||||
+2
-20
@@ -10,13 +10,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/ini.v1"
|
|
||||||
|
|
||||||
"github.com/go-stack/stack"
|
"github.com/go-stack/stack"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/inconshreveable/log15"
|
"github.com/inconshreveable/log15"
|
||||||
isatty "github.com/mattn/go-isatty"
|
isatty "github.com/mattn/go-isatty"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Root log15.Logger
|
var Root log15.Logger
|
||||||
@@ -58,10 +56,6 @@ func Debug(format string, v ...interface{}) {
|
|||||||
Root.Debug(message)
|
Root.Debug(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Debug2(message string, v ...interface{}) {
|
|
||||||
Root.Debug(message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Info(format string, v ...interface{}) {
|
func Info(format string, v ...interface{}) {
|
||||||
var message string
|
var message string
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
@@ -73,10 +67,6 @@ func Info(format string, v ...interface{}) {
|
|||||||
Root.Info(message)
|
Root.Info(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Info2(message string, v ...interface{}) {
|
|
||||||
Root.Info(message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Warn(format string, v ...interface{}) {
|
func Warn(format string, v ...interface{}) {
|
||||||
var message string
|
var message string
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
@@ -88,18 +78,10 @@ func Warn(format string, v ...interface{}) {
|
|||||||
Root.Warn(message)
|
Root.Warn(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Warn2(message string, v ...interface{}) {
|
|
||||||
Root.Warn(message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Error(skip int, format string, v ...interface{}) {
|
func Error(skip int, format string, v ...interface{}) {
|
||||||
Root.Error(fmt.Sprintf(format, v...))
|
Root.Error(fmt.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Error2(message string, v ...interface{}) {
|
|
||||||
Root.Error(message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Critical(skip int, format string, v ...interface{}) {
|
func Critical(skip int, format string, v ...interface{}) {
|
||||||
Root.Crit(fmt.Sprintf(format, v...))
|
Root.Crit(fmt.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ func init() {
|
|||||||
bus.AddHandler("auth", UpsertUser)
|
bus.AddHandler("auth", UpsertUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger = log.New("login.ext_user")
|
||||||
|
)
|
||||||
|
|
||||||
func UpsertUser(cmd *m.UpsertUserCommand) error {
|
func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||||
extUser := cmd.ExternalUser
|
extUser := cmd.ExternalUser
|
||||||
|
|
||||||
@@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd)
|
logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||||
return bus.Dispatch(updateCmd)
|
return bus.Dispatch(updateCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
|
|||||||
defer func() {
|
defer func() {
|
||||||
err := imageFile.Close()
|
err := imageFile.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error2("Could not close Telegram inline image.", "err", err)
|
this.log.Error("Could not close Telegram inline image.", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ type NotificationTestCommand struct {
|
|||||||
Settings *simplejson.Json
|
Settings *simplejson.Json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger = log.New("alerting.testnotification")
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
bus.AddHandler("alerting", handleNotificationTestCommand)
|
bus.AddHandler("alerting", handleNotificationTestCommand)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||||
@@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
|||||||
notifiers, err := InitNotifier(model)
|
notifiers, err := InitNotifier(model)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error2("Failed to create notifier", "error", err.Error())
|
logger.Error("Failed to create notifier", "error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dashboards
|
package dashboards
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -69,7 +70,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
|||||||
|
|
||||||
parsedDashboards, err := cr.parseConfigs(file)
|
parsedDashboards, err := cr.parseConfigs(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("could not parse provisioning config file: %s error: %v", file.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parsedDashboards) > 0 {
|
if len(parsedDashboards) > 0 {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
||||||
if fr.Cfg.DisableDeletion {
|
if fr.Cfg.DisableDeletion {
|
||||||
return
|
return
|
||||||
@@ -180,7 +181,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
|||||||
dash.Dashboard.SetId(provisionedData.DashboardId)
|
dash.Dashboard.SetId(provisionedData.DashboardId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fr.log.Debug("saving new dashboard", "file", path)
|
fr.log.Debug("saving new dashboard", "provisoner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId)
|
||||||
dp := &models.DashboardProvisioning{
|
dp := &models.DashboardProvisioning{
|
||||||
ExternalId: path,
|
ExternalId: path,
|
||||||
Name: fr.Cfg.Name,
|
Name: fr.Cfg.Name,
|
||||||
|
|||||||
@@ -166,6 +166,36 @@ func TestDashboardFileReader(t *testing.T) {
|
|||||||
_, err := NewDashboardFileReader(cfg, logger)
|
_, err := NewDashboardFileReader(cfg, logger)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Two dashboard providers should be able to provisioned the same dashboard without uid", func() {
|
||||||
|
cfg1 := &DashboardsAsConfig{Name: "1", Type: "file", OrgId: 1, Folder: "f1", Options: map[string]interface{}{"path": containingId}}
|
||||||
|
cfg2 := &DashboardsAsConfig{Name: "2", Type: "file", OrgId: 1, Folder: "f2", Options: map[string]interface{}{"path": containingId}}
|
||||||
|
|
||||||
|
reader1, err := NewDashboardFileReader(cfg1, logger)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = reader1.startWalkingDisk()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
reader2, err := NewDashboardFileReader(cfg2, logger)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = reader2.startWalkingDisk()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
var folderCount int
|
||||||
|
var dashCount int
|
||||||
|
for _, o := range fakeService.inserted {
|
||||||
|
if o.Dashboard.IsFolder {
|
||||||
|
folderCount++
|
||||||
|
} else {
|
||||||
|
dashCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
So(folderCount, ShouldEqual, 2)
|
||||||
|
So(dashCount, ShouldEqual, 2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should not create new folder if folder name is missing", func() {
|
Convey("Should not create new folder if folder name is missing", func() {
|
||||||
@@ -256,7 +286,9 @@ func (ffi FakeFileInfo) Sys() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
||||||
mock := fakeDashboardProvisioningService{}
|
mock := fakeDashboardProvisioningService{
|
||||||
|
provisioned: map[string][]*models.DashboardProvisioning{},
|
||||||
|
}
|
||||||
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
|
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
|
||||||
return &mock
|
return &mock
|
||||||
}
|
}
|
||||||
@@ -265,17 +297,26 @@ func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
|||||||
|
|
||||||
type fakeDashboardProvisioningService struct {
|
type fakeDashboardProvisioningService struct {
|
||||||
inserted []*dashboards.SaveDashboardDTO
|
inserted []*dashboards.SaveDashboardDTO
|
||||||
provisioned []*models.DashboardProvisioning
|
provisioned map[string][]*models.DashboardProvisioning
|
||||||
getDashboard []*models.Dashboard
|
getDashboard []*models.Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||||
return s.provisioned, nil
|
if _, ok := s.provisioned[name]; !ok {
|
||||||
|
s.provisioned[name] = []*models.DashboardProvisioning{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.provisioned[name], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||||
s.inserted = append(s.inserted, dto)
|
s.inserted = append(s.inserted, dto)
|
||||||
s.provisioned = append(s.provisioned, provisioning)
|
|
||||||
|
if _, ok := s.provisioned[provisioning.Name]; !ok {
|
||||||
|
s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], provisioning)
|
||||||
return dto.Dashboard, nil
|
return dto.Dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error
|
|||||||
func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
||||||
result := &models.DashboardProvisioning{}
|
result := &models.DashboardProvisioning{}
|
||||||
|
|
||||||
exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
|
exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ func (a *MetricAggregation) MarshalJSON() ([]byte, error) {
|
|||||||
|
|
||||||
// PipelineAggregation represents a metric aggregation
|
// PipelineAggregation represents a metric aggregation
|
||||||
type PipelineAggregation struct {
|
type PipelineAggregation struct {
|
||||||
BucketPath string
|
BucketPath interface{}
|
||||||
Settings map[string]interface{}
|
Settings map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ type AggBuilder interface {
|
|||||||
Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder
|
Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder
|
||||||
GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder
|
GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder
|
||||||
Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder
|
Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder
|
||||||
Pipeline(key, pipelineType, bucketPath string, fn func(a *PipelineAggregation)) AggBuilder
|
Pipeline(key, pipelineType string, bucketPath interface{}, fn func(a *PipelineAggregation)) AggBuilder
|
||||||
Build() (AggArray, error)
|
Build() (AggArray, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ func (b *aggBuilderImpl) Metric(key, metricType, field string, fn func(a *Metric
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *aggBuilderImpl) Pipeline(key, pipelineType, bucketPath string, fn func(a *PipelineAggregation)) AggBuilder {
|
func (b *aggBuilderImpl) Pipeline(key, pipelineType string, bucketPath interface{}, fn func(a *PipelineAggregation)) AggBuilder {
|
||||||
innerAgg := &PipelineAggregation{
|
innerAgg := &PipelineAggregation{
|
||||||
BucketPath: bucketPath,
|
BucketPath: bucketPath,
|
||||||
Settings: make(map[string]interface{}),
|
Settings: make(map[string]interface{}),
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ type BucketAgg struct {
|
|||||||
|
|
||||||
// MetricAgg represents a metric aggregation of the time series query model of the datasource
|
// MetricAgg represents a metric aggregation of the time series query model of the datasource
|
||||||
type MetricAgg struct {
|
type MetricAgg struct {
|
||||||
Field string `json:"field"`
|
Field string `json:"field"`
|
||||||
Hide bool `json:"hide"`
|
Hide bool `json:"hide"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
PipelineAggregate string `json:"pipelineAgg"`
|
PipelineAggregate string `json:"pipelineAgg"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
PipelineVariables map[string]string `json:"pipelineVariables"`
|
||||||
Meta *simplejson.Json `json:"meta"`
|
Settings *simplejson.Json `json:"settings"`
|
||||||
Type string `json:"type"`
|
Meta *simplejson.Json `json:"meta"`
|
||||||
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var metricAggType = map[string]string{
|
var metricAggType = map[string]string{
|
||||||
@@ -45,6 +46,7 @@ var metricAggType = map[string]string{
|
|||||||
"cardinality": "Unique Count",
|
"cardinality": "Unique Count",
|
||||||
"moving_avg": "Moving Average",
|
"moving_avg": "Moving Average",
|
||||||
"derivative": "Derivative",
|
"derivative": "Derivative",
|
||||||
|
"bucket_script": "Bucket Script",
|
||||||
"raw_document": "Raw Document",
|
"raw_document": "Raw Document",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +62,13 @@ var extendedStats = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pipelineAggType = map[string]string{
|
var pipelineAggType = map[string]string{
|
||||||
"moving_avg": "moving_avg",
|
"moving_avg": "moving_avg",
|
||||||
"derivative": "derivative",
|
"derivative": "derivative",
|
||||||
|
"bucket_script": "bucket_script",
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipelineAggWithMultipleBucketPathsType = map[string]string{
|
||||||
|
"bucket_script": "bucket_script",
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPipelineAgg(metricType string) bool {
|
func isPipelineAgg(metricType string) bool {
|
||||||
@@ -71,6 +78,13 @@ func isPipelineAgg(metricType string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPipelineAggWithMultipleBucketPaths(metricType string) bool {
|
||||||
|
if _, ok := pipelineAggWithMultipleBucketPathsType[metricType]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func describeMetric(metricType, field string) string {
|
func describeMetric(metricType, field string) string {
|
||||||
text := metricAggType[metricType]
|
text := metricAggType[metricType]
|
||||||
if metricType == countType {
|
if metricType == countType {
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
|
|||||||
|
|
||||||
newSeries.Tags["metric"] = metric.Type
|
newSeries.Tags["metric"] = metric.Type
|
||||||
newSeries.Tags["field"] = metric.Field
|
newSeries.Tags["field"] = metric.Field
|
||||||
|
newSeries.Tags["metricId"] = metric.ID
|
||||||
for _, v := range esAgg.Get("buckets").MustArray() {
|
for _, v := range esAgg.Get("buckets").MustArray() {
|
||||||
bucket := simplejson.NewFromAny(v)
|
bucket := simplejson.NewFromAny(v)
|
||||||
key := castToNullFloat(bucket.Get("key"))
|
key := castToNullFloat(bucket.Get("key"))
|
||||||
@@ -459,20 +460,42 @@ func (rp *responseParser) getSeriesName(series *tsdb.TimeSeries, target *Query,
|
|||||||
}
|
}
|
||||||
// todo, if field and pipelineAgg
|
// todo, if field and pipelineAgg
|
||||||
if field != "" && isPipelineAgg(metricType) {
|
if field != "" && isPipelineAgg(metricType) {
|
||||||
found := false
|
if isPipelineAggWithMultipleBucketPaths(metricType) {
|
||||||
for _, metric := range target.Metrics {
|
metricID := ""
|
||||||
if metric.ID == field {
|
if v, ok := series.Tags["metricId"]; ok {
|
||||||
metricName += " " + describeMetric(metric.Type, field)
|
metricID = v
|
||||||
found = true
|
}
|
||||||
|
|
||||||
|
for _, metric := range target.Metrics {
|
||||||
|
if metric.ID == metricID {
|
||||||
|
metricName = metric.Settings.Get("script").MustString()
|
||||||
|
for name, pipelineAgg := range metric.PipelineVariables {
|
||||||
|
for _, m := range target.Metrics {
|
||||||
|
if m.ID == pipelineAgg {
|
||||||
|
metricName = strings.Replace(metricName, "params."+name, describeMetric(m.Type, m.Field), -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
found := false
|
||||||
|
for _, metric := range target.Metrics {
|
||||||
|
if metric.ID == field {
|
||||||
|
metricName += " " + describeMetric(metric.Type, field)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
metricName = "Unset"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
metricName = "Unset"
|
|
||||||
}
|
}
|
||||||
} else if field != "" {
|
} else if field != "" {
|
||||||
metricName += " " + field
|
metricName += " " + field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(series.Tags, "metricId")
|
||||||
|
|
||||||
if len(series.Tags) == 0 {
|
if len(series.Tags) == 0 {
|
||||||
return metricName
|
return metricName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -787,6 +787,84 @@ func TestResponseParser(t *testing.T) {
|
|||||||
So(rows[0][2].(null.Float).Float64, ShouldEqual, 3000)
|
So(rows[0][2].(null.Float).Float64, ShouldEqual, 3000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("With bucket_script", func() {
|
||||||
|
targets := map[string]string{
|
||||||
|
"A": `{
|
||||||
|
"timeField": "@timestamp",
|
||||||
|
"metrics": [
|
||||||
|
{ "id": "1", "type": "sum", "field": "@value" },
|
||||||
|
{ "id": "3", "type": "max", "field": "@value" },
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"field": "select field",
|
||||||
|
"pipelineVariables": [{ "name": "var1", "pipelineAgg": "1" }, { "name": "var2", "pipelineAgg": "3" }],
|
||||||
|
"settings": { "script": "params.var1 * params.var2" },
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }]
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
response := `{
|
||||||
|
"responses": [
|
||||||
|
{
|
||||||
|
"aggregations": {
|
||||||
|
"2": {
|
||||||
|
"buckets": [
|
||||||
|
{
|
||||||
|
"1": { "value": 2 },
|
||||||
|
"3": { "value": 3 },
|
||||||
|
"4": { "value": 6 },
|
||||||
|
"doc_count": 60,
|
||||||
|
"key": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"1": { "value": 3 },
|
||||||
|
"3": { "value": 4 },
|
||||||
|
"4": { "value": 12 },
|
||||||
|
"doc_count": 60,
|
||||||
|
"key": 2000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
rp, err := newResponseParserForTest(targets, response)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
result, err := rp.getTimeSeries()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(result.Results, ShouldHaveLength, 1)
|
||||||
|
|
||||||
|
queryRes := result.Results["A"]
|
||||||
|
So(queryRes, ShouldNotBeNil)
|
||||||
|
So(queryRes.Series, ShouldHaveLength, 3)
|
||||||
|
seriesOne := queryRes.Series[0]
|
||||||
|
So(seriesOne.Name, ShouldEqual, "Sum @value")
|
||||||
|
So(seriesOne.Points, ShouldHaveLength, 2)
|
||||||
|
So(seriesOne.Points[0][0].Float64, ShouldEqual, 2)
|
||||||
|
So(seriesOne.Points[0][1].Float64, ShouldEqual, 1000)
|
||||||
|
So(seriesOne.Points[1][0].Float64, ShouldEqual, 3)
|
||||||
|
So(seriesOne.Points[1][1].Float64, ShouldEqual, 2000)
|
||||||
|
|
||||||
|
seriesTwo := queryRes.Series[1]
|
||||||
|
So(seriesTwo.Name, ShouldEqual, "Max @value")
|
||||||
|
So(seriesTwo.Points, ShouldHaveLength, 2)
|
||||||
|
So(seriesTwo.Points[0][0].Float64, ShouldEqual, 3)
|
||||||
|
So(seriesTwo.Points[0][1].Float64, ShouldEqual, 1000)
|
||||||
|
So(seriesTwo.Points[1][0].Float64, ShouldEqual, 4)
|
||||||
|
So(seriesTwo.Points[1][1].Float64, ShouldEqual, 2000)
|
||||||
|
|
||||||
|
seriesThree := queryRes.Series[2]
|
||||||
|
So(seriesThree.Name, ShouldEqual, "Sum @value * Max @value")
|
||||||
|
So(seriesThree.Points, ShouldHaveLength, 2)
|
||||||
|
So(seriesThree.Points[0][0].Float64, ShouldEqual, 6)
|
||||||
|
So(seriesThree.Points[0][1].Float64, ShouldEqual, 1000)
|
||||||
|
So(seriesThree.Points[1][0].Float64, ShouldEqual, 12)
|
||||||
|
So(seriesThree.Points[1][1].Float64, ShouldEqual, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
// Convey("Raw documents query", func() {
|
// Convey("Raw documents query", func() {
|
||||||
// targets := map[string]string{
|
// targets := map[string]string{
|
||||||
// "A": `{
|
// "A": `{
|
||||||
|
|||||||
@@ -94,26 +94,56 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isPipelineAgg(m.Type) {
|
if isPipelineAgg(m.Type) {
|
||||||
if _, err := strconv.Atoi(m.PipelineAggregate); err == nil {
|
if isPipelineAggWithMultipleBucketPaths(m.Type) {
|
||||||
var appliedAgg *MetricAgg
|
if len(m.PipelineVariables) > 0 {
|
||||||
for _, pipelineMetric := range q.Metrics {
|
bucketPaths := map[string]interface{}{}
|
||||||
if pipelineMetric.ID == m.PipelineAggregate {
|
for name, pipelineAgg := range m.PipelineVariables {
|
||||||
appliedAgg = pipelineMetric
|
if _, err := strconv.Atoi(pipelineAgg); err == nil {
|
||||||
break
|
var appliedAgg *MetricAgg
|
||||||
}
|
for _, pipelineMetric := range q.Metrics {
|
||||||
}
|
if pipelineMetric.ID == pipelineAgg {
|
||||||
if appliedAgg != nil {
|
appliedAgg = pipelineMetric
|
||||||
bucketPath := m.PipelineAggregate
|
break
|
||||||
if appliedAgg.Type == countType {
|
}
|
||||||
bucketPath = "_count"
|
}
|
||||||
|
if appliedAgg != nil {
|
||||||
|
if appliedAgg.Type == countType {
|
||||||
|
bucketPaths[name] = "_count"
|
||||||
|
} else {
|
||||||
|
bucketPaths[name] = pipelineAgg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) {
|
aggBuilder.Pipeline(m.ID, m.Type, bucketPaths, func(a *es.PipelineAggregation) {
|
||||||
a.Settings = m.Settings.MustMap()
|
a.Settings = m.Settings.MustMap()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
continue
|
if _, err := strconv.Atoi(m.PipelineAggregate); err == nil {
|
||||||
|
var appliedAgg *MetricAgg
|
||||||
|
for _, pipelineMetric := range q.Metrics {
|
||||||
|
if pipelineMetric.ID == m.PipelineAggregate {
|
||||||
|
appliedAgg = pipelineMetric
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if appliedAgg != nil {
|
||||||
|
bucketPath := m.PipelineAggregate
|
||||||
|
if appliedAgg.Type == countType {
|
||||||
|
bucketPath = "_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) {
|
||||||
|
a.Settings = m.Settings.MustMap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) {
|
aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) {
|
||||||
@@ -328,12 +358,20 @@ func (p *timeSeriesQueryParser) parseMetrics(model *simplejson.Json) ([]*MetricA
|
|||||||
metric.PipelineAggregate = metricJSON.Get("pipelineAgg").MustString()
|
metric.PipelineAggregate = metricJSON.Get("pipelineAgg").MustString()
|
||||||
metric.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap())
|
metric.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap())
|
||||||
metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap())
|
metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap())
|
||||||
|
|
||||||
metric.Type, err = metricJSON.Get("type").String()
|
metric.Type, err = metricJSON.Get("type").String()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPipelineAggWithMultipleBucketPaths(metric.Type) {
|
||||||
|
metric.PipelineVariables = map[string]string{}
|
||||||
|
pvArr := metricJSON.Get("pipelineVariables").MustArray()
|
||||||
|
for _, v := range pvArr {
|
||||||
|
kv := v.(map[string]interface{})
|
||||||
|
metric.PipelineVariables[kv["name"].(string)] = kv["pipelineAgg"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, metric)
|
result = append(result, metric)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -543,6 +543,77 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
|
|||||||
plAgg := derivativeAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
|
plAgg := derivativeAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
|
||||||
So(plAgg.BucketPath, ShouldEqual, "_count")
|
So(plAgg.BucketPath, ShouldEqual, "_count")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("With bucket_script", func() {
|
||||||
|
c := newFakeClient(5)
|
||||||
|
_, err := executeTsdbQuery(c, `{
|
||||||
|
"timeField": "@timestamp",
|
||||||
|
"bucketAggs": [
|
||||||
|
{ "type": "date_histogram", "field": "@timestamp", "id": "4" }
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{ "id": "3", "type": "sum", "field": "@value" },
|
||||||
|
{ "id": "5", "type": "max", "field": "@value" },
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "bucket_script",
|
||||||
|
"pipelineVariables": [
|
||||||
|
{ "name": "var1", "pipelineAgg": "3" },
|
||||||
|
{ "name": "var2", "pipelineAgg": "5" }
|
||||||
|
],
|
||||||
|
"settings": { "script": "params.var1 * params.var2" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, from, to, 15*time.Second)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
sr := c.multisearchRequests[0].Requests[0]
|
||||||
|
|
||||||
|
firstLevel := sr.Aggs[0]
|
||||||
|
So(firstLevel.Key, ShouldEqual, "4")
|
||||||
|
So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram")
|
||||||
|
|
||||||
|
bucketScriptAgg := firstLevel.Aggregation.Aggs[2]
|
||||||
|
So(bucketScriptAgg.Key, ShouldEqual, "2")
|
||||||
|
plAgg := bucketScriptAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
|
||||||
|
So(plAgg.BucketPath.(map[string]interface{}), ShouldResemble, map[string]interface{}{
|
||||||
|
"var1": "3",
|
||||||
|
"var2": "5",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("With bucket_script doc count", func() {
|
||||||
|
c := newFakeClient(5)
|
||||||
|
_, err := executeTsdbQuery(c, `{
|
||||||
|
"timeField": "@timestamp",
|
||||||
|
"bucketAggs": [
|
||||||
|
{ "type": "date_histogram", "field": "@timestamp", "id": "4" }
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{ "id": "3", "type": "count", "field": "select field" },
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "bucket_script",
|
||||||
|
"pipelineVariables": [
|
||||||
|
{ "name": "var1", "pipelineAgg": "3" }
|
||||||
|
],
|
||||||
|
"settings": { "script": "params.var1 * 1000" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, from, to, 15*time.Second)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
sr := c.multisearchRequests[0].Requests[0]
|
||||||
|
|
||||||
|
firstLevel := sr.Aggs[0]
|
||||||
|
So(firstLevel.Key, ShouldEqual, "4")
|
||||||
|
So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram")
|
||||||
|
|
||||||
|
bucketScriptAgg := firstLevel.Aggregation.Aggs[0]
|
||||||
|
So(bucketScriptAgg.Key, ShouldEqual, "2")
|
||||||
|
plAgg := bucketScriptAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
|
||||||
|
So(plAgg.BucketPath.(map[string]interface{}), ShouldResemble, map[string]interface{}{
|
||||||
|
"var1": "_count",
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
|
import { QueryEditor as StackdriverQueryEditor } from 'app/plugins/datasource/stackdriver/components/QueryEditor';
|
||||||
|
import { AnnotationQueryEditor as StackdriverAnnotationQueryEditor } from 'app/plugins/datasource/stackdriver/components/AnnotationQueryEditor';
|
||||||
import { PasswordStrength } from './components/PasswordStrength';
|
import { PasswordStrength } from './components/PasswordStrength';
|
||||||
import PageHeader from './components/PageHeader/PageHeader';
|
import PageHeader from './components/PageHeader/PageHeader';
|
||||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||||
import { SearchResult } from './components/search/SearchResult';
|
import { SearchResult } from './components/search/SearchResult';
|
||||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||||
|
import { MetricSelect } from './components/Select/MetricSelect';
|
||||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||||
import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
|
import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
|
||||||
|
|
||||||
@@ -29,4 +32,28 @@ export function registerAngularDirectives() {
|
|||||||
'onColorChange',
|
'onColorChange',
|
||||||
'onToggleAxis',
|
'onToggleAxis',
|
||||||
]);
|
]);
|
||||||
|
react2AngularDirective('metricSelect', MetricSelect, [
|
||||||
|
'options',
|
||||||
|
'onChange',
|
||||||
|
'value',
|
||||||
|
'isSearchable',
|
||||||
|
'className',
|
||||||
|
'placeholder',
|
||||||
|
['variables', { watchDepth: 'reference' }],
|
||||||
|
]);
|
||||||
|
react2AngularDirective('stackdriverQueryEditor', StackdriverQueryEditor, [
|
||||||
|
'target',
|
||||||
|
'onQueryChange',
|
||||||
|
'onExecuteQuery',
|
||||||
|
['events', { watchDepth: 'reference' }],
|
||||||
|
['datasource', { watchDepth: 'reference' }],
|
||||||
|
['templateSrv', { watchDepth: 'reference' }],
|
||||||
|
]);
|
||||||
|
react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [
|
||||||
|
'target',
|
||||||
|
'onQueryChange',
|
||||||
|
'onExecuteQuery',
|
||||||
|
['datasource', { watchDepth: 'reference' }],
|
||||||
|
['templateSrv', { watchDepth: 'reference' }],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { Select } from '@grafana/ui';
|
||||||
|
import { SelectOptionItem } from '@grafana/ui';
|
||||||
|
import { Variable } from 'app/types/templates';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
options: SelectOptionItem[];
|
||||||
|
isSearchable: boolean;
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
variables?: Variable[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
options: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetricSelect extends React.Component<Props, State> {
|
||||||
|
static defaultProps = {
|
||||||
|
variables: [],
|
||||||
|
options: [],
|
||||||
|
isSearchable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { options: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({ options: this.buildOptions(this.props) });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (nextProps.options.length > 0 || nextProps.variables.length) {
|
||||||
|
this.setState({ options: this.buildOptions(nextProps) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps: Props) {
|
||||||
|
const nextOptions = this.buildOptions(nextProps);
|
||||||
|
return nextProps.value !== this.props.value || !_.isEqual(nextOptions, this.state.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOptions({ variables = [], options }) {
|
||||||
|
return variables.length > 0 ? [this.getVariablesGroup(), ...options] : options;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariablesGroup() {
|
||||||
|
return {
|
||||||
|
label: 'Template Variables',
|
||||||
|
options: this.props.variables.map(v => ({
|
||||||
|
label: `$${v.name}`,
|
||||||
|
value: `$${v.name}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedOption() {
|
||||||
|
const { options } = this.state;
|
||||||
|
const allOptions = options.every(o => o.options) ? _.flatten(options.map(o => o.options)) : options;
|
||||||
|
return allOptions.find(option => option.value === this.props.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { placeholder, className, isSearchable, onChange } = this.props;
|
||||||
|
const { options } = this.state;
|
||||||
|
const selectedOption = this.getSelectedOption();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
isMulti={false}
|
||||||
|
isClearable={false}
|
||||||
|
backspaceRemovesValue={false}
|
||||||
|
onChange={item => onChange(item.value)}
|
||||||
|
options={options}
|
||||||
|
isSearchable={isSearchable}
|
||||||
|
maxMenuHeight={500}
|
||||||
|
placeholder={placeholder}
|
||||||
|
noOptionsMessage={() => 'No options found'}
|
||||||
|
value={selectedOption}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,6 +141,9 @@ export function dropdownTypeahead2($compile) {
|
|||||||
link: ($scope, elem, attrs) => {
|
link: ($scope, elem, attrs) => {
|
||||||
const $input = $(inputTemplate);
|
const $input = $(inputTemplate);
|
||||||
const $button = $(buttonTemplate);
|
const $button = $(buttonTemplate);
|
||||||
|
const timeoutId = {
|
||||||
|
blur: null
|
||||||
|
};
|
||||||
$input.appendTo(elem);
|
$input.appendTo(elem);
|
||||||
$button.appendTo(elem);
|
$button.appendTo(elem);
|
||||||
|
|
||||||
@@ -177,6 +180,14 @@ export function dropdownTypeahead2($compile) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeDropdownMenu = () => {
|
||||||
|
$input.hide();
|
||||||
|
$input.val('');
|
||||||
|
$button.show();
|
||||||
|
$button.focus();
|
||||||
|
elem.removeClass('open');
|
||||||
|
};
|
||||||
|
|
||||||
$scope.menuItemSelected = (index, subIndex) => {
|
$scope.menuItemSelected = (index, subIndex) => {
|
||||||
const menuItem = $scope.menuItems[index];
|
const menuItem = $scope.menuItems[index];
|
||||||
const payload: any = { $item: menuItem };
|
const payload: any = { $item: menuItem };
|
||||||
@@ -184,6 +195,7 @@ export function dropdownTypeahead2($compile) {
|
|||||||
payload.$subItem = menuItem.submenu[subIndex];
|
payload.$subItem = menuItem.submenu[subIndex];
|
||||||
}
|
}
|
||||||
$scope.dropdownTypeaheadOnSelect(payload);
|
$scope.dropdownTypeaheadOnSelect(payload);
|
||||||
|
closeDropdownMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
$input.attr('data-provide', 'typeahead');
|
$input.attr('data-provide', 'typeahead');
|
||||||
@@ -223,16 +235,15 @@ export function dropdownTypeahead2($compile) {
|
|||||||
elem.toggleClass('open', $input.val() === '');
|
elem.toggleClass('open', $input.val() === '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elem.mousedown((evt: Event) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
timeoutId.blur = null;
|
||||||
|
});
|
||||||
|
|
||||||
$input.blur(() => {
|
$input.blur(() => {
|
||||||
$input.hide();
|
timeoutId.blur = setTimeout(() => {
|
||||||
$input.val('');
|
closeDropdownMenu();
|
||||||
$button.show();
|
}, 1);
|
||||||
$button.focus();
|
|
||||||
// clicking the function dropdown menu won't
|
|
||||||
// work if you remove class at once
|
|
||||||
setTimeout(() => {
|
|
||||||
elem.removeClass('open');
|
|
||||||
}, 200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$compile(elem.contents())($scope);
|
$compile(elem.contents())($scope);
|
||||||
|
|||||||
@@ -6,26 +6,13 @@ import {
|
|||||||
clearHistory,
|
clearHistory,
|
||||||
hasNonEmptyQuery,
|
hasNonEmptyQuery,
|
||||||
} from './explore';
|
} from './explore';
|
||||||
import { ExploreState } from 'app/types/explore';
|
import { ExploreUrlState } from 'app/types/explore';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
|
|
||||||
const DEFAULT_EXPLORE_STATE: ExploreState = {
|
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||||
datasource: null,
|
datasource: null,
|
||||||
datasourceError: null,
|
queries: [],
|
||||||
datasourceLoading: null,
|
|
||||||
datasourceMissing: false,
|
|
||||||
exploreDatasources: [],
|
|
||||||
graphInterval: 1000,
|
|
||||||
history: [],
|
|
||||||
initialQueries: [],
|
|
||||||
queryTransactions: [],
|
|
||||||
range: DEFAULT_RANGE,
|
range: DEFAULT_RANGE,
|
||||||
showingGraph: true,
|
|
||||||
showingLogs: true,
|
|
||||||
showingTable: true,
|
|
||||||
supportsGraph: null,
|
|
||||||
supportsLogs: null,
|
|
||||||
supportsTable: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('state functions', () => {
|
describe('state functions', () => {
|
||||||
@@ -68,21 +55,19 @@ describe('state functions', () => {
|
|||||||
it('returns url parameter value for a state object', () => {
|
it('returns url parameter value for a state object', () => {
|
||||||
const state = {
|
const state = {
|
||||||
...DEFAULT_EXPLORE_STATE,
|
...DEFAULT_EXPLORE_STATE,
|
||||||
initialDatasource: 'foo',
|
datasource: 'foo',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
expr: 'metric{test="a/b"}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expr: 'super{foo="x/z"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
range: {
|
range: {
|
||||||
from: 'now-5h',
|
from: 'now-5h',
|
||||||
to: 'now',
|
to: 'now',
|
||||||
},
|
},
|
||||||
initialQueries: [
|
|
||||||
{
|
|
||||||
refId: '1',
|
|
||||||
expr: 'metric{test="a/b"}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refId: '2',
|
|
||||||
expr: 'super{foo="x/z"}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
expect(serializeStateToUrlParam(state)).toBe(
|
expect(serializeStateToUrlParam(state)).toBe(
|
||||||
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
||||||
@@ -93,21 +78,19 @@ describe('state functions', () => {
|
|||||||
it('returns url parameter value for a state object', () => {
|
it('returns url parameter value for a state object', () => {
|
||||||
const state = {
|
const state = {
|
||||||
...DEFAULT_EXPLORE_STATE,
|
...DEFAULT_EXPLORE_STATE,
|
||||||
initialDatasource: 'foo',
|
datasource: 'foo',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
expr: 'metric{test="a/b"}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expr: 'super{foo="x/z"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
range: {
|
range: {
|
||||||
from: 'now-5h',
|
from: 'now-5h',
|
||||||
to: 'now',
|
to: 'now',
|
||||||
},
|
},
|
||||||
initialQueries: [
|
|
||||||
{
|
|
||||||
refId: '1',
|
|
||||||
expr: 'metric{test="a/b"}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refId: '2',
|
|
||||||
expr: 'super{foo="x/z"}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
expect(serializeStateToUrlParam(state, true)).toBe(
|
expect(serializeStateToUrlParam(state, true)).toBe(
|
||||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
||||||
@@ -119,35 +102,24 @@ describe('state functions', () => {
|
|||||||
it('can parse the serialized state into the original state', () => {
|
it('can parse the serialized state into the original state', () => {
|
||||||
const state = {
|
const state = {
|
||||||
...DEFAULT_EXPLORE_STATE,
|
...DEFAULT_EXPLORE_STATE,
|
||||||
initialDatasource: 'foo',
|
datasource: 'foo',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
expr: 'metric{test="a/b"}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expr: 'super{foo="x/z"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
range: {
|
range: {
|
||||||
from: 'now - 5h',
|
from: 'now - 5h',
|
||||||
to: 'now',
|
to: 'now',
|
||||||
},
|
},
|
||||||
initialQueries: [
|
|
||||||
{
|
|
||||||
refId: '1',
|
|
||||||
expr: 'metric{test="a/b"}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refId: '2',
|
|
||||||
expr: 'super{foo="x/z"}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
const serialized = serializeStateToUrlParam(state);
|
const serialized = serializeStateToUrlParam(state);
|
||||||
const parsed = parseUrlState(serialized);
|
const parsed = parseUrlState(serialized);
|
||||||
|
|
||||||
// Account for datasource vs datasourceName
|
expect(state).toMatchObject(parsed);
|
||||||
const { datasource, queries, ...rest } = parsed;
|
|
||||||
const resultState = {
|
|
||||||
...rest,
|
|
||||||
datasource: DEFAULT_EXPLORE_STATE.datasource,
|
|
||||||
initialDatasource: datasource,
|
|
||||||
initialQueries: queries,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(state).toMatchObject(resultState);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { colors } from '@grafana/ui';
|
import { colors, RawTimeRange, IntervalValues } from '@grafana/ui';
|
||||||
|
|
||||||
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import { renderUrl } from 'app/core/utils/url';
|
import { renderUrl } from 'app/core/utils/url';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
@@ -8,9 +9,15 @@ import { parse as parseDate } from 'app/core/utils/datemath';
|
|||||||
|
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
|
import {
|
||||||
import { DataQuery, DataSourceApi } from 'app/types/series';
|
ExploreUrlState,
|
||||||
import { RawTimeRange, IntervalValues } from '@grafana/ui';
|
HistoryItem,
|
||||||
|
QueryTransaction,
|
||||||
|
ResultType,
|
||||||
|
QueryIntervals,
|
||||||
|
QueryOptions,
|
||||||
|
} from 'app/types/explore';
|
||||||
|
import { DataQuery } from 'app/types/series';
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
export const DEFAULT_RANGE = {
|
||||||
from: 'now-6h',
|
from: 'now-6h',
|
||||||
@@ -19,6 +26,8 @@ export const DEFAULT_RANGE = {
|
|||||||
|
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
|
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
|
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
|
||||||
*
|
*
|
||||||
@@ -77,7 +86,63 @@ export async function getExploreUrl(
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
export function buildQueryTransaction(
|
||||||
|
query: DataQuery,
|
||||||
|
rowIndex: number,
|
||||||
|
resultType: ResultType,
|
||||||
|
queryOptions: QueryOptions,
|
||||||
|
range: RawTimeRange,
|
||||||
|
queryIntervals: QueryIntervals,
|
||||||
|
scanning: boolean
|
||||||
|
): QueryTransaction {
|
||||||
|
const { interval, intervalMs } = queryIntervals;
|
||||||
|
|
||||||
|
const configuredQueries = [
|
||||||
|
{
|
||||||
|
...query,
|
||||||
|
...queryOptions,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Clone range for query request
|
||||||
|
// const queryRange: RawTimeRange = { ...range };
|
||||||
|
// const { from, to, raw } = this.timeSrv.timeRange();
|
||||||
|
// Most datasource is using `panelId + query.refId` for cancellation logic.
|
||||||
|
// Using `format` here because it relates to the view panel that the request is for.
|
||||||
|
// However, some datasources don't use `panelId + query.refId`, but only `panelId`.
|
||||||
|
// Therefore panel id has to be unique.
|
||||||
|
const panelId = `${queryOptions.format}-${query.key}`;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
interval,
|
||||||
|
intervalMs,
|
||||||
|
panelId,
|
||||||
|
targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
|
||||||
|
range: {
|
||||||
|
from: dateMath.parse(range.from, false),
|
||||||
|
to: dateMath.parse(range.to, true),
|
||||||
|
raw: range,
|
||||||
|
},
|
||||||
|
rangeRaw: range,
|
||||||
|
scopedVars: {
|
||||||
|
__interval: { text: interval, value: interval },
|
||||||
|
__interval_ms: { text: intervalMs, value: intervalMs },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
query,
|
||||||
|
resultType,
|
||||||
|
rowIndex,
|
||||||
|
scanning,
|
||||||
|
id: generateKey(), // reusing for unique ID
|
||||||
|
done: false,
|
||||||
|
latency: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||||
|
|
||||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||||
if (initial) {
|
if (initial) {
|
||||||
@@ -103,12 +168,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
const urlState: ExploreUrlState = {
|
|
||||||
datasource: state.initialDatasource,
|
|
||||||
queries: state.initialQueries.map(clearQueryKeys),
|
|
||||||
range: state.range,
|
|
||||||
};
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
||||||
}
|
}
|
||||||
@@ -123,7 +183,7 @@ export function generateRefId(index = 0): string {
|
|||||||
return `${index + 1}`;
|
return `${index + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateQueryKeys(index = 0): { refId: string; key: string } {
|
export function generateEmptyQuery(index = 0): { refId: string; key: string } {
|
||||||
return { refId: generateRefId(index), key: generateKey(index) };
|
return { refId: generateRefId(index), key: generateKey(index) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,20 +192,23 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } {
|
|||||||
*/
|
*/
|
||||||
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
|
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
|
||||||
if (queries && typeof queries === 'object' && queries.length > 0) {
|
if (queries && typeof queries === 'object' && queries.length > 0) {
|
||||||
return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
|
return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
|
||||||
}
|
}
|
||||||
return [{ ...generateQueryKeys() }];
|
return [{ ...generateEmptyQuery() }];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A target is non-empty when it has keys (with non-empty values) other than refId and key.
|
* A target is non-empty when it has keys (with non-empty values) other than refId and key.
|
||||||
*/
|
*/
|
||||||
export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
|
export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
|
||||||
return queries.some(
|
return (
|
||||||
query =>
|
queries &&
|
||||||
Object.keys(query)
|
queries.some(
|
||||||
.map(k => query[k])
|
query =>
|
||||||
.filter(v => v).length > 2
|
Object.keys(query)
|
||||||
|
.map(k => query[k])
|
||||||
|
.filter(v => v).length > 2
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,8 +243,8 @@ export function calculateResultsFromQueryTransactions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
|
export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
|
||||||
if (!datasource || !resolution) {
|
if (!resolution) {
|
||||||
return { interval: '1s', intervalMs: 1000 };
|
return { interval: '1s', intervalMs: 1000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +253,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res
|
|||||||
to: parseDate(range.to, true),
|
to: parseDate(range.to, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeTimeSeriesList(dataList) {
|
export function makeTimeSeriesList(dataList) {
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ import './alerting/NotificationsListCtrl';
|
|||||||
import './manage-dashboards';
|
import './manage-dashboards';
|
||||||
import './teams/CreateTeamCtrl';
|
import './teams/CreateTeamCtrl';
|
||||||
import './profile/all';
|
import './profile/all';
|
||||||
|
import './datasources/settings/HttpSettingsCtrl';
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { PanelPlugin } from 'app/types';
|
|||||||
import { TimeRange } from '@grafana/ui';
|
import { TimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
import variables from 'sass/_variables.scss';
|
import variables from 'sass/_variables.scss';
|
||||||
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -78,6 +79,10 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onInterpolate = (value: string, format?: string) => {
|
||||||
|
return templateSrv.replace(value, this.props.panel.scopedVars, format);
|
||||||
|
};
|
||||||
|
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||||
}
|
}
|
||||||
@@ -124,9 +129,10 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
timeSeries={timeSeries}
|
timeSeries={timeSeries}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||||
width={width - 2 * variables.panelHorizontalPadding }
|
width={width - 2 * variables.panelHorizontalPadding}
|
||||||
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
|
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
|
||||||
renderCounter={renderCounter}
|
renderCounter={renderCounter}
|
||||||
|
onInterpolate={this.onInterpolate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
import PanelHeaderCorner from './PanelHeaderCorner';
|
||||||
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
||||||
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||||
@@ -45,7 +46,9 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
const isFullscreen = false;
|
const isFullscreen = false;
|
||||||
const isLoading = false;
|
const isLoading = false;
|
||||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||||
const { panel, dashboard, timeInfo } = this.props;
|
const { panel, dashboard, timeInfo, scopedVars } = this.props;
|
||||||
|
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PanelHeaderCorner
|
<PanelHeaderCorner
|
||||||
@@ -65,7 +68,7 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
<div className="panel-title">
|
<div className="panel-title">
|
||||||
<span className="icon-gf panel-alert-icon" />
|
<span className="icon-gf panel-alert-icon" />
|
||||||
<span className="panel-title-text">
|
<span className="panel-title-text">
|
||||||
{panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
|
{title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{this.state.panelMenuOpen && (
|
{this.state.panelMenuOpen && (
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { coreModule } from 'app/core/core';
|
||||||
|
|
||||||
|
coreModule.directive('datasourceHttpSettings', () => {
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
current: '=',
|
||||||
|
suggestUrl: '@',
|
||||||
|
noDirectAccess: '@',
|
||||||
|
},
|
||||||
|
templateUrl: 'public/app/features/datasources/partials/http_settings.html',
|
||||||
|
link: {
|
||||||
|
pre: ($scope, elem, attrs) => {
|
||||||
|
// do not show access option if direct access is disabled
|
||||||
|
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
|
||||||
|
$scope.showAccessHelp = false;
|
||||||
|
$scope.toggleAccessHelp = () => {
|
||||||
|
$scope.showAccessHelp = !$scope.showAccessHelp;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getSuggestUrls = () => {
|
||||||
|
return [$scope.suggestUrl];
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
|
import { toggleGraph } from './state/actions';
|
||||||
|
import Graph from './Graph';
|
||||||
|
import Panel from './Panel';
|
||||||
|
|
||||||
|
interface GraphContainerProps {
|
||||||
|
onChangeTime: (range: TimeRange) => void;
|
||||||
|
exploreId: ExploreId;
|
||||||
|
graphResult?: any[];
|
||||||
|
loading: boolean;
|
||||||
|
range: RawTimeRange;
|
||||||
|
showingGraph: boolean;
|
||||||
|
showingTable: boolean;
|
||||||
|
split: boolean;
|
||||||
|
toggleGraph: typeof toggleGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphContainer extends PureComponent<GraphContainerProps> {
|
||||||
|
onClickGraphButton = () => {
|
||||||
|
this.props.toggleGraph(this.props.exploreId);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props;
|
||||||
|
const graphHeight = showingGraph && showingTable ? '200px' : '400px';
|
||||||
|
return (
|
||||||
|
<Panel label="Graph" isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
|
||||||
|
<Graph
|
||||||
|
data={graphResult}
|
||||||
|
height={graphHeight}
|
||||||
|
id={`explore-graph-${exploreId}`}
|
||||||
|
onChangeTime={onChangeTime}
|
||||||
|
range={range}
|
||||||
|
split={split}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: StoreState, { exploreId }) {
|
||||||
|
const explore = state.explore;
|
||||||
|
const { split } = explore;
|
||||||
|
const item: ExploreItemState = explore[exploreId];
|
||||||
|
const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
|
||||||
|
const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
|
||||||
|
return { graphResult, loading, range, showingGraph, showingTable, split };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleGraph,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));
|
||||||
@@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
|
|||||||
|
|
||||||
interface LogsProps {
|
interface LogsProps {
|
||||||
data: LogsModel;
|
data: LogsModel;
|
||||||
|
exploreId: string;
|
||||||
highlighterExpressions: string[];
|
highlighterExpressions: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
position: string;
|
|
||||||
range?: RawTimeRange;
|
range?: RawTimeRange;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
@@ -348,10 +348,10 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
|
exploreId,
|
||||||
highlighterExpressions,
|
highlighterExpressions,
|
||||||
loading = false,
|
loading = false,
|
||||||
onClickLabel,
|
onClickLabel,
|
||||||
position,
|
|
||||||
range,
|
range,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
scanRange,
|
||||||
@@ -400,7 +400,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
data={data.series}
|
data={data.series}
|
||||||
height="100px"
|
height="100px"
|
||||||
range={range}
|
range={range}
|
||||||
id={`explore-logs-graph-${position}`}
|
id={`explore-logs-graph-${exploreId}`}
|
||||||
onChangeTime={this.props.onChangeTime}
|
onChangeTime={this.props.onChangeTime}
|
||||||
onToggleSeries={this.onToggleLogLevel}
|
onToggleSeries={this.onToggleLogLevel}
|
||||||
userOptions={graphOptions}
|
userOptions={graphOptions}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
|
import { LogsModel } from 'app/core/logs_model';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
|
import { toggleLogs } from './state/actions';
|
||||||
|
import Logs from './Logs';
|
||||||
|
import Panel from './Panel';
|
||||||
|
|
||||||
|
interface LogsContainerProps {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
loading: boolean;
|
||||||
|
logsHighlighterExpressions?: string[];
|
||||||
|
logsResult?: LogsModel;
|
||||||
|
onChangeTime: (range: TimeRange) => void;
|
||||||
|
onClickLabel: (key: string, value: string) => void;
|
||||||
|
onStartScanning: () => void;
|
||||||
|
onStopScanning: () => void;
|
||||||
|
range: RawTimeRange;
|
||||||
|
scanning?: boolean;
|
||||||
|
scanRange?: RawTimeRange;
|
||||||
|
showingLogs: boolean;
|
||||||
|
toggleLogs: typeof toggleLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||||
|
onClickLogsButton = () => {
|
||||||
|
this.props.toggleLogs(this.props.exploreId);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
exploreId,
|
||||||
|
loading,
|
||||||
|
logsHighlighterExpressions,
|
||||||
|
logsResult,
|
||||||
|
onChangeTime,
|
||||||
|
onClickLabel,
|
||||||
|
onStartScanning,
|
||||||
|
onStopScanning,
|
||||||
|
range,
|
||||||
|
showingLogs,
|
||||||
|
scanning,
|
||||||
|
scanRange,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<Panel label="Logs" loading={loading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
|
||||||
|
<Logs
|
||||||
|
data={logsResult}
|
||||||
|
exploreId={exploreId}
|
||||||
|
key={logsResult.id}
|
||||||
|
highlighterExpressions={logsHighlighterExpressions}
|
||||||
|
loading={loading}
|
||||||
|
onChangeTime={onChangeTime}
|
||||||
|
onClickLabel={onClickLabel}
|
||||||
|
onStartScanning={onStartScanning}
|
||||||
|
onStopScanning={onStopScanning}
|
||||||
|
range={range}
|
||||||
|
scanning={scanning}
|
||||||
|
scanRange={scanRange}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: StoreState, { exploreId }) {
|
||||||
|
const explore = state.explore;
|
||||||
|
const item: ExploreItemState = explore[exploreId];
|
||||||
|
const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item;
|
||||||
|
const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
logsHighlighterExpressions,
|
||||||
|
logsResult,
|
||||||
|
scanning,
|
||||||
|
scanRange,
|
||||||
|
showingLogs,
|
||||||
|
range,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleLogs,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
|
||||||
@@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
|||||||
getNextQueryLetter: x => '',
|
getNextQueryLetter: x => '',
|
||||||
},
|
},
|
||||||
hideEditorRowActions: true,
|
hideEditorRowActions: true,
|
||||||
...getIntervals(range, datasource, null), // Possible to get resolution?
|
...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RawTimeRange } from '@grafana/ui';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore';
|
||||||
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
|
import { DataQuery, StoreState } from 'app/types';
|
||||||
|
|
||||||
|
// import DefaultQueryField from './QueryField';
|
||||||
|
import QueryEditor from './QueryEditor';
|
||||||
|
import QueryTransactionStatus from './QueryTransactionStatus';
|
||||||
|
import {
|
||||||
|
addQueryRow,
|
||||||
|
changeQuery,
|
||||||
|
highlightLogsExpression,
|
||||||
|
modifyQueries,
|
||||||
|
removeQueryRow,
|
||||||
|
runQueries,
|
||||||
|
} from './state/actions';
|
||||||
|
|
||||||
|
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||||
|
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||||
|
if (transaction) {
|
||||||
|
return transaction.hints[0];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryRowProps {
|
||||||
|
addQueryRow: typeof addQueryRow;
|
||||||
|
changeQuery: typeof changeQuery;
|
||||||
|
className?: string;
|
||||||
|
exploreId: ExploreId;
|
||||||
|
datasourceInstance: any;
|
||||||
|
highlightLogsExpression: typeof highlightLogsExpression;
|
||||||
|
history: HistoryItem[];
|
||||||
|
index: number;
|
||||||
|
initialQuery: DataQuery;
|
||||||
|
modifyQueries: typeof modifyQueries;
|
||||||
|
queryTransactions: QueryTransaction[];
|
||||||
|
exploreEvents: Emitter;
|
||||||
|
range: RawTimeRange;
|
||||||
|
removeQueryRow: typeof removeQueryRow;
|
||||||
|
runQueries: typeof runQueries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryRow extends PureComponent<QueryRowProps> {
|
||||||
|
onExecuteQuery = () => {
|
||||||
|
const { exploreId } = this.props;
|
||||||
|
this.props.runQueries(exploreId);
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeQuery = (query: DataQuery, override?: boolean) => {
|
||||||
|
const { datasourceInstance, exploreId, index } = this.props;
|
||||||
|
this.props.changeQuery(exploreId, query, index, override);
|
||||||
|
if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) {
|
||||||
|
// Live preview of log search matches. Only use on first row for now
|
||||||
|
this.updateLogsHighlights(query);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickAddButton = () => {
|
||||||
|
const { exploreId, index } = this.props;
|
||||||
|
this.props.addQueryRow(exploreId, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickClearButton = () => {
|
||||||
|
this.onChangeQuery(null, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickHintFix = action => {
|
||||||
|
const { datasourceInstance, exploreId, index } = this.props;
|
||||||
|
if (datasourceInstance && datasourceInstance.modifyQuery) {
|
||||||
|
const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
|
||||||
|
this.props.modifyQueries(exploreId, action, index, modifier);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickRemoveButton = () => {
|
||||||
|
const { exploreId, index } = this.props;
|
||||||
|
this.props.removeQueryRow(exploreId, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateLogsHighlights = _.debounce((value: DataQuery) => {
|
||||||
|
const { datasourceInstance } = this.props;
|
||||||
|
if (datasourceInstance.getHighlighterExpression) {
|
||||||
|
const expressions = [datasourceInstance.getHighlighterExpression(value)];
|
||||||
|
this.props.highlightLogsExpression(this.props.exploreId, expressions);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
|
||||||
|
const transactions = queryTransactions.filter(t => t.rowIndex === index);
|
||||||
|
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||||
|
const hint = getFirstHintFromTransactions(transactions);
|
||||||
|
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||||
|
const QueryField = datasourceInstance.pluginExports.ExploreQueryField;
|
||||||
|
return (
|
||||||
|
<div className="query-row">
|
||||||
|
<div className="query-row-status">
|
||||||
|
<QueryTransactionStatus transactions={transactions} />
|
||||||
|
</div>
|
||||||
|
<div className="query-row-field">
|
||||||
|
{QueryField ? (
|
||||||
|
<QueryField
|
||||||
|
datasource={datasourceInstance}
|
||||||
|
error={queryError}
|
||||||
|
hint={hint}
|
||||||
|
initialQuery={initialQuery}
|
||||||
|
history={history}
|
||||||
|
onClickHintFix={this.onClickHintFix}
|
||||||
|
onPressEnter={this.onExecuteQuery}
|
||||||
|
onQueryChange={this.onChangeQuery}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<QueryEditor
|
||||||
|
datasource={datasourceInstance}
|
||||||
|
error={queryError}
|
||||||
|
onQueryChange={this.onChangeQuery}
|
||||||
|
onExecuteQuery={this.onExecuteQuery}
|
||||||
|
initialQuery={initialQuery}
|
||||||
|
exploreEvents={exploreEvents}
|
||||||
|
range={range}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="query-row-tools">
|
||||||
|
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
|
||||||
|
<i className="fa fa-plus" />
|
||||||
|
</button>
|
||||||
|
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
|
||||||
|
<i className="fa fa-minus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: StoreState, { exploreId, index }) {
|
||||||
|
const explore = state.explore;
|
||||||
|
const item: ExploreItemState = explore[exploreId];
|
||||||
|
const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
|
||||||
|
const initialQuery = initialQueries[index];
|
||||||
|
return { datasourceInstance, history, initialQuery, queryTransactions, range };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
addQueryRow,
|
||||||
|
changeQuery,
|
||||||
|
highlightLogsExpression,
|
||||||
|
modifyQueries,
|
||||||
|
removeQueryRow,
|
||||||
|
runQueries,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));
|
||||||
@@ -1,159 +1,25 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore';
|
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
|
import { DataQuery } from 'app/types';
|
||||||
|
import { ExploreId } from 'app/types/explore';
|
||||||
|
|
||||||
// import DefaultQueryField from './QueryField';
|
import QueryRow from './QueryRow';
|
||||||
import QueryEditor from './QueryEditor';
|
|
||||||
import QueryTransactionStatus from './QueryTransactionStatus';
|
|
||||||
import { DataSource, DataQuery } from 'app/types';
|
|
||||||
import { RawTimeRange } from '@grafana/ui';
|
|
||||||
|
|
||||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
interface QueryRowsProps {
|
||||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
|
||||||
if (transaction) {
|
|
||||||
return transaction.hints[0];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QueryRowEventHandlers {
|
|
||||||
onAddQueryRow: (index: number) => void;
|
|
||||||
onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void;
|
|
||||||
onClickHintFix: (action: object, index?: number) => void;
|
|
||||||
onExecuteQuery: () => void;
|
|
||||||
onRemoveQueryRow: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QueryRowCommonProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
datasource: DataSource;
|
|
||||||
history: HistoryItem[];
|
|
||||||
transactions: QueryTransaction[];
|
|
||||||
exploreEvents: Emitter;
|
exploreEvents: Emitter;
|
||||||
range: RawTimeRange;
|
exploreId: ExploreId;
|
||||||
|
initialQueries: DataQuery[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryRowProps = QueryRowCommonProps &
|
|
||||||
QueryRowEventHandlers & {
|
|
||||||
index: number;
|
|
||||||
initialQuery: DataQuery;
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueryRow extends PureComponent<QueryRowProps> {
|
|
||||||
onExecuteQuery = () => {
|
|
||||||
const { onExecuteQuery } = this.props;
|
|
||||||
onExecuteQuery();
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeQuery = (value: DataQuery, override?: boolean) => {
|
|
||||||
const { index, onChangeQuery } = this.props;
|
|
||||||
if (onChangeQuery) {
|
|
||||||
onChangeQuery(value, index, override);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickAddButton = () => {
|
|
||||||
const { index, onAddQueryRow } = this.props;
|
|
||||||
if (onAddQueryRow) {
|
|
||||||
onAddQueryRow(index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickClearButton = () => {
|
|
||||||
this.onChangeQuery(null, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickHintFix = action => {
|
|
||||||
const { index, onClickHintFix } = this.props;
|
|
||||||
if (onClickHintFix) {
|
|
||||||
onClickHintFix(action, index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickRemoveButton = () => {
|
|
||||||
const { index, onRemoveQueryRow } = this.props;
|
|
||||||
if (onRemoveQueryRow) {
|
|
||||||
onRemoveQueryRow(index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPressEnter = () => {
|
|
||||||
const { onExecuteQuery } = this.props;
|
|
||||||
if (onExecuteQuery) {
|
|
||||||
onExecuteQuery();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props;
|
|
||||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
|
||||||
const hint = getFirstHintFromTransactions(transactions);
|
|
||||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
|
||||||
const QueryField = datasource.pluginExports.ExploreQueryField;
|
|
||||||
return (
|
|
||||||
<div className="query-row">
|
|
||||||
<div className="query-row-status">
|
|
||||||
<QueryTransactionStatus transactions={transactions} />
|
|
||||||
</div>
|
|
||||||
<div className="query-row-field">
|
|
||||||
{QueryField ? (
|
|
||||||
<QueryField
|
|
||||||
datasource={datasource}
|
|
||||||
error={queryError}
|
|
||||||
hint={hint}
|
|
||||||
initialQuery={initialQuery}
|
|
||||||
history={history}
|
|
||||||
onClickHintFix={this.onClickHintFix}
|
|
||||||
onPressEnter={this.onPressEnter}
|
|
||||||
onQueryChange={this.onChangeQuery}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<QueryEditor
|
|
||||||
datasource={datasource}
|
|
||||||
error={queryError}
|
|
||||||
onQueryChange={this.onChangeQuery}
|
|
||||||
onExecuteQuery={this.onExecuteQuery}
|
|
||||||
initialQuery={initialQuery}
|
|
||||||
exploreEvents={exploreEvents}
|
|
||||||
range={range}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="query-row-tools">
|
|
||||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
|
|
||||||
<i className="fa fa-times" />
|
|
||||||
</button>
|
|
||||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
|
|
||||||
<i className="fa fa-plus" />
|
|
||||||
</button>
|
|
||||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
|
|
||||||
<i className="fa fa-minus" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryRowsProps = QueryRowCommonProps &
|
|
||||||
QueryRowEventHandlers & {
|
|
||||||
initialQueries: DataQuery[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
||||||
render() {
|
render() {
|
||||||
const { className = '', initialQueries, transactions, ...handlers } = this.props;
|
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{initialQueries.map((query, index) => (
|
{initialQueries.map((query, index) => (
|
||||||
<QueryRow
|
// TODO instead of relying on initialQueries, move to react key list in redux
|
||||||
key={query.key}
|
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
|
||||||
index={index}
|
|
||||||
initialQuery={query}
|
|
||||||
transactions={transactions.filter(t => t.rowIndex === index)}
|
|
||||||
{...handlers}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
|
import { toggleGraph } from './state/actions';
|
||||||
|
import Table from './Table';
|
||||||
|
import Panel from './Panel';
|
||||||
|
import TableModel from 'app/core/table_model';
|
||||||
|
|
||||||
|
interface TableContainerProps {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
loading: boolean;
|
||||||
|
onClickCell: (key: string, value: string) => void;
|
||||||
|
showingTable: boolean;
|
||||||
|
tableResult?: TableModel;
|
||||||
|
toggleGraph: typeof toggleGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableContainer extends PureComponent<TableContainerProps> {
|
||||||
|
onClickTableButton = () => {
|
||||||
|
this.props.toggleGraph(this.props.exploreId);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, onClickCell, showingTable, tableResult } = this.props;
|
||||||
|
return (
|
||||||
|
<Panel label="Table" loading={loading} isOpen={showingTable} onToggle={this.onClickTableButton}>
|
||||||
|
<Table data={tableResult} loading={loading} onClickCell={onClickCell} />
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: StoreState, { exploreId }) {
|
||||||
|
const explore = state.explore;
|
||||||
|
const item: ExploreItemState = explore[exploreId];
|
||||||
|
const { queryTransactions, showingTable, tableResult } = item;
|
||||||
|
const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
|
||||||
|
return { loading, showingTable, tableResult };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleGraph,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
||||||
@@ -3,91 +3,56 @@ import { hot } from 'react-hot-loader';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { updateLocation } from 'app/core/actions';
|
import { updateLocation } from 'app/core/actions';
|
||||||
import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
|
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ExploreState } from 'app/types/explore';
|
import { ExploreId, ExploreUrlState } from 'app/types/explore';
|
||||||
|
import { parseUrlState } from 'app/core/utils/explore';
|
||||||
|
|
||||||
|
import { initializeExploreSplit } from './state/actions';
|
||||||
import ErrorBoundary from './ErrorBoundary';
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
import Explore from './Explore';
|
import Explore from './Explore';
|
||||||
|
|
||||||
interface WrapperProps {
|
interface WrapperProps {
|
||||||
backendSrv?: any;
|
initializeExploreSplit: typeof initializeExploreSplit;
|
||||||
datasourceSrv?: any;
|
split: boolean;
|
||||||
updateLocation: typeof updateLocation;
|
updateLocation: typeof updateLocation;
|
||||||
urlStates: { [key: string]: string };
|
urlStates: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WrapperState {
|
export class Wrapper extends Component<WrapperProps> {
|
||||||
split: boolean;
|
initialSplit: boolean;
|
||||||
splitState: ExploreState;
|
urlStates: { [key: string]: ExploreUrlState };
|
||||||
}
|
|
||||||
|
|
||||||
const STATE_KEY_LEFT = 'state';
|
|
||||||
const STATE_KEY_RIGHT = 'stateRight';
|
|
||||||
|
|
||||||
export class Wrapper extends Component<WrapperProps, WrapperState> {
|
|
||||||
urlStates: { [key: string]: string };
|
|
||||||
|
|
||||||
constructor(props: WrapperProps) {
|
constructor(props: WrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.urlStates = props.urlStates;
|
this.urlStates = {};
|
||||||
this.state = {
|
const { left, right } = props.urlStates;
|
||||||
split: Boolean(props.urlStates[STATE_KEY_RIGHT]),
|
if (props.urlStates.left) {
|
||||||
splitState: undefined,
|
this.urlStates.leftState = parseUrlState(left);
|
||||||
};
|
}
|
||||||
|
if (props.urlStates.right) {
|
||||||
|
this.urlStates.rightState = parseUrlState(right);
|
||||||
|
this.initialSplit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeSplit = (split: boolean, splitState: ExploreState) => {
|
componentDidMount() {
|
||||||
this.setState({ split, splitState });
|
if (this.initialSplit) {
|
||||||
// When closing split, remove URL state for split part
|
this.props.initializeExploreSplit();
|
||||||
if (!split) {
|
|
||||||
delete this.urlStates[STATE_KEY_RIGHT];
|
|
||||||
this.props.updateLocation({
|
|
||||||
query: this.urlStates,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onSaveState = (key: string, state: ExploreState) => {
|
|
||||||
const urlState = serializeStateToUrlParam(state, true);
|
|
||||||
this.urlStates[key] = urlState;
|
|
||||||
this.props.updateLocation({
|
|
||||||
query: this.urlStates,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { datasourceSrv } = this.props;
|
const { split } = this.props;
|
||||||
// State overrides for props from first Explore
|
const { leftState, rightState } = this.urlStates;
|
||||||
const { split, splitState } = this.state;
|
|
||||||
const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]);
|
|
||||||
const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="explore-wrapper">
|
<div className="explore-wrapper">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Explore
|
<Explore exploreId={ExploreId.left} urlState={leftState} />
|
||||||
datasourceSrv={datasourceSrv}
|
|
||||||
onChangeSplit={this.onChangeSplit}
|
|
||||||
onSaveState={this.onSaveState}
|
|
||||||
position="left"
|
|
||||||
split={split}
|
|
||||||
stateKey={STATE_KEY_LEFT}
|
|
||||||
urlState={urlStateLeft}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
{split && (
|
{split && (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Explore
|
<Explore exploreId={ExploreId.right} urlState={rightState} />
|
||||||
datasourceSrv={datasourceSrv}
|
|
||||||
onChangeSplit={this.onChangeSplit}
|
|
||||||
onSaveState={this.onSaveState}
|
|
||||||
position="right"
|
|
||||||
split={split}
|
|
||||||
splitState={splitState}
|
|
||||||
stateKey={STATE_KEY_RIGHT}
|
|
||||||
urlState={urlStateRight}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -95,11 +60,14 @@ export class Wrapper extends Component<WrapperProps, WrapperState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => {
|
||||||
urlStates: state.location.query,
|
const urlStates = state.location.query;
|
||||||
});
|
const { split } = state.explore;
|
||||||
|
return { split, urlStates };
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
initializeExploreSplit,
|
||||||
updateLocation,
|
updateLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { Emitter } from 'app/core/core';
|
||||||
|
import {
|
||||||
|
ExploreId,
|
||||||
|
ExploreItemState,
|
||||||
|
HistoryItem,
|
||||||
|
RangeScanner,
|
||||||
|
ResultType,
|
||||||
|
QueryTransaction,
|
||||||
|
} from 'app/types/explore';
|
||||||
|
import { DataSourceSelectItem } from 'app/types/datasources';
|
||||||
|
import { DataQuery } from 'app/types';
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
AddQueryRow = 'explore/ADD_QUERY_ROW',
|
||||||
|
ChangeDatasource = 'explore/CHANGE_DATASOURCE',
|
||||||
|
ChangeQuery = 'explore/CHANGE_QUERY',
|
||||||
|
ChangeSize = 'explore/CHANGE_SIZE',
|
||||||
|
ChangeTime = 'explore/CHANGE_TIME',
|
||||||
|
ClearQueries = 'explore/CLEAR_QUERIES',
|
||||||
|
HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
|
||||||
|
InitializeExplore = 'explore/INITIALIZE_EXPLORE',
|
||||||
|
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
|
||||||
|
LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
|
||||||
|
LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
|
||||||
|
LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
|
||||||
|
LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
|
||||||
|
ModifyQueries = 'explore/MODIFY_QUERIES',
|
||||||
|
QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
|
||||||
|
QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
|
||||||
|
QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
|
||||||
|
RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
|
||||||
|
RunQueries = 'explore/RUN_QUERIES',
|
||||||
|
RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
|
||||||
|
ScanRange = 'explore/SCAN_RANGE',
|
||||||
|
ScanStart = 'explore/SCAN_START',
|
||||||
|
ScanStop = 'explore/SCAN_STOP',
|
||||||
|
SetQueries = 'explore/SET_QUERIES',
|
||||||
|
SplitClose = 'explore/SPLIT_CLOSE',
|
||||||
|
SplitOpen = 'explore/SPLIT_OPEN',
|
||||||
|
StateSave = 'explore/STATE_SAVE',
|
||||||
|
ToggleGraph = 'explore/TOGGLE_GRAPH',
|
||||||
|
ToggleLogs = 'explore/TOGGLE_LOGS',
|
||||||
|
ToggleTable = 'explore/TOGGLE_TABLE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddQueryRowAction {
|
||||||
|
type: ActionTypes.AddQueryRow;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
index: number;
|
||||||
|
query: DataQuery;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeQueryAction {
|
||||||
|
type: ActionTypes.ChangeQuery;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
query: DataQuery;
|
||||||
|
index: number;
|
||||||
|
override: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeSizeAction {
|
||||||
|
type: ActionTypes.ChangeSize;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeTimeAction {
|
||||||
|
type: ActionTypes.ChangeTime;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
range: TimeRange;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClearQueriesAction {
|
||||||
|
type: ActionTypes.ClearQueries;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightLogsExpressionAction {
|
||||||
|
type: ActionTypes.HighlightLogsExpression;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
expressions: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializeExploreAction {
|
||||||
|
type: ActionTypes.InitializeExplore;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
containerWidth: number;
|
||||||
|
datasource: string;
|
||||||
|
eventBridge: Emitter;
|
||||||
|
exploreDatasources: DataSourceSelectItem[];
|
||||||
|
queries: DataQuery[];
|
||||||
|
range: RawTimeRange;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializeExploreSplitAction {
|
||||||
|
type: ActionTypes.InitializeExploreSplit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadDatasourceFailureAction {
|
||||||
|
type: ActionTypes.LoadDatasourceFailure;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadDatasourcePendingAction {
|
||||||
|
type: ActionTypes.LoadDatasourcePending;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
datasourceId: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadDatasourceMissingAction {
|
||||||
|
type: ActionTypes.LoadDatasourceMissing;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadDatasourceSuccessAction {
|
||||||
|
type: ActionTypes.LoadDatasourceSuccess;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
StartPage?: any;
|
||||||
|
datasourceInstance: any;
|
||||||
|
history: HistoryItem[];
|
||||||
|
initialDatasource: string;
|
||||||
|
initialQueries: DataQuery[];
|
||||||
|
logsHighlighterExpressions?: any[];
|
||||||
|
showingStartPage: boolean;
|
||||||
|
supportsGraph: boolean;
|
||||||
|
supportsLogs: boolean;
|
||||||
|
supportsTable: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModifyQueriesAction {
|
||||||
|
type: ActionTypes.ModifyQueries;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
modification: any;
|
||||||
|
index: number;
|
||||||
|
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryTransactionFailureAction {
|
||||||
|
type: ActionTypes.QueryTransactionFailure;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
queryTransactions: QueryTransaction[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryTransactionStartAction {
|
||||||
|
type: ActionTypes.QueryTransactionStart;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
resultType: ResultType;
|
||||||
|
rowIndex: number;
|
||||||
|
transaction: QueryTransaction;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryTransactionSuccessAction {
|
||||||
|
type: ActionTypes.QueryTransactionSuccess;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
history: HistoryItem[];
|
||||||
|
queryTransactions: QueryTransaction[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveQueryRowAction {
|
||||||
|
type: ActionTypes.RemoveQueryRow;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunQueriesEmptyAction {
|
||||||
|
type: ActionTypes.RunQueriesEmpty;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScanStartAction {
|
||||||
|
type: ActionTypes.ScanStart;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
scanner: RangeScanner;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScanRangeAction {
|
||||||
|
type: ActionTypes.ScanRange;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
range: RawTimeRange;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScanStopAction {
|
||||||
|
type: ActionTypes.ScanStop;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetQueriesAction {
|
||||||
|
type: ActionTypes.SetQueries;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
queries: DataQuery[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitCloseAction {
|
||||||
|
type: ActionTypes.SplitClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitOpenAction {
|
||||||
|
type: ActionTypes.SplitOpen;
|
||||||
|
payload: {
|
||||||
|
itemState: ExploreItemState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateSaveAction {
|
||||||
|
type: ActionTypes.StateSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleTableAction {
|
||||||
|
type: ActionTypes.ToggleTable;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleGraphAction {
|
||||||
|
type: ActionTypes.ToggleGraph;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleLogsAction {
|
||||||
|
type: ActionTypes.ToggleLogs;
|
||||||
|
payload: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| AddQueryRowAction
|
||||||
|
| ChangeQueryAction
|
||||||
|
| ChangeSizeAction
|
||||||
|
| ChangeTimeAction
|
||||||
|
| ClearQueriesAction
|
||||||
|
| HighlightLogsExpressionAction
|
||||||
|
| InitializeExploreAction
|
||||||
|
| InitializeExploreSplitAction
|
||||||
|
| LoadDatasourceFailureAction
|
||||||
|
| LoadDatasourceMissingAction
|
||||||
|
| LoadDatasourcePendingAction
|
||||||
|
| LoadDatasourceSuccessAction
|
||||||
|
| ModifyQueriesAction
|
||||||
|
| QueryTransactionFailureAction
|
||||||
|
| QueryTransactionStartAction
|
||||||
|
| QueryTransactionSuccessAction
|
||||||
|
| RemoveQueryRowAction
|
||||||
|
| RunQueriesEmptyAction
|
||||||
|
| ScanRangeAction
|
||||||
|
| ScanStartAction
|
||||||
|
| ScanStopAction
|
||||||
|
| SetQueriesAction
|
||||||
|
| SplitCloseAction
|
||||||
|
| SplitOpenAction
|
||||||
|
| ToggleGraphAction
|
||||||
|
| ToggleLogsAction
|
||||||
|
| ToggleTableAction;
|
||||||
@@ -0,0 +1,757 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LAST_USED_DATASOURCE_KEY,
|
||||||
|
clearQueryKeys,
|
||||||
|
ensureQueries,
|
||||||
|
generateEmptyQuery,
|
||||||
|
hasNonEmptyQuery,
|
||||||
|
makeTimeSeriesList,
|
||||||
|
updateHistory,
|
||||||
|
buildQueryTransaction,
|
||||||
|
serializeStateToUrlParam,
|
||||||
|
} from 'app/core/utils/explore';
|
||||||
|
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
import store from 'app/core/store';
|
||||||
|
import { DataSourceSelectItem } from 'app/types/datasources';
|
||||||
|
import { DataQuery, StoreState } from 'app/types';
|
||||||
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
import {
|
||||||
|
ExploreId,
|
||||||
|
ExploreUrlState,
|
||||||
|
RangeScanner,
|
||||||
|
ResultType,
|
||||||
|
QueryOptions,
|
||||||
|
QueryTransaction,
|
||||||
|
QueryHint,
|
||||||
|
QueryHintGetter,
|
||||||
|
} from 'app/types/explore';
|
||||||
|
import { Emitter } from 'app/core/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Action as ThunkableAction,
|
||||||
|
ActionTypes,
|
||||||
|
AddQueryRowAction,
|
||||||
|
ChangeSizeAction,
|
||||||
|
HighlightLogsExpressionAction,
|
||||||
|
LoadDatasourceFailureAction,
|
||||||
|
LoadDatasourceMissingAction,
|
||||||
|
LoadDatasourcePendingAction,
|
||||||
|
LoadDatasourceSuccessAction,
|
||||||
|
QueryTransactionStartAction,
|
||||||
|
ScanStopAction,
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a query row after the row with the given index.
|
||||||
|
*/
|
||||||
|
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
|
||||||
|
const query = generateEmptyQuery(index + 1);
|
||||||
|
return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a new datasource identified by the given name.
|
||||||
|
*/
|
||||||
|
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const instance = await getDatasourceSrv().get(datasource);
|
||||||
|
dispatch(loadDatasource(exploreId, instance));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query change handler for the query row with the given index.
|
||||||
|
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
|
||||||
|
*/
|
||||||
|
export function changeQuery(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
query: DataQuery,
|
||||||
|
index: number,
|
||||||
|
override: boolean
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
// Null query means reset
|
||||||
|
if (query === null) {
|
||||||
|
query = { ...generateEmptyQuery(index) };
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
|
||||||
|
if (override) {
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of the Explore container size, in particular the width.
|
||||||
|
* The width will be used to calculate graph intervals (number of datapoints).
|
||||||
|
*/
|
||||||
|
export function changeSize(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
{ height, width }: { height: number; width: number }
|
||||||
|
): ChangeSizeAction {
|
||||||
|
return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
||||||
|
*/
|
||||||
|
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all queries and results.
|
||||||
|
*/
|
||||||
|
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(scanStop(exploreId));
|
||||||
|
dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
|
||||||
|
dispatch(stateSave());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight expressions in the log results
|
||||||
|
*/
|
||||||
|
export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
|
||||||
|
return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Explore state with state from the URL and the React component.
|
||||||
|
* Call this only on components for with the Explore state has not been initialized.
|
||||||
|
*/
|
||||||
|
export function initializeExplore(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
datasource: string,
|
||||||
|
queries: DataQuery[],
|
||||||
|
range: RawTimeRange,
|
||||||
|
containerWidth: number,
|
||||||
|
eventBridge: Emitter
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||||
|
.getExternal()
|
||||||
|
.map(ds => ({
|
||||||
|
value: ds.name,
|
||||||
|
name: ds.name,
|
||||||
|
meta: ds.meta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.InitializeExplore,
|
||||||
|
payload: {
|
||||||
|
exploreId,
|
||||||
|
containerWidth,
|
||||||
|
datasource,
|
||||||
|
eventBridge,
|
||||||
|
exploreDatasources,
|
||||||
|
queries,
|
||||||
|
range,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exploreDatasources.length > 1) {
|
||||||
|
let instance;
|
||||||
|
if (datasource) {
|
||||||
|
instance = await getDatasourceSrv().get(datasource);
|
||||||
|
} else {
|
||||||
|
instance = await getDatasourceSrv().get();
|
||||||
|
}
|
||||||
|
dispatch(loadDatasource(exploreId, instance));
|
||||||
|
} else {
|
||||||
|
dispatch(loadDatasourceMissing(exploreId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the wrapper split state
|
||||||
|
*/
|
||||||
|
export function initializeExploreSplit() {
|
||||||
|
return async dispatch => {
|
||||||
|
dispatch({ type: ActionTypes.InitializeExploreSplit });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an error that happened during the selection of a datasource
|
||||||
|
*/
|
||||||
|
export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
|
||||||
|
type: ActionTypes.LoadDatasourceFailure,
|
||||||
|
payload: {
|
||||||
|
exploreId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an error when no datasources have been configured
|
||||||
|
*/
|
||||||
|
export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
|
||||||
|
type: ActionTypes.LoadDatasourceMissing,
|
||||||
|
payload: { exploreId },
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the async process of loading a datasource to display a loading indicator
|
||||||
|
*/
|
||||||
|
export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({
|
||||||
|
type: ActionTypes.LoadDatasourcePending,
|
||||||
|
payload: {
|
||||||
|
exploreId,
|
||||||
|
datasourceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
|
||||||
|
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
||||||
|
* e.g., Prometheus -> Loki queries.
|
||||||
|
*/
|
||||||
|
export const loadDatasourceSuccess = (
|
||||||
|
exploreId: ExploreId,
|
||||||
|
instance: any,
|
||||||
|
queries: DataQuery[]
|
||||||
|
): LoadDatasourceSuccessAction => {
|
||||||
|
// Capabilities
|
||||||
|
const supportsGraph = instance.meta.metrics;
|
||||||
|
const supportsLogs = instance.meta.logs;
|
||||||
|
const supportsTable = instance.meta.tables;
|
||||||
|
// Custom components
|
||||||
|
const StartPage = instance.pluginExports.ExploreStartPage;
|
||||||
|
|
||||||
|
const historyKey = `grafana.explore.history.${instance.meta.id}`;
|
||||||
|
const history = store.getObject(historyKey, []);
|
||||||
|
// Save last-used datasource
|
||||||
|
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: ActionTypes.LoadDatasourceSuccess,
|
||||||
|
payload: {
|
||||||
|
exploreId,
|
||||||
|
StartPage,
|
||||||
|
datasourceInstance: instance,
|
||||||
|
history,
|
||||||
|
initialDatasource: instance.name,
|
||||||
|
initialQueries: queries,
|
||||||
|
showingStartPage: Boolean(StartPage),
|
||||||
|
supportsGraph,
|
||||||
|
supportsLogs,
|
||||||
|
supportsTable,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
|
||||||
|
*/
|
||||||
|
export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult<void> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const datasourceId = instance.meta.id;
|
||||||
|
|
||||||
|
// Keep ID to track selection
|
||||||
|
dispatch(loadDatasourcePending(exploreId, datasourceId));
|
||||||
|
|
||||||
|
let datasourceError = null;
|
||||||
|
try {
|
||||||
|
const testResult = await instance.testDatasource();
|
||||||
|
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||||
|
} catch (error) {
|
||||||
|
datasourceError = (error && error.statusText) || 'Network error';
|
||||||
|
}
|
||||||
|
if (datasourceError) {
|
||||||
|
dispatch(loadDatasourceFailure(exploreId, datasourceError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
|
||||||
|
// User already changed datasource again, discard results
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.init) {
|
||||||
|
instance.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if queries can be imported from previously selected datasource
|
||||||
|
const queries = getState().explore[exploreId].modifiedQueries;
|
||||||
|
let importedQueries = queries;
|
||||||
|
const origin = getState().explore[exploreId].datasourceInstance;
|
||||||
|
if (origin) {
|
||||||
|
if (origin.meta.id === instance.meta.id) {
|
||||||
|
// Keep same queries if same type of datasource
|
||||||
|
importedQueries = [...queries];
|
||||||
|
} else if (instance.importQueries) {
|
||||||
|
// Datasource-specific importers
|
||||||
|
importedQueries = await instance.importQueries(queries, origin.meta);
|
||||||
|
} else {
|
||||||
|
// Default is blank queries
|
||||||
|
importedQueries = ensureQueries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
|
||||||
|
// User already changed datasource again, discard results
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset edit state with new queries
|
||||||
|
const nextQueries = importedQueries.map((q, i) => ({
|
||||||
|
...importedQueries[i],
|
||||||
|
...generateEmptyQuery(i),
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries));
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to modify a query given a datasource-specific modifier action.
|
||||||
|
* @param exploreId Explore area
|
||||||
|
* @param modification Action object with a type, e.g., ADD_FILTER
|
||||||
|
* @param index Optional query row index. If omitted, the modification is applied to all query rows.
|
||||||
|
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
|
||||||
|
*/
|
||||||
|
export function modifyQueries(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
modification: any,
|
||||||
|
index: number,
|
||||||
|
modifier: any
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
|
||||||
|
if (!modification.preventSubmit) {
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a query transaction as failed with an error extracted from the query response.
|
||||||
|
* The transaction will be marked as `done`.
|
||||||
|
*/
|
||||||
|
export function queryTransactionFailure(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
transactionId: string,
|
||||||
|
response: any,
|
||||||
|
datasourceId: string
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
|
||||||
|
if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
|
||||||
|
// Navigated away, queries did not matter
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction might have been discarded
|
||||||
|
if (!queryTransactions.find(qt => qt.id === transactionId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(response);
|
||||||
|
|
||||||
|
let error: string;
|
||||||
|
let errorDetails: string;
|
||||||
|
if (response.data) {
|
||||||
|
if (typeof response.data === 'string') {
|
||||||
|
error = response.data;
|
||||||
|
} else if (response.data.error) {
|
||||||
|
error = response.data.error;
|
||||||
|
if (response.data.response) {
|
||||||
|
errorDetails = response.data.response;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not handle error response');
|
||||||
|
}
|
||||||
|
} else if (response.message) {
|
||||||
|
error = response.message;
|
||||||
|
} else if (typeof response === 'string') {
|
||||||
|
error = response;
|
||||||
|
} else {
|
||||||
|
error = 'Unknown error during query transaction. Please check JS console logs.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark transactions as complete
|
||||||
|
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||||
|
if (qt.id === transactionId) {
|
||||||
|
return {
|
||||||
|
...qt,
|
||||||
|
error,
|
||||||
|
errorDetails,
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return qt;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.QueryTransactionFailure,
|
||||||
|
payload: { exploreId, queryTransactions: nextQueryTransactions },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a query transaction for the given result type.
|
||||||
|
* @param exploreId Explore area
|
||||||
|
* @param transaction Query options and `done` status.
|
||||||
|
* @param resultType Associate the transaction with a result viewer, e.g., Graph
|
||||||
|
* @param rowIndex Index is used to associate latency for this transaction with a query row
|
||||||
|
*/
|
||||||
|
export function queryTransactionStart(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
transaction: QueryTransaction,
|
||||||
|
resultType: ResultType,
|
||||||
|
rowIndex: number
|
||||||
|
): QueryTransactionStartAction {
|
||||||
|
return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
|
||||||
|
* If the transaction was started by a scanner, it keeps on scanning for more results.
|
||||||
|
* Side-effect: the query is stored in localStorage.
|
||||||
|
* @param exploreId Explore area
|
||||||
|
* @param transactionId ID
|
||||||
|
* @param result Response from `datasourceInstance.query()`
|
||||||
|
* @param latency Duration between request and response
|
||||||
|
* @param queries Queries from all query rows
|
||||||
|
* @param datasourceId Origin datasource instance, used to discard results if current datasource is different
|
||||||
|
*/
|
||||||
|
export function queryTransactionSuccess(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
transactionId: string,
|
||||||
|
result: any,
|
||||||
|
latency: number,
|
||||||
|
queries: DataQuery[],
|
||||||
|
datasourceId: string
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
|
||||||
|
|
||||||
|
// If datasource already changed, results do not matter
|
||||||
|
if (datasourceInstance.meta.id !== datasourceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction might have been discarded
|
||||||
|
const transaction = queryTransactions.find(qt => qt.id === transactionId);
|
||||||
|
if (!transaction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get query hints
|
||||||
|
let hints: QueryHint[];
|
||||||
|
if (datasourceInstance.getQueryHints as QueryHintGetter) {
|
||||||
|
hints = datasourceInstance.getQueryHints(transaction.query, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark transactions as complete and attach result
|
||||||
|
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||||
|
if (qt.id === transactionId) {
|
||||||
|
return {
|
||||||
|
...qt,
|
||||||
|
hints,
|
||||||
|
latency,
|
||||||
|
result,
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return qt;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Side-effect: Saving history in localstorage
|
||||||
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.QueryTransactionSuccess,
|
||||||
|
payload: {
|
||||||
|
exploreId,
|
||||||
|
history: nextHistory,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep scanning for results if this was the last scanning transaction
|
||||||
|
if (scanning) {
|
||||||
|
if (_.size(result) === 0) {
|
||||||
|
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
|
||||||
|
if (!other) {
|
||||||
|
const range = scanner();
|
||||||
|
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We can stop scanning if we have a result
|
||||||
|
dispatch(scanStop(exploreId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove query row of the given index, as well as associated query results.
|
||||||
|
*/
|
||||||
|
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
||||||
|
*/
|
||||||
|
export function runQueries(exploreId: ExploreId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {
|
||||||
|
datasourceInstance,
|
||||||
|
modifiedQueries,
|
||||||
|
showingLogs,
|
||||||
|
showingGraph,
|
||||||
|
showingTable,
|
||||||
|
supportsGraph,
|
||||||
|
supportsLogs,
|
||||||
|
supportsTable,
|
||||||
|
} = getState().explore[exploreId];
|
||||||
|
|
||||||
|
if (!hasNonEmptyQuery(modifiedQueries)) {
|
||||||
|
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some datasource's query builders allow per-query interval limits,
|
||||||
|
// but we're using the datasource interval limit for now
|
||||||
|
const interval = datasourceInstance.interval;
|
||||||
|
|
||||||
|
// Keep table queries first since they need to return quickly
|
||||||
|
if (showingTable && supportsTable) {
|
||||||
|
dispatch(
|
||||||
|
runQueriesForType(
|
||||||
|
exploreId,
|
||||||
|
'Table',
|
||||||
|
{
|
||||||
|
interval,
|
||||||
|
format: 'table',
|
||||||
|
instant: true,
|
||||||
|
valueWithRefId: true,
|
||||||
|
},
|
||||||
|
data => data[0]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showingGraph && supportsGraph) {
|
||||||
|
dispatch(
|
||||||
|
runQueriesForType(
|
||||||
|
exploreId,
|
||||||
|
'Graph',
|
||||||
|
{
|
||||||
|
interval,
|
||||||
|
format: 'time_series',
|
||||||
|
instant: false,
|
||||||
|
},
|
||||||
|
makeTimeSeriesList
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showingLogs && supportsLogs) {
|
||||||
|
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
|
||||||
|
}
|
||||||
|
dispatch(stateSave());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper action to build a query transaction object and handing the query to the datasource.
|
||||||
|
* @param exploreId Explore area
|
||||||
|
* @param resultType Result viewer that will be associated with this query result
|
||||||
|
* @param queryOptions Query options as required by the datasource's `query()` function.
|
||||||
|
* @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
|
||||||
|
*/
|
||||||
|
function runQueriesForType(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
resultType: ResultType,
|
||||||
|
queryOptions: QueryOptions,
|
||||||
|
resultGetter?: any
|
||||||
|
) {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const {
|
||||||
|
datasourceInstance,
|
||||||
|
eventBridge,
|
||||||
|
modifiedQueries: queries,
|
||||||
|
queryIntervals,
|
||||||
|
range,
|
||||||
|
scanning,
|
||||||
|
} = getState().explore[exploreId];
|
||||||
|
const datasourceId = datasourceInstance.meta.id;
|
||||||
|
|
||||||
|
// Run all queries concurrently
|
||||||
|
queries.forEach(async (query, rowIndex) => {
|
||||||
|
const transaction = buildQueryTransaction(
|
||||||
|
query,
|
||||||
|
rowIndex,
|
||||||
|
resultType,
|
||||||
|
queryOptions,
|
||||||
|
range,
|
||||||
|
queryIntervals,
|
||||||
|
scanning
|
||||||
|
);
|
||||||
|
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
const res = await datasourceInstance.query(transaction.options);
|
||||||
|
eventBridge.emit('data-received', res.data || []);
|
||||||
|
const latency = Date.now() - now;
|
||||||
|
const results = resultGetter ? resultGetter(res.data) : res.data;
|
||||||
|
dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
|
||||||
|
} catch (response) {
|
||||||
|
eventBridge.emit('data-error', response);
|
||||||
|
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a scan for more results using the given scanner.
|
||||||
|
* @param exploreId Explore area
|
||||||
|
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
||||||
|
*/
|
||||||
|
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
// Register the scanner
|
||||||
|
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
|
||||||
|
// Scanning must trigger query run, and return the new range
|
||||||
|
const range = scanner();
|
||||||
|
// Set the new range to be displayed
|
||||||
|
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop any scanning for more results.
|
||||||
|
*/
|
||||||
|
export function scanStop(exploreId: ExploreId): ScanStopAction {
|
||||||
|
return { type: ActionTypes.ScanStop, payload: { exploreId } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset queries to the given queries. Any modifications will be discarded.
|
||||||
|
* Use this action for clicks on query examples. Triggers a query run.
|
||||||
|
*/
|
||||||
|
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
// Inject react keys into query objects
|
||||||
|
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SetQueries,
|
||||||
|
payload: {
|
||||||
|
exploreId,
|
||||||
|
queries,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the split view and save URL state.
|
||||||
|
*/
|
||||||
|
export function splitClose(): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: ActionTypes.SplitClose });
|
||||||
|
dispatch(stateSave());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the split view and copy the left state to be the right state.
|
||||||
|
* The right state is automatically initialized.
|
||||||
|
* The copy keeps all query modifications but wipes the query results.
|
||||||
|
*/
|
||||||
|
export function splitOpen(): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
// Clone left state to become the right state
|
||||||
|
const leftState = getState().explore.left;
|
||||||
|
const itemState = {
|
||||||
|
...leftState,
|
||||||
|
queryTransactions: [],
|
||||||
|
initialQueries: leftState.modifiedQueries.slice(),
|
||||||
|
};
|
||||||
|
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
|
||||||
|
dispatch(stateSave());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves Explore state to URL using the `left` and `right` parameters.
|
||||||
|
* If split view is not active, `right` will not be set.
|
||||||
|
*/
|
||||||
|
export function stateSave() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { left, right, split } = getState().explore;
|
||||||
|
const urlStates: { [index: string]: string } = {};
|
||||||
|
const leftUrlState: ExploreUrlState = {
|
||||||
|
datasource: left.datasourceInstance.name,
|
||||||
|
queries: left.modifiedQueries.map(clearQueryKeys),
|
||||||
|
range: left.range,
|
||||||
|
};
|
||||||
|
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
||||||
|
if (split) {
|
||||||
|
const rightUrlState: ExploreUrlState = {
|
||||||
|
datasource: right.datasourceInstance.name,
|
||||||
|
queries: right.modifiedQueries.map(clearQueryKeys),
|
||||||
|
range: right.range,
|
||||||
|
};
|
||||||
|
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
||||||
|
}
|
||||||
|
dispatch(updateLocation({ query: urlStates }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
||||||
|
*/
|
||||||
|
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
|
||||||
|
if (getState().explore[exploreId].showingGraph) {
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
|
||||||
|
*/
|
||||||
|
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
|
||||||
|
if (getState().explore[exploreId].showingLogs) {
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
||||||
|
*/
|
||||||
|
export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
|
||||||
|
if (getState().explore[exploreId].showingTable) {
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
import {
|
||||||
|
calculateResultsFromQueryTransactions,
|
||||||
|
generateEmptyQuery,
|
||||||
|
getIntervals,
|
||||||
|
ensureQueries,
|
||||||
|
} from 'app/core/utils/explore';
|
||||||
|
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
|
||||||
|
import { DataQuery } from 'app/types/series';
|
||||||
|
|
||||||
|
import { Action, ActionTypes } from './actionTypes';
|
||||||
|
|
||||||
|
export const DEFAULT_RANGE = {
|
||||||
|
from: 'now-6h',
|
||||||
|
to: 'now',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Millies step for helper bar charts
|
||||||
|
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a fresh Explore area state
|
||||||
|
*/
|
||||||
|
const makeExploreItemState = (): ExploreItemState => ({
|
||||||
|
StartPage: undefined,
|
||||||
|
containerWidth: 0,
|
||||||
|
datasourceInstance: null,
|
||||||
|
datasourceError: null,
|
||||||
|
datasourceLoading: null,
|
||||||
|
datasourceMissing: false,
|
||||||
|
exploreDatasources: [],
|
||||||
|
history: [],
|
||||||
|
initialQueries: [],
|
||||||
|
initialized: false,
|
||||||
|
modifiedQueries: [],
|
||||||
|
queryTransactions: [],
|
||||||
|
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
scanning: false,
|
||||||
|
scanRange: null,
|
||||||
|
showingGraph: true,
|
||||||
|
showingLogs: true,
|
||||||
|
showingTable: true,
|
||||||
|
supportsGraph: null,
|
||||||
|
supportsLogs: null,
|
||||||
|
supportsTable: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Explore state that handles multiple Explore areas and the split state
|
||||||
|
*/
|
||||||
|
const initialExploreState: ExploreState = {
|
||||||
|
split: null,
|
||||||
|
left: makeExploreItemState(),
|
||||||
|
right: makeExploreItemState(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer for an Explore area, to be used by the global Explore reducer.
|
||||||
|
*/
|
||||||
|
const itemReducer = (state, action: Action): ExploreItemState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.AddQueryRow: {
|
||||||
|
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||||
|
const { index, query } = action.payload;
|
||||||
|
|
||||||
|
// Add new query row after given index, keep modifications of existing rows
|
||||||
|
const nextModifiedQueries = [
|
||||||
|
...modifiedQueries.slice(0, index + 1),
|
||||||
|
{ ...query },
|
||||||
|
...initialQueries.slice(index + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add to initialQueries, which will cause a new row to be rendered
|
||||||
|
const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
|
||||||
|
|
||||||
|
// Ongoing transactions need to update their row indices
|
||||||
|
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||||
|
if (qt.rowIndex > index) {
|
||||||
|
return {
|
||||||
|
...qt,
|
||||||
|
rowIndex: qt.rowIndex + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return qt;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialQueries: nextQueries,
|
||||||
|
logsHighlighterExpressions: undefined,
|
||||||
|
modifiedQueries: nextModifiedQueries,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ChangeQuery: {
|
||||||
|
const { initialQueries, queryTransactions } = state;
|
||||||
|
let { modifiedQueries } = state;
|
||||||
|
const { query, index, override } = action.payload;
|
||||||
|
|
||||||
|
// Fast path: only change modifiedQueries to not trigger an update
|
||||||
|
modifiedQueries[index] = query;
|
||||||
|
if (!override) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modifiedQueries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override path: queries are completely reset
|
||||||
|
const nextQuery: DataQuery = {
|
||||||
|
...query,
|
||||||
|
...generateEmptyQuery(index),
|
||||||
|
};
|
||||||
|
const nextQueries = [...initialQueries];
|
||||||
|
nextQueries[index] = nextQuery;
|
||||||
|
modifiedQueries = [...nextQueries];
|
||||||
|
|
||||||
|
// Discard ongoing transaction related to row query
|
||||||
|
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialQueries: nextQueries,
|
||||||
|
modifiedQueries: nextQueries.slice(),
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ChangeSize: {
|
||||||
|
const { range, datasourceInstance } = state;
|
||||||
|
let interval = '1s';
|
||||||
|
if (datasourceInstance && datasourceInstance.interval) {
|
||||||
|
interval = datasourceInstance.interval;
|
||||||
|
}
|
||||||
|
const containerWidth = action.payload.width;
|
||||||
|
const queryIntervals = getIntervals(range, interval, containerWidth);
|
||||||
|
return { ...state, containerWidth, queryIntervals };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ChangeTime: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
range: action.payload.range,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ClearQueries: {
|
||||||
|
const queries = ensureQueries();
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialQueries: queries.slice(),
|
||||||
|
modifiedQueries: queries.slice(),
|
||||||
|
queryTransactions: [],
|
||||||
|
showingStartPage: Boolean(state.StartPage),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.HighlightLogsExpression: {
|
||||||
|
const { expressions } = action.payload;
|
||||||
|
return { ...state, logsHighlighterExpressions: expressions };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.InitializeExplore: {
|
||||||
|
const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
containerWidth,
|
||||||
|
eventBridge,
|
||||||
|
exploreDatasources,
|
||||||
|
range,
|
||||||
|
initialDatasource: datasource,
|
||||||
|
initialQueries: queries,
|
||||||
|
initialized: true,
|
||||||
|
modifiedQueries: queries.slice(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.LoadDatasourceFailure: {
|
||||||
|
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.LoadDatasourceMissing: {
|
||||||
|
return { ...state, datasourceMissing: true, datasourceLoading: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.LoadDatasourcePending: {
|
||||||
|
return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.LoadDatasourceSuccess: {
|
||||||
|
const { containerWidth, range } = state;
|
||||||
|
const {
|
||||||
|
StartPage,
|
||||||
|
datasourceInstance,
|
||||||
|
history,
|
||||||
|
initialDatasource,
|
||||||
|
initialQueries,
|
||||||
|
showingStartPage,
|
||||||
|
supportsGraph,
|
||||||
|
supportsLogs,
|
||||||
|
supportsTable,
|
||||||
|
} = action.payload;
|
||||||
|
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queryIntervals,
|
||||||
|
StartPage,
|
||||||
|
datasourceInstance,
|
||||||
|
history,
|
||||||
|
initialDatasource,
|
||||||
|
initialQueries,
|
||||||
|
showingStartPage,
|
||||||
|
supportsGraph,
|
||||||
|
supportsLogs,
|
||||||
|
supportsTable,
|
||||||
|
datasourceLoading: false,
|
||||||
|
datasourceMissing: false,
|
||||||
|
logsHighlighterExpressions: undefined,
|
||||||
|
modifiedQueries: initialQueries.slice(),
|
||||||
|
queryTransactions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ModifyQueries: {
|
||||||
|
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||||
|
const { modification, index, modifier } = action.payload as any;
|
||||||
|
let nextQueries: DataQuery[];
|
||||||
|
let nextQueryTransactions;
|
||||||
|
if (index === undefined) {
|
||||||
|
// Modify all queries
|
||||||
|
nextQueries = initialQueries.map((query, i) => ({
|
||||||
|
...modifier(modifiedQueries[i], modification),
|
||||||
|
...generateEmptyQuery(i),
|
||||||
|
}));
|
||||||
|
// Discard all ongoing transactions
|
||||||
|
nextQueryTransactions = [];
|
||||||
|
} else {
|
||||||
|
// Modify query only at index
|
||||||
|
nextQueries = initialQueries.map((query, i) => {
|
||||||
|
// Synchronize all queries with local query cache to ensure consistency
|
||||||
|
// TODO still needed?
|
||||||
|
return i === index
|
||||||
|
? {
|
||||||
|
...modifier(modifiedQueries[i], modification),
|
||||||
|
...generateEmptyQuery(i),
|
||||||
|
}
|
||||||
|
: query;
|
||||||
|
});
|
||||||
|
nextQueryTransactions = queryTransactions
|
||||||
|
// Consume the hint corresponding to the action
|
||||||
|
.map(qt => {
|
||||||
|
if (qt.hints != null && qt.rowIndex === index) {
|
||||||
|
qt.hints = qt.hints.filter(hint => hint.fix.action !== modification);
|
||||||
|
}
|
||||||
|
return qt;
|
||||||
|
})
|
||||||
|
// Preserve previous row query transaction to keep results visible if next query is incomplete
|
||||||
|
.filter(qt => modification.preventSubmit || qt.rowIndex !== index);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialQueries: nextQueries,
|
||||||
|
modifiedQueries: nextQueries.slice(),
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.QueryTransactionFailure: {
|
||||||
|
const { queryTransactions } = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queryTransactions,
|
||||||
|
showingStartPage: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.QueryTransactionStart: {
|
||||||
|
const { datasourceInstance, queryIntervals, queryTransactions } = state;
|
||||||
|
const { resultType, rowIndex, transaction } = action.payload;
|
||||||
|
// Discarding existing transactions of same type
|
||||||
|
const remainingTransactions = queryTransactions.filter(
|
||||||
|
qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Append new transaction
|
||||||
|
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
|
||||||
|
|
||||||
|
const results = calculateResultsFromQueryTransactions(
|
||||||
|
nextQueryTransactions,
|
||||||
|
datasourceInstance,
|
||||||
|
queryIntervals.intervalMs
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...results,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
showingStartPage: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.QueryTransactionSuccess: {
|
||||||
|
const { datasourceInstance, queryIntervals } = state;
|
||||||
|
const { history, queryTransactions } = action.payload;
|
||||||
|
const results = calculateResultsFromQueryTransactions(
|
||||||
|
queryTransactions,
|
||||||
|
datasourceInstance,
|
||||||
|
queryIntervals.intervalMs
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...results,
|
||||||
|
history,
|
||||||
|
queryTransactions,
|
||||||
|
showingStartPage: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.RemoveQueryRow: {
|
||||||
|
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
|
||||||
|
let { modifiedQueries } = state;
|
||||||
|
const { index } = action.payload;
|
||||||
|
|
||||||
|
modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
|
||||||
|
|
||||||
|
if (initialQueries.length <= 1) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
|
||||||
|
|
||||||
|
// Discard transactions related to row query
|
||||||
|
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||||
|
const results = calculateResultsFromQueryTransactions(
|
||||||
|
nextQueryTransactions,
|
||||||
|
datasourceInstance,
|
||||||
|
queryIntervals.intervalMs
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...results,
|
||||||
|
initialQueries: nextQueries,
|
||||||
|
logsHighlighterExpressions: undefined,
|
||||||
|
modifiedQueries: nextQueries.slice(),
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.RunQueriesEmpty: {
|
||||||
|
return { ...state, queryTransactions: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ScanRange: {
|
||||||
|
return { ...state, scanRange: action.payload.range };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ScanStart: {
|
||||||
|
return { ...state, scanning: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ScanStop: {
|
||||||
|
const { queryTransactions } = state;
|
||||||
|
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
|
||||||
|
return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.SetQueries: {
|
||||||
|
const { queries } = action.payload;
|
||||||
|
return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ToggleGraph: {
|
||||||
|
const showingGraph = !state.showingGraph;
|
||||||
|
let nextQueryTransactions = state.queryTransactions;
|
||||||
|
if (!showingGraph) {
|
||||||
|
// Discard transactions related to Graph query
|
||||||
|
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
|
||||||
|
}
|
||||||
|
return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ToggleLogs: {
|
||||||
|
const showingLogs = !state.showingLogs;
|
||||||
|
let nextQueryTransactions = state.queryTransactions;
|
||||||
|
if (!showingLogs) {
|
||||||
|
// Discard transactions related to Logs query
|
||||||
|
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
|
||||||
|
}
|
||||||
|
return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.ToggleTable: {
|
||||||
|
const showingTable = !state.showingTable;
|
||||||
|
if (showingTable) {
|
||||||
|
return { ...state, showingTable, queryTransactions: state.queryTransactions };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle off needs discarding of table queries and results
|
||||||
|
const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
|
||||||
|
const results = calculateResultsFromQueryTransactions(
|
||||||
|
nextQueryTransactions,
|
||||||
|
state.datasourceInstance,
|
||||||
|
state.queryIntervals.intervalMs
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Explore reducer that handles multiple Explore areas (left and right).
|
||||||
|
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
|
||||||
|
*/
|
||||||
|
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SplitClose: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
split: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.SplitOpen: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
split: true,
|
||||||
|
right: action.payload.itemState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.InitializeExploreSplit: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
split: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.payload) {
|
||||||
|
const { exploreId } = action.payload as any;
|
||||||
|
if (exploreId !== undefined) {
|
||||||
|
const exploreItemState = state[exploreId];
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[exploreId]: itemReducer(exploreItemState, action),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
explore: exploreReducer,
|
||||||
|
};
|
||||||
@@ -132,7 +132,7 @@ export class VariableSrv {
|
|||||||
|
|
||||||
return this.$q.all(promises).then(() => {
|
return this.$q.all(promises).then(() => {
|
||||||
if (emitChangeEvents) {
|
if (emitChangeEvents) {
|
||||||
this.$rootScope.$emit('template-variable-value-updated');
|
this.$rootScope.appEvent('template-variable-value-updated');
|
||||||
this.dashboard.startRefresh();
|
this.dashboard.startRefresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export class ElasticResponse {
|
|||||||
datapoints: [],
|
datapoints: [],
|
||||||
metric: metric.type,
|
metric: metric.type,
|
||||||
field: metric.field,
|
field: metric.field,
|
||||||
|
metricId: metric.id,
|
||||||
props: props,
|
props: props,
|
||||||
};
|
};
|
||||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||||
@@ -240,7 +241,7 @@ export class ElasticResponse {
|
|||||||
return metricName;
|
return metricName;
|
||||||
}
|
}
|
||||||
if (group === 'field') {
|
if (group === 'field') {
|
||||||
return series.field;
|
return series.field || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return match;
|
return match;
|
||||||
@@ -248,11 +249,27 @@ export class ElasticResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (series.field && queryDef.isPipelineAgg(series.metric)) {
|
if (series.field && queryDef.isPipelineAgg(series.metric)) {
|
||||||
const appliedAgg = _.find(target.metrics, { id: series.field });
|
if (series.metric && queryDef.isPipelineAggWithMultipleBucketPaths(series.metric)) {
|
||||||
if (appliedAgg) {
|
const agg = _.find(target.metrics, { id: series.metricId });
|
||||||
metricName += ' ' + queryDef.describeMetric(appliedAgg);
|
if (agg && agg.settings.script) {
|
||||||
|
metricName = agg.settings.script;
|
||||||
|
|
||||||
|
for (const pv of agg.pipelineVariables) {
|
||||||
|
const appliedAgg = _.find(target.metrics, { id: pv.pipelineAgg });
|
||||||
|
if (appliedAgg) {
|
||||||
|
metricName = metricName.replace('params.' + pv.name, queryDef.describeMetric(appliedAgg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metricName = 'Unset';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
metricName = 'Unset';
|
const appliedAgg = _.find(target.metrics, { id: series.field });
|
||||||
|
if (appliedAgg) {
|
||||||
|
metricName += ' ' + queryDef.describeMetric(appliedAgg);
|
||||||
|
} else {
|
||||||
|
metricName = 'Unset';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (series.field) {
|
} else if (series.field) {
|
||||||
metricName += ' ' + series.field;
|
metricName += ' ' + series.field;
|
||||||
|
|||||||
@@ -35,11 +35,20 @@ export class ElasticMetricAggCtrl {
|
|||||||
$scope.isFirst = $scope.index === 0;
|
$scope.isFirst = $scope.index === 0;
|
||||||
$scope.isSingle = metricAggs.length === 1;
|
$scope.isSingle = metricAggs.length === 1;
|
||||||
$scope.settingsLinkText = '';
|
$scope.settingsLinkText = '';
|
||||||
|
$scope.variablesLinkText = '';
|
||||||
$scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
|
$scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
|
||||||
|
|
||||||
if (queryDef.isPipelineAgg($scope.agg.type)) {
|
if (queryDef.isPipelineAgg($scope.agg.type)) {
|
||||||
$scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
|
if (queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type)) {
|
||||||
$scope.agg.field = $scope.agg.pipelineAgg;
|
$scope.variablesLinkText = 'Options';
|
||||||
|
|
||||||
|
if ($scope.agg.settings.script) {
|
||||||
|
$scope.variablesLinkText = 'Script: ' + $scope.agg.settings.script.replace(new RegExp('params.', 'g'), '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
|
||||||
|
$scope.agg.field = $scope.agg.pipelineAgg;
|
||||||
|
}
|
||||||
|
|
||||||
const pipelineOptions = queryDef.getPipelineOptions($scope.agg);
|
const pipelineOptions = queryDef.getPipelineOptions($scope.agg);
|
||||||
if (pipelineOptions.length > 0) {
|
if (pipelineOptions.length > 0) {
|
||||||
@@ -119,6 +128,10 @@ export class ElasticMetricAggCtrl {
|
|||||||
$scope.updatePipelineAggOptions();
|
$scope.updatePipelineAggOptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.toggleVariables = () => {
|
||||||
|
$scope.showVariables = !$scope.showVariables;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.onChangeInternal = () => {
|
$scope.onChangeInternal = () => {
|
||||||
$scope.onChange();
|
$scope.onChange();
|
||||||
};
|
};
|
||||||
@@ -152,6 +165,7 @@ export class ElasticMetricAggCtrl {
|
|||||||
$scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
|
$scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.showVariables = queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type);
|
||||||
$scope.updatePipelineAggOptions();
|
$scope.updatePipelineAggOptions();
|
||||||
$scope.onChange();
|
$scope.onChange();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,17 @@
|
|||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="width-10"></metric-segment-model>
|
<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="width-10"></metric-segment-model>
|
||||||
<metric-segment-model ng-if="aggDef.requiresField" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
|
<metric-segment-model ng-if="aggDef.requiresField" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
|
||||||
<metric-segment-model ng-if="aggDef.isPipelineAgg" property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
|
<metric-segment-model ng-if="aggDef.isPipelineAgg && !aggDef.supportsMultipleBucketPaths" property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow" ng-if="aggDef.isPipelineAgg && aggDef.supportsMultipleBucketPaths">
|
||||||
|
<label class="gf-form-label gf-form-label--grow">
|
||||||
|
<a ng-click="toggleVariables()">
|
||||||
|
<i class="fa fa-caret-down" ng-show="showVariables"></i>
|
||||||
|
<i class="fa fa-caret-right" ng-hide="showVariables"></i>
|
||||||
|
{{variablesLinkText}}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
@@ -36,6 +46,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-group" ng-if="showVariables">
|
||||||
|
<elastic-pipeline-variables variables="agg.pipelineVariables" options="pipelineAggOptions" on-change="onChangeInternal()" />
|
||||||
|
<div class="gf-form offset-width-7">
|
||||||
|
<label class="gf-form-label width-10">
|
||||||
|
Script
|
||||||
|
<info-popover mode="right-normal">
|
||||||
|
Elasticsearch v5.0 and above: Scripting language is Painless. Use <i>params.<var></i> to reference a variable.<br/><br/>
|
||||||
|
Elasticsearch pre-v5.0: Scripting language is per default Groovy if not changed. For Groovy use <i><var></i> to reference a variable.
|
||||||
|
</info-popover>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="gf-form-input max-width-24" empty-to-null ng-model="agg.settings.script" ng-blur="onChangeInternal()" spellcheck='false' placeholder="params.var1 / params.var2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group" ng-if="showOptions">
|
<div class="gf-form-group" ng-if="showOptions">
|
||||||
<div class="gf-form offset-width-7" ng-if="agg.type === 'derivative'">
|
<div class="gf-form offset-width-7" ng-if="agg.type === 'derivative'">
|
||||||
<label class="gf-form-label width-10">Unit</label>
|
<label class="gf-form-label width-10">Unit</label>
|
||||||
@@ -103,5 +127,5 @@
|
|||||||
<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
|
<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
|
||||||
</label>
|
</label>
|
||||||
<input type="number" class="gf-form-input max-width-12" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
|
<input type="number" class="gf-form-input max-width-12" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<div ng-repeat="var in variables">
|
||||||
|
<div class="gf-form offset-width-7" ng-if="$index === 0">
|
||||||
|
<label class="gf-form-label width-10">Variables</label>
|
||||||
|
<input type="text" class="gf-form-input max-width-12" ng-model="var.name" placeholder="Variable name" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||||
|
<metric-segment-model property="var.pipelineAgg" options="options" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="remove($index)"><i class="fa fa-minus"></i></a>
|
||||||
|
</label>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="add()"><i class="fa fa-plus"></i></a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form offset-width-17" ng-if="$index !== 0">
|
||||||
|
<input type="text" class="gf-form-input max-width-12" ng-model="var.name" placeholder="Variable name" ng-blur="onChangeInternal()" spellcheck='false'>
|
||||||
|
<metric-segment-model property="var.pipelineAgg" options="options" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="remove($index)"><i class="fa fa-minus"></i></a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export function elasticPipelineVariables() {
|
||||||
|
return {
|
||||||
|
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/pipeline_variables.html',
|
||||||
|
controller: 'ElasticPipelineVariablesCtrl',
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
onChange: '&',
|
||||||
|
variables: '=',
|
||||||
|
options: '=',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVariable = index => {
|
||||||
|
return {
|
||||||
|
name: 'var' + index,
|
||||||
|
pipelineAgg: 'select metric',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ElasticPipelineVariablesCtrl {
|
||||||
|
constructor($scope) {
|
||||||
|
$scope.variables = $scope.variables || [newVariable(1)];
|
||||||
|
|
||||||
|
$scope.onChangeInternal = () => {
|
||||||
|
$scope.onChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.add = () => {
|
||||||
|
$scope.variables.push(newVariable($scope.variables.length + 1));
|
||||||
|
$scope.onChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.remove = index => {
|
||||||
|
$scope.variables.splice(index, 1);
|
||||||
|
$scope.onChange();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('elasticPipelineVariables', elasticPipelineVariables);
|
||||||
|
coreModule.controller('ElasticPipelineVariablesCtrl', ElasticPipelineVariablesCtrl);
|
||||||
@@ -189,7 +189,7 @@ export class ElasticQueryBuilder {
|
|||||||
target.bucketAggs = target.bucketAggs || [queryDef.defaultBucketAgg()];
|
target.bucketAggs = target.bucketAggs || [queryDef.defaultBucketAgg()];
|
||||||
target.timeField = this.timeField;
|
target.timeField = this.timeField;
|
||||||
|
|
||||||
let i, nestedAggs, metric;
|
let i, j, pv, nestedAggs, metric;
|
||||||
const query = {
|
const query = {
|
||||||
size: 0,
|
size: 0,
|
||||||
query: {
|
query: {
|
||||||
@@ -269,17 +269,42 @@ export class ElasticQueryBuilder {
|
|||||||
let metricAgg = null;
|
let metricAgg = null;
|
||||||
|
|
||||||
if (queryDef.isPipelineAgg(metric.type)) {
|
if (queryDef.isPipelineAgg(metric.type)) {
|
||||||
if (metric.pipelineAgg && /^\d*$/.test(metric.pipelineAgg)) {
|
if (queryDef.isPipelineAggWithMultipleBucketPaths(metric.type)) {
|
||||||
const appliedAgg = queryDef.findMetricById(target.metrics, metric.pipelineAgg);
|
if (metric.pipelineVariables) {
|
||||||
if (appliedAgg) {
|
metricAgg = {
|
||||||
if (appliedAgg.type === 'count') {
|
buckets_path: {},
|
||||||
metricAgg = { buckets_path: '_count' };
|
};
|
||||||
} else {
|
|
||||||
metricAgg = { buckets_path: metric.pipelineAgg };
|
for (j = 0; j < metric.pipelineVariables.length; j++) {
|
||||||
|
pv = metric.pipelineVariables[j];
|
||||||
|
|
||||||
|
if (pv.name && pv.pipelineAgg && /^\d*$/.test(pv.pipelineAgg)) {
|
||||||
|
const appliedAgg = queryDef.findMetricById(target.metrics, pv.pipelineAgg);
|
||||||
|
if (appliedAgg) {
|
||||||
|
if (appliedAgg.type === 'count') {
|
||||||
|
metricAgg.buckets_path[pv.name] = '_count';
|
||||||
|
} else {
|
||||||
|
metricAgg.buckets_path[pv.name] = pv.pipelineAgg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
continue;
|
if (metric.pipelineAgg && /^\d*$/.test(metric.pipelineAgg)) {
|
||||||
|
const appliedAgg = queryDef.findMetricById(target.metrics, metric.pipelineAgg);
|
||||||
|
if (appliedAgg) {
|
||||||
|
if (appliedAgg.type === 'count') {
|
||||||
|
metricAgg = { buckets_path: '_count' };
|
||||||
|
} else {
|
||||||
|
metricAgg = { buckets_path: metric.pipelineAgg };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
metricAgg = { field: metric.field };
|
metricAgg = { field: metric.field };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import './bucket_agg';
|
import './bucket_agg';
|
||||||
import './metric_agg';
|
import './metric_agg';
|
||||||
|
import './pipeline_variables';
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -70,6 +71,9 @@ export class ElasticQueryCtrl extends QueryCtrl {
|
|||||||
if (aggDef.requiresField) {
|
if (aggDef.requiresField) {
|
||||||
text += metric.field;
|
text += metric.field;
|
||||||
}
|
}
|
||||||
|
if (aggDef.supportsMultipleBucketPaths) {
|
||||||
|
text += metric.settings.script.replace(new RegExp('params.', 'g'), '');
|
||||||
|
}
|
||||||
text += '), ';
|
text += '), ';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ export const metricAggTypes = [
|
|||||||
isPipelineAgg: true,
|
isPipelineAgg: true,
|
||||||
minVersion: 2,
|
minVersion: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Bucket Script',
|
||||||
|
value: 'bucket_script',
|
||||||
|
requiresField: false,
|
||||||
|
isPipelineAgg: true,
|
||||||
|
supportsMultipleBucketPaths: true,
|
||||||
|
minVersion: 2,
|
||||||
|
},
|
||||||
{ text: 'Raw Document', value: 'raw_document', requiresField: false },
|
{ text: 'Raw Document', value: 'raw_document', requiresField: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -128,6 +136,7 @@ export const pipelineOptions = {
|
|||||||
{ text: 'minimize', default: false },
|
{ text: 'minimize', default: false },
|
||||||
],
|
],
|
||||||
derivative: [{ text: 'unit', default: undefined }],
|
derivative: [{ text: 'unit', default: undefined }],
|
||||||
|
bucket_script: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const movingAvgModelSettings = {
|
export const movingAvgModelSettings = {
|
||||||
@@ -171,6 +180,14 @@ export function isPipelineAgg(metricType) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPipelineAggWithMultipleBucketPaths(metricType) {
|
||||||
|
if (metricType) {
|
||||||
|
return metricAggTypes.find(t => t.value === metricType && t.supportsMultipleBucketPaths) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function getPipelineAggOptions(targets) {
|
export function getPipelineAggOptions(targets) {
|
||||||
const result = [];
|
const result = [];
|
||||||
_.each(targets.metrics, metric => {
|
_.each(targets.metrics, metric => {
|
||||||
|
|||||||
@@ -665,4 +665,70 @@ describe('ElasticResponse', () => {
|
|||||||
expect(result.data[0].datapoints[0].fieldProp).toBe('field');
|
expect(result.data[0].datapoints[0].fieldProp).toBe('field');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with bucket_script ', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
targets = [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
metrics: [
|
||||||
|
{ id: '1', type: 'sum', field: '@value' },
|
||||||
|
{ id: '3', type: 'max', field: '@value' },
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
field: 'select field',
|
||||||
|
pipelineVariables: [{ name: 'var1', pipelineAgg: '1' }, { name: 'var2', pipelineAgg: '3' }],
|
||||||
|
settings: { script: 'params.var1 * params.var2' },
|
||||||
|
type: 'bucket_script',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
response = {
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
aggregations: {
|
||||||
|
'2': {
|
||||||
|
buckets: [
|
||||||
|
{
|
||||||
|
1: { value: 2 },
|
||||||
|
3: { value: 3 },
|
||||||
|
4: { value: 6 },
|
||||||
|
doc_count: 60,
|
||||||
|
key: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
1: { value: 3 },
|
||||||
|
3: { value: 4 },
|
||||||
|
4: { value: 12 },
|
||||||
|
doc_count: 60,
|
||||||
|
key: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
result = new ElasticResponse(targets, response).getTimeSeries();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 3 series', () => {
|
||||||
|
expect(result.data.length).toBe(3);
|
||||||
|
expect(result.data[0].datapoints.length).toBe(2);
|
||||||
|
expect(result.data[0].target).toBe('Sum @value');
|
||||||
|
expect(result.data[1].target).toBe('Max @value');
|
||||||
|
expect(result.data[2].target).toBe('Sum @value * Max @value');
|
||||||
|
expect(result.data[0].datapoints[0][0]).toBe(2);
|
||||||
|
expect(result.data[1].datapoints[0][0]).toBe(3);
|
||||||
|
expect(result.data[2].datapoints[0][0]).toBe(6);
|
||||||
|
expect(result.data[0].datapoints[1][0]).toBe(3);
|
||||||
|
expect(result.data[1].datapoints[1][0]).toBe(4);
|
||||||
|
expect(result.data[2].datapoints[1][0]).toBe(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -353,6 +353,83 @@ describe('ElasticQueryBuilder', () => {
|
|||||||
expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('_count');
|
expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('_count');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('with bucket_script', () => {
|
||||||
|
const query = builder.build({
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'sum',
|
||||||
|
field: '@value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'max',
|
||||||
|
field: '@value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'select field',
|
||||||
|
id: '4',
|
||||||
|
meta: {},
|
||||||
|
pipelineVariables: [
|
||||||
|
{
|
||||||
|
name: 'var1',
|
||||||
|
pipelineAgg: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'var2',
|
||||||
|
pipelineAgg: '3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
script: 'params.var1 * params.var2',
|
||||||
|
},
|
||||||
|
type: 'bucket_script',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstLevel = query.aggs['2'];
|
||||||
|
|
||||||
|
expect(firstLevel.aggs['4']).not.toBe(undefined);
|
||||||
|
expect(firstLevel.aggs['4'].bucket_script).not.toBe(undefined);
|
||||||
|
expect(firstLevel.aggs['4'].bucket_script.buckets_path).toMatchObject({ var1: '1', var2: '3' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with bucket_script doc count', () => {
|
||||||
|
const query = builder.build({
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'count',
|
||||||
|
field: 'select field',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'select field',
|
||||||
|
id: '4',
|
||||||
|
meta: {},
|
||||||
|
pipelineVariables: [
|
||||||
|
{
|
||||||
|
name: 'var1',
|
||||||
|
pipelineAgg: '3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
script: 'params.var1 * 1000',
|
||||||
|
},
|
||||||
|
type: 'bucket_script',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstLevel = query.aggs['2'];
|
||||||
|
|
||||||
|
expect(firstLevel.aggs['4']).not.toBe(undefined);
|
||||||
|
expect(firstLevel.aggs['4'].bucket_script).not.toBe(undefined);
|
||||||
|
expect(firstLevel.aggs['4'].bucket_script.buckets_path).toMatchObject({ var1: '_count' });
|
||||||
|
});
|
||||||
|
|
||||||
it('with histogram', () => {
|
it('with histogram', () => {
|
||||||
const query = builder.build({
|
const query = builder.build({
|
||||||
metrics: [{ id: '1', type: 'count' }],
|
metrics: [{ id: '1', type: 'count' }],
|
||||||
|
|||||||
@@ -65,6 +65,24 @@ describe('ElasticQueryDef', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isPipelineAggWithMultipleBucketPaths', () => {
|
||||||
|
describe('bucket_script', () => {
|
||||||
|
const result = queryDef.isPipelineAggWithMultipleBucketPaths('bucket_script');
|
||||||
|
|
||||||
|
test('should have multiple bucket paths support', () => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moving_avg', () => {
|
||||||
|
const result = queryDef.isPipelineAggWithMultipleBucketPaths('moving_avg');
|
||||||
|
|
||||||
|
test('should not have multiple bucket paths support', () => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('pipeline aggs depending on esverison', () => {
|
describe('pipeline aggs depending on esverison', () => {
|
||||||
describe('using esversion undefined', () => {
|
describe('using esversion undefined', () => {
|
||||||
test('should not get pipeline aggs', () => {
|
test('should not get pipeline aggs', () => {
|
||||||
@@ -80,13 +98,13 @@ describe('ElasticQueryDef', () => {
|
|||||||
|
|
||||||
describe('using esversion 2', () => {
|
describe('using esversion 2', () => {
|
||||||
test('should get pipeline aggs', () => {
|
test('should get pipeline aggs', () => {
|
||||||
expect(queryDef.getMetricAggTypes(2).length).toBe(11);
|
expect(queryDef.getMetricAggTypes(2).length).toBe(12);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('using esversion 5', () => {
|
describe('using esversion 5', () => {
|
||||||
test('should get pipeline aggs', () => {
|
test('should get pipeline aggs', () => {
|
||||||
expect(queryDef.getMetricAggTypes(5).length).toBe(11);
|
expect(queryDef.getMetricAggTypes(5).length).toBe(12);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -219,8 +219,9 @@ export class PrometheusDatasource {
|
|||||||
};
|
};
|
||||||
const range = Math.ceil(end - start);
|
const range = Math.ceil(end - start);
|
||||||
|
|
||||||
|
// options.interval is the dynamically calculated interval
|
||||||
let interval = kbn.interval_to_seconds(options.interval);
|
let interval = kbn.interval_to_seconds(options.interval);
|
||||||
// Minimum interval ("Min step"), if specified for the query. or same as interval otherwise
|
// Minimum interval ("Min step"), if specified for the query or datasource. or same as interval otherwise
|
||||||
const minInterval = kbn.interval_to_seconds(
|
const minInterval = kbn.interval_to_seconds(
|
||||||
this.templateSrv.replace(target.interval, options.scopedVars) || options.interval
|
this.templateSrv.replace(target.interval, options.scopedVars) || options.interval
|
||||||
);
|
);
|
||||||
@@ -366,12 +367,13 @@ export class PrometheusDatasource {
|
|||||||
const step = annotation.step || '60s';
|
const step = annotation.step || '60s';
|
||||||
const start = this.getPrometheusTime(options.range.from, false);
|
const start = this.getPrometheusTime(options.range.from, false);
|
||||||
const end = this.getPrometheusTime(options.range.to, true);
|
const end = this.getPrometheusTime(options.range.to, true);
|
||||||
// Unsetting min interval
|
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
...options,
|
...options,
|
||||||
interval: '0s',
|
interval: step,
|
||||||
};
|
};
|
||||||
const query = this.createQuery({ expr, interval: step }, queryOptions, start, end);
|
// Unsetting min interval for accurate event resolution
|
||||||
|
const minStep = '1s';
|
||||||
|
const query = this.createQuery({ expr, interval: minStep }, queryOptions, start, end);
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
return this.performTimeSeriesQuery(query, query.start, query.end).then(results => {
|
return this.performTimeSeriesQuery(query, query.start, query.end).then(results => {
|
||||||
|
|||||||
@@ -577,6 +577,79 @@ describe('PrometheusDatasource', () => {
|
|||||||
expect(results[0].time).toEqual(1);
|
expect(results[0].time).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('step parameter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||||
|
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default step for short range if no interval is given', () => {
|
||||||
|
const query = {
|
||||||
|
...options,
|
||||||
|
range: {
|
||||||
|
from: time({ seconds: 63 }),
|
||||||
|
to: time({ seconds: 123 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ctx.ds.annotationQuery(query);
|
||||||
|
const req = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||||
|
expect(req.url).toContain('step=60');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use custom step for short range', () => {
|
||||||
|
const annotation = {
|
||||||
|
...options.annotation,
|
||||||
|
step: '10s',
|
||||||
|
};
|
||||||
|
const query = {
|
||||||
|
...options,
|
||||||
|
annotation,
|
||||||
|
range: {
|
||||||
|
from: time({ seconds: 63 }),
|
||||||
|
to: time({ seconds: 123 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ctx.ds.annotationQuery(query);
|
||||||
|
const req = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||||
|
expect(req.url).toContain('step=10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use custom step for short range', () => {
|
||||||
|
const annotation = {
|
||||||
|
...options.annotation,
|
||||||
|
step: '10s',
|
||||||
|
};
|
||||||
|
const query = {
|
||||||
|
...options,
|
||||||
|
annotation,
|
||||||
|
range: {
|
||||||
|
from: time({ seconds: 63 }),
|
||||||
|
to: time({ seconds: 123 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ctx.ds.annotationQuery(query);
|
||||||
|
const req = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||||
|
expect(req.url).toContain('step=10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use dynamic step on long ranges if no option was given', () => {
|
||||||
|
const query = {
|
||||||
|
...options,
|
||||||
|
range: {
|
||||||
|
from: time({ seconds: 63 }),
|
||||||
|
to: time({ hours: 24 * 30, seconds: 63 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ctx.ds.annotationQuery(query);
|
||||||
|
const req = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||||
|
// Range in seconds: (to - from) / 1000
|
||||||
|
// Max_datapoints: 11000
|
||||||
|
// Step: range / max_datapoints
|
||||||
|
const step = 236;
|
||||||
|
expect(req.url).toContain(`step=${step}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When resultFormat is table and instant = true', () => {
|
describe('When resultFormat is table and instant = true', () => {
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
import _ from 'lodash';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import './query_filter_ctrl';
|
|
||||||
|
|
||||||
export class StackdriverAnnotationsQueryCtrl {
|
export class StackdriverAnnotationsQueryCtrl {
|
||||||
static templateUrl = 'partials/annotations.editor.html';
|
static templateUrl = 'partials/annotations.editor.html';
|
||||||
annotation: any;
|
annotation: any;
|
||||||
datasource: any;
|
templateSrv: TemplateSrv;
|
||||||
|
|
||||||
defaultDropdownValue = 'Select Metric';
|
|
||||||
defaultServiceValue = 'All Services';
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
project: {
|
|
||||||
id: 'default',
|
|
||||||
name: 'loading project...',
|
|
||||||
},
|
|
||||||
metricType: this.defaultDropdownValue,
|
|
||||||
service: this.defaultServiceValue,
|
|
||||||
metric: '',
|
|
||||||
filters: [],
|
|
||||||
metricKind: '',
|
|
||||||
valueType: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor() {
|
constructor(templateSrv) {
|
||||||
|
this.templateSrv = templateSrv;
|
||||||
this.annotation.target = this.annotation.target || {};
|
this.annotation.target = this.annotation.target || {};
|
||||||
this.annotation.target.refId = 'annotationQuery';
|
this.onQueryChange = this.onQueryChange.bind(this);
|
||||||
_.defaultsDeep(this.annotation.target, this.defaults);
|
}
|
||||||
|
|
||||||
|
onQueryChange(target) {
|
||||||
|
Object.assign(this.annotation.target, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import { Aggregations, Props } from './Aggregations';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { ValueTypes, MetricKind } from '../constants';
|
||||||
|
import { TemplateSrvStub } from 'test/specs/helpers';
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
onChange: () => {},
|
||||||
|
templateSrv: new TemplateSrvStub(),
|
||||||
|
metricDescriptor: {
|
||||||
|
valueType: '',
|
||||||
|
metricKind: '',
|
||||||
|
},
|
||||||
|
crossSeriesReducer: '',
|
||||||
|
groupBys: [],
|
||||||
|
children: renderProps => <div />,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Aggregations', () => {
|
||||||
|
let wrapper;
|
||||||
|
it('renders correctly', () => {
|
||||||
|
const tree = renderer.create(<Aggregations {...props} />).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('options', () => {
|
||||||
|
describe('when DOUBLE and DELTA is passed as props', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const newProps = { ...props, metricDescriptor: { valueType: ValueTypes.DOUBLE, metricKind: MetricKind.GAUGE } };
|
||||||
|
wrapper = shallow(<Aggregations {...newProps} />);
|
||||||
|
});
|
||||||
|
it('', () => {
|
||||||
|
const options = wrapper.state().aggOptions[0].options;
|
||||||
|
expect(options.length).toEqual(11);
|
||||||
|
expect(options.map(o => o.value)).toEqual(
|
||||||
|
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when MONEY and CUMULATIVE is passed as props', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const newProps = {
|
||||||
|
...props,
|
||||||
|
metricDescriptor: { valueType: ValueTypes.MONEY, metricKind: MetricKind.CUMULATIVE },
|
||||||
|
};
|
||||||
|
wrapper = shallow(<Aggregations {...newProps} />);
|
||||||
|
});
|
||||||
|
it('', () => {
|
||||||
|
const options = wrapper.state().aggOptions[0].options;
|
||||||
|
expect(options.length).toEqual(5);
|
||||||
|
expect(options.map(o => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
|
||||||
|
import { getAggregationOptionsByMetric } from '../functions';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (metricDescriptor) => void;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
metricDescriptor: {
|
||||||
|
valueType: string;
|
||||||
|
metricKind: string;
|
||||||
|
};
|
||||||
|
crossSeriesReducer: string;
|
||||||
|
groupBys: string[];
|
||||||
|
children?: (renderProps: any) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
aggOptions: any[];
|
||||||
|
displayAdvancedOptions: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Aggregations extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
aggOptions: [],
|
||||||
|
displayAdvancedOptions: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setAggOptions(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
this.setAggOptions(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAggOptions({ metricDescriptor }: Props) {
|
||||||
|
let aggOptions = [];
|
||||||
|
if (metricDescriptor) {
|
||||||
|
aggOptions = [
|
||||||
|
{
|
||||||
|
label: 'Aggregations',
|
||||||
|
expanded: true,
|
||||||
|
options: getAggregationOptionsByMetric(metricDescriptor.valueType, metricDescriptor.metricKind).map(a => ({
|
||||||
|
...a,
|
||||||
|
label: a.text,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
this.setState({ aggOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleDisplayAdvanced = () => {
|
||||||
|
this.setState(state => ({
|
||||||
|
displayAdvancedOptions: !state.displayAdvancedOptions,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { displayAdvancedOptions, aggOptions } = this.state;
|
||||||
|
const { templateSrv, onChange, crossSeriesReducer } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label query-keyword width-9">Aggregation</label>
|
||||||
|
<MetricSelect
|
||||||
|
onChange={onChange}
|
||||||
|
value={crossSeriesReducer}
|
||||||
|
variables={templateSrv.variables}
|
||||||
|
options={aggOptions}
|
||||||
|
placeholder="Select Aggregation"
|
||||||
|
className="width-15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<label className="gf-form-label gf-form-label--grow">
|
||||||
|
<a onClick={this.onToggleDisplayAdvanced}>
|
||||||
|
<>
|
||||||
|
<i className={`fa fa-caret-${displayAdvancedOptions ? 'down' : 'right'}`} /> Advanced Options
|
||||||
|
</>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.props.children(this.state.displayAdvancedOptions)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (alignmentPeriod) => void;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AliasBy extends Component<Props, State> {
|
||||||
|
propagateOnChange: (value) => void;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.propagateOnChange = debounce(this.props.onChange, 500);
|
||||||
|
this.state = { value: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({ value: this.props.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (nextProps.value !== this.props.value) {
|
||||||
|
this.setState({ value: nextProps.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = e => {
|
||||||
|
this.setState({ value: e.target.value });
|
||||||
|
this.propagateOnChange(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label query-keyword width-9">Alias By</label>
|
||||||
|
<input type="text" className="gf-form-input width-24" value={this.state.value} onChange={this.onChange} />
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
|
||||||
|
import { alignmentPeriods, alignOptions } from '../constants';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (alignmentPeriod) => void;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
alignmentPeriod: string;
|
||||||
|
perSeriesAligner: string;
|
||||||
|
usedAlignmentPeriod: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlignmentPeriods: SFC<Props> = ({
|
||||||
|
alignmentPeriod,
|
||||||
|
templateSrv,
|
||||||
|
onChange,
|
||||||
|
perSeriesAligner,
|
||||||
|
usedAlignmentPeriod,
|
||||||
|
}) => {
|
||||||
|
const alignment = alignOptions.find(ap => ap.value === templateSrv.replace(perSeriesAligner));
|
||||||
|
const formatAlignmentText = `${kbn.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
|
||||||
|
<MetricSelect
|
||||||
|
onChange={onChange}
|
||||||
|
value={alignmentPeriod}
|
||||||
|
variables={templateSrv.variables}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Alignment options',
|
||||||
|
expanded: true,
|
||||||
|
options: alignmentPeriods.map(ap => ({
|
||||||
|
...ap,
|
||||||
|
label: ap.text,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
placeholder="Select Alignment"
|
||||||
|
className="width-15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { SelectOptionItem } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (perSeriesAligner) => void;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
alignOptions: SelectOptionItem[];
|
||||||
|
perSeriesAligner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Alignments: SFC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form offset-width-9">
|
||||||
|
<label className="gf-form-label query-keyword width-15">Aligner</label>
|
||||||
|
<MetricSelect
|
||||||
|
onChange={onChange}
|
||||||
|
value={perSeriesAligner}
|
||||||
|
variables={templateSrv.variables}
|
||||||
|
options={alignOptions}
|
||||||
|
placeholder="Select Alignment"
|
||||||
|
className="width-15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import StackdriverDatasource from '../datasource';
|
||||||
|
import { Metrics } from './Metrics';
|
||||||
|
import { Filter } from './Filter';
|
||||||
|
import { AnnotationTarget } from '../types';
|
||||||
|
import { AnnotationsHelp } from './AnnotationsHelp';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onQueryChange: (target: AnnotationTarget) => void;
|
||||||
|
target: AnnotationTarget;
|
||||||
|
datasource: StackdriverDatasource;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State extends AnnotationTarget {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultTarget: State = {
|
||||||
|
defaultProject: 'loading project...',
|
||||||
|
metricType: '',
|
||||||
|
filters: [],
|
||||||
|
metricKind: '',
|
||||||
|
valueType: '',
|
||||||
|
refId: 'annotationQuery',
|
||||||
|
title: '',
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AnnotationQueryEditor extends React.Component<Props, State> {
|
||||||
|
state: State = DefaultTarget;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({
|
||||||
|
...this.props.target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMetricTypeChange = ({ valueType, metricKind, type, unit }) => {
|
||||||
|
const { onQueryChange } = this.props;
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
metricType: type,
|
||||||
|
unit,
|
||||||
|
valueType,
|
||||||
|
metricKind,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
onQueryChange(this.state);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange(prop, value) {
|
||||||
|
this.setState({ [prop]: value }, () => {
|
||||||
|
this.props.onQueryChange(this.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { defaultProject, metricType, filters, refId, title, text } = this.state;
|
||||||
|
const { datasource, templateSrv } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Metrics
|
||||||
|
defaultProject={defaultProject}
|
||||||
|
metricType={metricType}
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
datasource={datasource}
|
||||||
|
onChange={this.onMetricTypeChange}
|
||||||
|
>
|
||||||
|
{metric => (
|
||||||
|
<>
|
||||||
|
<Filter
|
||||||
|
filtersChanged={value => this.onChange('filters', value)}
|
||||||
|
filters={filters}
|
||||||
|
refId={refId}
|
||||||
|
hideGroupBys={true}
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
datasource={datasource}
|
||||||
|
metricType={metric ? metric.type : ''}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Metrics>
|
||||||
|
<div className="gf-form gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label query-keyword width-9">Title</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input width-20"
|
||||||
|
value={title}
|
||||||
|
onChange={e => this.onChange('title', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label query-keyword width-9">Text</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input width-20"
|
||||||
|
value={text}
|
||||||
|
onChange={e => this.onChange('text', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnnotationsHelp />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
export const AnnotationsHelp: SFC = () => {
|
||||||
|
return (
|
||||||
|
<div className="gf-form grafana-info-box" style={{ padding: 0 }}>
|
||||||
|
<pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>
|
||||||
|
<h5>Annotation Query Format</h5>
|
||||||
|
<p>
|
||||||
|
An annotation is an event that is overlaid on top of graphs. Annotation rendering is expensive so it is
|
||||||
|
important to limit the number of rows returned.{' '}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The Title and Text fields support templating and can use data returned from the query. For example, the Title
|
||||||
|
field could have the following text:
|
||||||
|
</p>
|
||||||
|
<code>
|
||||||
|
{`${'{{metric.type}}'}`} has value: {`${'{{metric.value}}'}`}
|
||||||
|
</code>
|
||||||
|
<p>
|
||||||
|
Example Result: <code>monitoring.googleapis.com/uptime_check/http_status has this value: 502</code>
|
||||||
|
</p>
|
||||||
|
<label>Patterns:</label>
|
||||||
|
<p>
|
||||||
|
<code>{`${'{{metric.value}}'}`}</code> = value of the metric/point
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g. metric.label.instance_name
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
|
||||||
|
</p>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
|
import { QueryMeta } from '../types';
|
||||||
|
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import StackdriverDatasource from '../datasource';
|
||||||
|
import '../query_filter_ctrl';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
filtersChanged: (filters: string[]) => void;
|
||||||
|
groupBysChanged?: (groupBys: string[]) => void;
|
||||||
|
metricType: string;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
groupBys?: string[];
|
||||||
|
filters: string[];
|
||||||
|
datasource: StackdriverDatasource;
|
||||||
|
refId: string;
|
||||||
|
hideGroupBys: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
labelData: QueryMeta;
|
||||||
|
loading: Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelData = {
|
||||||
|
metricLabels: {},
|
||||||
|
resourceLabels: {},
|
||||||
|
resourceTypes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Filter extends React.Component<Props, State> {
|
||||||
|
element: any;
|
||||||
|
component: AngularComponent;
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
if (!this.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { groupBys, filters, hideGroupBys } = this.props;
|
||||||
|
const loader = getAngularLoader();
|
||||||
|
|
||||||
|
const filtersChanged = filters => {
|
||||||
|
this.props.filtersChanged(filters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupBysChanged = groupBys => {
|
||||||
|
this.props.groupBysChanged(groupBys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopeProps = {
|
||||||
|
loading: null,
|
||||||
|
labelData,
|
||||||
|
groupBys,
|
||||||
|
filters,
|
||||||
|
filtersChanged,
|
||||||
|
groupBysChanged,
|
||||||
|
hideGroupBys,
|
||||||
|
};
|
||||||
|
const loading = this.loadLabels(scopeProps);
|
||||||
|
scopeProps.loading = loading;
|
||||||
|
const template = `<stackdriver-filter
|
||||||
|
filters="filters"
|
||||||
|
group-bys="groupBys"
|
||||||
|
label-data="labelData"
|
||||||
|
loading="loading"
|
||||||
|
filters-changed="filtersChanged(filters)"
|
||||||
|
group-bys-changed="groupBysChanged(groupBys)"
|
||||||
|
hide-group-bys="hideGroupBys"/>`;
|
||||||
|
this.component = loader.load(this.element, scopeProps, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (!this.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scope = this.component.getScope();
|
||||||
|
if (prevProps.metricType !== this.props.metricType) {
|
||||||
|
scope.loading = this.loadLabels(scope);
|
||||||
|
}
|
||||||
|
scope.filters = this.props.filters;
|
||||||
|
scope.groupBys = this.props.groupBys;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.component) {
|
||||||
|
this.component.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLabels(scope) {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
try {
|
||||||
|
if (!this.props.metricType) {
|
||||||
|
scope.labelData = labelData;
|
||||||
|
} else {
|
||||||
|
const { meta } = await this.props.datasource.getLabels(this.props.metricType, this.props.refId);
|
||||||
|
scope.labelData = meta;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.props.metricType]);
|
||||||
|
scope.labelData = labelData;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div ref={element => (this.element = element)} style={{ width: '100%' }} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Project } from './Project';
|
||||||
|
import StackdriverDatasource from '../datasource';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
datasource: StackdriverDatasource;
|
||||||
|
rawQuery: string;
|
||||||
|
lastQueryError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
displayHelp: boolean;
|
||||||
|
displaRawQuery: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Help extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
displayHelp: false,
|
||||||
|
displaRawQuery: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
onHelpClicked = () => {
|
||||||
|
this.setState({ displayHelp: !this.state.displayHelp });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRawQueryClicked = () => {
|
||||||
|
this.setState({ displaRawQuery: !this.state.displaRawQuery });
|
||||||
|
};
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
return nextProps.metricDescriptor !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { displayHelp, displaRawQuery } = this.state;
|
||||||
|
const { datasource, rawQuery, lastQueryError } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<Project datasource={datasource} />
|
||||||
|
<div className="gf-form" onClick={this.onHelpClicked}>
|
||||||
|
<label className="gf-form-label query-keyword pointer">
|
||||||
|
Show Help <i className={`fa fa-caret-${displayHelp ? 'down' : 'right'}`} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rawQuery && (
|
||||||
|
<div className="gf-form" onClick={this.onRawQueryClicked}>
|
||||||
|
<label className="gf-form-label query-keyword">
|
||||||
|
Raw Query <i className={`fa fa-caret-${displaRawQuery ? 'down' : 'right'}`} ng-show="ctrl.showHelp" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{rawQuery &&
|
||||||
|
displaRawQuery && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<pre className="gf-form-pre">{rawQuery}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{displayHelp && (
|
||||||
|
<div className="gf-form grafana-info-box" style={{ padding: 0 }}>
|
||||||
|
<pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>
|
||||||
|
<h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns. Format the legend
|
||||||
|
keys any way you want by using alias patterns.<br /> <br />
|
||||||
|
Example:
|
||||||
|
<code>{`${'{{metricDescriptor.name}} - {{metricDescriptor.label.instance_name}}'}`}</code>
|
||||||
|
<br />
|
||||||
|
Result: <code>cpu/usage_time - server1-europe-west-1</code>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<strong>Patterns</strong>
|
||||||
|
<br />
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>{`${'{{metricDescriptor.type}}'}`}</code> = metric type e.g.
|
||||||
|
compute.googleapis.com/instance/cpu/usage_time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{`${'{{metricDescriptor.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{`${'{{metricDescriptor.service}}'}`}</code> = service part of metric e.g. compute
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{`${'{{metricDescriptor.label.label_name}}'}`}</code> = Metric label metadata e.g.
|
||||||
|
metricDescriptor.label.instance_name
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
|
||||||
|
Grafana
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lastQueryError && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<pre className="gf-form-pre alert alert-error">{lastQueryError}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import StackdriverDatasource from '../datasource';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { MetricDescriptor } from '../types';
|
||||||
|
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (metricDescriptor: MetricDescriptor) => void;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
datasource: StackdriverDatasource;
|
||||||
|
defaultProject: string;
|
||||||
|
metricType: string;
|
||||||
|
children?: (renderProps: any) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
metricDescriptors: MetricDescriptor[];
|
||||||
|
metrics: any[];
|
||||||
|
services: any[];
|
||||||
|
service: string;
|
||||||
|
metric: string;
|
||||||
|
metricDescriptor: MetricDescriptor;
|
||||||
|
defaultProject: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Metrics extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
metricDescriptors: [],
|
||||||
|
metrics: [],
|
||||||
|
services: [],
|
||||||
|
service: '',
|
||||||
|
metric: '',
|
||||||
|
metricDescriptor: null,
|
||||||
|
defaultProject: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({ defaultProject: this.props.defaultProject }, () => {
|
||||||
|
this.getCurrentProject()
|
||||||
|
.then(this.loadMetricDescriptors.bind(this))
|
||||||
|
.then(this.initializeServiceAndMetrics.bind(this));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentProject() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!this.state.defaultProject || this.state.defaultProject === 'loading project...') {
|
||||||
|
const defaultProject = await this.props.datasource.getDefaultProject();
|
||||||
|
this.setState({ defaultProject });
|
||||||
|
}
|
||||||
|
resolve(this.state.defaultProject);
|
||||||
|
} catch (error) {
|
||||||
|
appEvents.emit('ds-request-error', error);
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMetricDescriptors() {
|
||||||
|
if (this.state.defaultProject !== 'loading project...') {
|
||||||
|
const metricDescriptors = await this.props.datasource.getMetricTypes(this.state.defaultProject);
|
||||||
|
this.setState({ metricDescriptors });
|
||||||
|
return metricDescriptors;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeServiceAndMetrics() {
|
||||||
|
const { metricDescriptors } = this.state;
|
||||||
|
const services = this.getServicesList(metricDescriptors);
|
||||||
|
const metrics = this.getMetricsList(metricDescriptors);
|
||||||
|
const service = metrics.length > 0 ? metrics[0].service : '';
|
||||||
|
const metricDescriptor = this.getSelectedMetricDescriptor(this.props.metricType);
|
||||||
|
this.setState({ metricDescriptors, services, metrics, service: service, metricDescriptor });
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedMetricDescriptor(metricType) {
|
||||||
|
return this.state.metricDescriptors.find(md => md.type === this.props.templateSrv.replace(metricType));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetricsList(metricDescriptors: MetricDescriptor[]) {
|
||||||
|
const selectedMetricDescriptor = this.getSelectedMetricDescriptor(this.props.metricType);
|
||||||
|
if (!selectedMetricDescriptor) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const metricsByService = metricDescriptors.filter(m => m.service === selectedMetricDescriptor.service).map(m => ({
|
||||||
|
service: m.service,
|
||||||
|
value: m.type,
|
||||||
|
label: m.displayName,
|
||||||
|
description: m.description,
|
||||||
|
}));
|
||||||
|
return metricsByService;
|
||||||
|
}
|
||||||
|
|
||||||
|
onServiceChange = service => {
|
||||||
|
const { metricDescriptors } = this.state;
|
||||||
|
const { templateSrv, metricType } = this.props;
|
||||||
|
|
||||||
|
const metrics = metricDescriptors.filter(m => m.service === templateSrv.replace(service)).map(m => ({
|
||||||
|
service: m.service,
|
||||||
|
value: m.type,
|
||||||
|
label: m.displayName,
|
||||||
|
description: m.description,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.setState({ service, metrics });
|
||||||
|
|
||||||
|
if (metrics.length > 0 && !metrics.some(m => m.value === templateSrv.replace(metricType))) {
|
||||||
|
this.onMetricTypeChange(metrics[0].value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMetricTypeChange = value => {
|
||||||
|
const metricDescriptor = this.getSelectedMetricDescriptor(value);
|
||||||
|
this.setState({ metricDescriptor });
|
||||||
|
this.props.onChange({ ...metricDescriptor, type: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
getServicesList(metricDescriptors: MetricDescriptor[]) {
|
||||||
|
const services = metricDescriptors.map(m => ({
|
||||||
|
value: m.service,
|
||||||
|
label: _.startCase(m.serviceShortName),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return services.length > 0 ? _.uniqBy(services, s => s.value) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getTemplateVariablesGroup() {
|
||||||
|
return {
|
||||||
|
label: 'Template Variables',
|
||||||
|
options: this.props.templateSrv.variables.map(v => ({
|
||||||
|
label: `$${v.name}`,
|
||||||
|
value: `$${v.name}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { services, service, metrics } = this.state;
|
||||||
|
const { metricType, templateSrv } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label width-9 query-keyword">Service</span>
|
||||||
|
<MetricSelect
|
||||||
|
onChange={this.onServiceChange}
|
||||||
|
value={service}
|
||||||
|
options={services}
|
||||||
|
isSearchable={false}
|
||||||
|
placeholder="Select Services"
|
||||||
|
className="width-15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label width-9 query-keyword">Metric</span>
|
||||||
|
<MetricSelect
|
||||||
|
onChange={this.onMetricTypeChange}
|
||||||
|
value={metricType}
|
||||||
|
variables={templateSrv.variables}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Metrics',
|
||||||
|
expanded: true,
|
||||||
|
options: metrics,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
placeholder="Select Metric"
|
||||||
|
className="width-15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.props.children(this.state.metricDescriptor)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import StackdriverDatasource from '../datasource';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
datasource: StackdriverDatasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Project extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
projectName: 'Loading project...',
|
||||||
|
};
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const projectName = await this.props.datasource.getDefaultProject();
|
||||||
|
this.setState({ projectName });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { projectName } = this.state;
|
||||||
|
return (
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label width-9 query-keyword">Project</span>
|
||||||
|
<input className="gf-form-input width-15" disabled type="text" value={projectName} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import { QueryEditor, Props, DefaultTarget } from './QueryEditor';
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
onQueryChange: target => {},
|
||||||
|
onExecuteQuery: () => {},
|
||||||
|
target: DefaultTarget,
|
||||||
|
events: { on: () => {} },
|
||||||
|
datasource: {
|
||||||
|
getDefaultProject: () => Promise.resolve('project'),
|
||||||
|
getMetricTypes: () => Promise.resolve([]),
|
||||||
|
} as any,
|
||||||
|
templateSrv: new TemplateSrv(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('QueryEditor', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
const tree = renderer.create(<QueryEditor {...props} />).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { Metrics } from './Metrics';
|
||||||
|
import { Filter } from './Filter';
|
||||||
|
import { Aggregations } from './Aggregations';
|
||||||
|
import { Alignments } from './Alignments';
|
||||||
|
import { AlignmentPeriods } from './AlignmentPeriods';
|
||||||
|
import { AliasBy } from './AliasBy';
|
||||||
|
import { Help } from './Help';
|
||||||
|
import { Target, MetricDescriptor } from '../types';
|
||||||
|
import { getAlignmentPickerData } from '../functions';
|
||||||
|
import StackdriverDatasource from '../datasource';
|
||||||
|
import { SelectOptionItem } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onQueryChange: (target: Target) => void;
|
||||||
|
onExecuteQuery: () => void;
|
||||||
|
target: Target;
|
||||||
|
events: any;
|
||||||
|
datasource: StackdriverDatasource;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State extends Target {
|
||||||
|
alignOptions: SelectOptionItem[];
|
||||||
|
lastQuery: string;
|
||||||
|
lastQueryError: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultTarget: State = {
|
||||||
|
defaultProject: 'loading project...',
|
||||||
|
metricType: '',
|
||||||
|
metricKind: '',
|
||||||
|
valueType: '',
|
||||||
|
refId: '',
|
||||||
|
service: '',
|
||||||
|
unit: '',
|
||||||
|
crossSeriesReducer: 'REDUCE_MEAN',
|
||||||
|
alignmentPeriod: 'stackdriver-auto',
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
groupBys: [],
|
||||||
|
filters: [],
|
||||||
|
aliasBy: '',
|
||||||
|
alignOptions: [],
|
||||||
|
lastQuery: '',
|
||||||
|
lastQueryError: '',
|
||||||
|
usedAlignmentPeriod: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class QueryEditor extends React.Component<Props, State> {
|
||||||
|
state: State = DefaultTarget;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { events, target, templateSrv } = this.props;
|
||||||
|
events.on('data-received', this.onDataReceived.bind(this));
|
||||||
|
events.on('data-error', this.onDataError.bind(this));
|
||||||
|
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
|
||||||
|
this.setState({
|
||||||
|
...this.props.target,
|
||||||
|
alignOptions,
|
||||||
|
perSeriesAligner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.events.off('data-received', this.onDataReceived);
|
||||||
|
this.props.events.off('data-error', this.onDataError);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataReceived(dataList) {
|
||||||
|
const series = dataList.find(item => item.refId === this.props.target.refId);
|
||||||
|
if (series) {
|
||||||
|
this.setState({
|
||||||
|
lastQuery: decodeURIComponent(series.meta.rawQuery),
|
||||||
|
lastQueryError: '',
|
||||||
|
usedAlignmentPeriod: series.meta.alignmentPeriod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataError(err) {
|
||||||
|
let lastQuery;
|
||||||
|
let lastQueryError;
|
||||||
|
if (err.data && err.data.error) {
|
||||||
|
lastQueryError = this.props.datasource.formatStackdriverError(err);
|
||||||
|
} else if (err.data && err.data.results) {
|
||||||
|
const queryRes = err.data.results[this.props.target.refId];
|
||||||
|
lastQuery = decodeURIComponent(queryRes.meta.rawQuery);
|
||||||
|
if (queryRes && queryRes.error) {
|
||||||
|
try {
|
||||||
|
lastQueryError = JSON.parse(queryRes.error).error.message;
|
||||||
|
} catch {
|
||||||
|
lastQueryError = queryRes.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ lastQuery, lastQueryError });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
|
||||||
|
const { templateSrv, onQueryChange, onExecuteQuery } = this.props;
|
||||||
|
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
|
||||||
|
{ valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
|
||||||
|
templateSrv
|
||||||
|
);
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
alignOptions,
|
||||||
|
perSeriesAligner,
|
||||||
|
metricType: type,
|
||||||
|
unit,
|
||||||
|
valueType,
|
||||||
|
metricKind,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
onQueryChange(this.state);
|
||||||
|
onExecuteQuery();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onPropertyChange(prop, value) {
|
||||||
|
this.setState({ [prop]: value }, () => {
|
||||||
|
this.props.onQueryChange(this.state);
|
||||||
|
this.props.onExecuteQuery();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
usedAlignmentPeriod,
|
||||||
|
defaultProject,
|
||||||
|
metricType,
|
||||||
|
crossSeriesReducer,
|
||||||
|
groupBys,
|
||||||
|
filters,
|
||||||
|
perSeriesAligner,
|
||||||
|
alignOptions,
|
||||||
|
alignmentPeriod,
|
||||||
|
aliasBy,
|
||||||
|
lastQuery,
|
||||||
|
lastQueryError,
|
||||||
|
refId,
|
||||||
|
} = this.state;
|
||||||
|
const { datasource, templateSrv } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Metrics
|
||||||
|
defaultProject={defaultProject}
|
||||||
|
metricType={metricType}
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
datasource={datasource}
|
||||||
|
onChange={this.onMetricTypeChange}
|
||||||
|
>
|
||||||
|
{metric => (
|
||||||
|
<>
|
||||||
|
<Filter
|
||||||
|
filtersChanged={value => this.onPropertyChange('filters', value)}
|
||||||
|
groupBysChanged={value => this.onPropertyChange('groupBys', value)}
|
||||||
|
filters={filters}
|
||||||
|
groupBys={groupBys}
|
||||||
|
refId={refId}
|
||||||
|
hideGroupBys={false}
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
datasource={datasource}
|
||||||
|
metricType={metric ? metric.type : ''}
|
||||||
|
/>
|
||||||
|
<Aggregations
|
||||||
|
metricDescriptor={metric}
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
crossSeriesReducer={crossSeriesReducer}
|
||||||
|
groupBys={groupBys}
|
||||||
|
onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
|
||||||
|
>
|
||||||
|
{displayAdvancedOptions =>
|
||||||
|
displayAdvancedOptions && (
|
||||||
|
<Alignments
|
||||||
|
alignOptions={alignOptions}
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
perSeriesAligner={perSeriesAligner}
|
||||||
|
onChange={value => this.onPropertyChange('perSeriesAligner', value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Aggregations>
|
||||||
|
<AlignmentPeriods
|
||||||
|
templateSrv={templateSrv}
|
||||||
|
alignmentPeriod={alignmentPeriod}
|
||||||
|
perSeriesAligner={perSeriesAligner}
|
||||||
|
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||||
|
onChange={value => this.onPropertyChange('alignmentPeriod', value)}
|
||||||
|
/>
|
||||||
|
<AliasBy value={aliasBy} onChange={value => this.onPropertyChange('aliasBy', value)} />
|
||||||
|
<Help datasource={datasource} rawQuery={lastQuery} lastQueryError={lastQueryError} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Metrics>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
|||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueryTypeChange(event) {
|
async onQueryTypeChange(event) {
|
||||||
const state: any = {
|
const state: any = {
|
||||||
selectedQueryType: event.target.value,
|
selectedQueryType: event.target.value,
|
||||||
...await this.getLabels(this.state.selectedMetricType, event.target.value),
|
...await this.getLabels(this.state.selectedMetricType, event.target.value),
|
||||||
@@ -134,7 +134,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
|||||||
case MetricFindQueryTypes.LabelValues:
|
case MetricFindQueryTypes.LabelValues:
|
||||||
case MetricFindQueryTypes.ResourceTypes:
|
case MetricFindQueryTypes.ResourceTypes:
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<SimpleSelect
|
<SimpleSelect
|
||||||
value={this.state.selectedService}
|
value={this.state.selectedService}
|
||||||
options={this.insertTemplateVariables(this.state.services)}
|
options={this.insertTemplateVariables(this.state.services)}
|
||||||
@@ -155,12 +155,12 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
|||||||
label="Label Key"
|
label="Label Key"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
case MetricFindQueryTypes.Aligners:
|
case MetricFindQueryTypes.Aligners:
|
||||||
case MetricFindQueryTypes.Aggregations:
|
case MetricFindQueryTypes.Aggregations:
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<SimpleSelect
|
<SimpleSelect
|
||||||
value={this.state.selectedService}
|
value={this.state.selectedService}
|
||||||
options={this.insertTemplateVariables(this.state.services)}
|
options={this.insertTemplateVariables(this.state.services)}
|
||||||
@@ -173,7 +173,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
|||||||
onValueChange={e => this.onMetricTypeChange(e)}
|
onValueChange={e => this.onMetricTypeChange(e)}
|
||||||
label="Metric Type"
|
label="Metric Type"
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
@@ -182,15 +182,15 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<SimpleSelect
|
<SimpleSelect
|
||||||
value={this.state.selectedQueryType}
|
value={this.state.selectedQueryType}
|
||||||
options={this.queryTypes}
|
options={this.queryTypes}
|
||||||
onValueChange={e => this.handleQueryTypeChange(e)}
|
onValueChange={e => this.onQueryTypeChange(e)}
|
||||||
label="Query Type"
|
label="Query Type"
|
||||||
/>
|
/>
|
||||||
{this.renderQueryTypeSwitch(this.state.selectedQueryType)}
|
{this.renderQueryTypeSwitch(this.state.selectedQueryType)}
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Aggregations renders correctly 1`] = `
|
||||||
|
Array [
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label query-keyword width-9"
|
||||||
|
>
|
||||||
|
Aggregation
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__value-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
|
>
|
||||||
|
Select Aggregation
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"display": "inline-block",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-2-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__indicators"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-select-box__select-arrow "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-caret-right"
|
||||||
|
/>
|
||||||
|
Advanced Options
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div />,
|
||||||
|
]
|
||||||
|
`;
|
||||||
+459
@@ -0,0 +1,459 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`QueryEditor renders correctly 1`] = `
|
||||||
|
Array [
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-label width-9 query-keyword"
|
||||||
|
>
|
||||||
|
Service
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__value-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
|
>
|
||||||
|
Select Services
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="css-14uuagi"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-2-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
readOnly={true}
|
||||||
|
tabIndex="0"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__indicators"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-select-box__select-arrow "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-label width-9 query-keyword"
|
||||||
|
>
|
||||||
|
Metric
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__value-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
|
>
|
||||||
|
Select Metric
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"display": "inline-block",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-3-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__indicators"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-select-box__select-arrow "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label query-keyword width-9"
|
||||||
|
>
|
||||||
|
Aggregation
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__value-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
|
>
|
||||||
|
Select Aggregation
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"display": "inline-block",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-4-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__indicators"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-select-box__select-arrow "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-caret-right"
|
||||||
|
/>
|
||||||
|
Advanced Options
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label query-keyword width-9"
|
||||||
|
>
|
||||||
|
Alignment Period
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__single-value"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-box__img-value"
|
||||||
|
>
|
||||||
|
stackdriver auto
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"display": "inline-block",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-5-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__indicators"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-select-box__select-arrow "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label query-keyword width-9"
|
||||||
|
>
|
||||||
|
Alias By
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="gf-form-input width-24"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-label width-9 query-keyword"
|
||||||
|
>
|
||||||
|
Project
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
className="gf-form-input width-15"
|
||||||
|
disabled={true}
|
||||||
|
type="text"
|
||||||
|
value="Loading project..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label query-keyword pointer"
|
||||||
|
>
|
||||||
|
Show Help
|
||||||
|
<i
|
||||||
|
className="fa fa-caret-right"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
`;
|
||||||
@@ -2,6 +2,7 @@ import { stackdriverUnitMappings } from './constants';
|
|||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
|
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
|
||||||
|
import { MetricDescriptor } from './types';
|
||||||
|
|
||||||
export default class StackdriverDatasource {
|
export default class StackdriverDatasource {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -28,21 +29,15 @@ export default class StackdriverDatasource {
|
|||||||
return !target.hide && target.metricType;
|
return !target.hide && target.metricType;
|
||||||
})
|
})
|
||||||
.map(t => {
|
.map(t => {
|
||||||
if (!t.hasOwnProperty('aggregation')) {
|
|
||||||
t.aggregation = {
|
|
||||||
crossSeriesReducer: 'REDUCE_MEAN',
|
|
||||||
groupBys: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
refId: t.refId,
|
refId: t.refId,
|
||||||
intervalMs: options.intervalMs,
|
intervalMs: options.intervalMs,
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
|
metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
|
||||||
primaryAggregation: this.templateSrv.replace(t.aggregation.crossSeriesReducer, options.scopedVars || {}),
|
primaryAggregation: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
|
||||||
perSeriesAligner: this.templateSrv.replace(t.aggregation.perSeriesAligner, options.scopedVars || {}),
|
perSeriesAligner: this.templateSrv.replace(t.perSeriesAligner, options.scopedVars || {}),
|
||||||
alignmentPeriod: this.templateSrv.replace(t.aggregation.alignmentPeriod, options.scopedVars || {}),
|
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
|
||||||
groupBys: this.interpolateGroupBys(t.aggregation.groupBys, options.scopedVars),
|
groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
|
||||||
view: t.view || 'FULL',
|
view: t.view || 'FULL',
|
||||||
filters: (t.filters || []).map(f => {
|
filters: (t.filters || []).map(f => {
|
||||||
return this.templateSrv.replace(f, options.scopedVars || {});
|
return this.templateSrv.replace(f, options.scopedVars || {});
|
||||||
@@ -75,9 +70,7 @@ export default class StackdriverDatasource {
|
|||||||
refId: refId,
|
refId: refId,
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
metricType: this.templateSrv.replace(metricType),
|
metricType: this.templateSrv.replace(metricType),
|
||||||
aggregation: {
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
|
||||||
},
|
|
||||||
view: 'HEADERS',
|
view: 'HEADERS',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -261,7 +254,7 @@ export default class StackdriverDatasource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetricTypes(projectName: string) {
|
async getMetricTypes(projectName: string): Promise<MetricDescriptor[]> {
|
||||||
try {
|
try {
|
||||||
if (this.metricTypes.length === 0) {
|
if (this.metricTypes.length === 0) {
|
||||||
const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
|
const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
|
||||||
@@ -273,6 +266,7 @@ export default class StackdriverDatasource {
|
|||||||
m.service = service;
|
m.service = service;
|
||||||
m.serviceShortName = serviceShortName;
|
m.serviceShortName = serviceShortName;
|
||||||
m.displayName = m.displayName || m.type;
|
m.displayName = m.displayName || m.type;
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ export class FilterSegments {
|
|||||||
filterSegments: any[];
|
filterSegments: any[];
|
||||||
removeSegment: any;
|
removeSegment: any;
|
||||||
|
|
||||||
constructor(private uiSegmentSrv, private target, private getFilterKeysFunc, private getFilterValuesFunc) {}
|
constructor(private uiSegmentSrv, private filters, private getFilterKeysFunc, private getFilterValuesFunc) {}
|
||||||
|
|
||||||
buildSegmentModel() {
|
buildSegmentModel() {
|
||||||
this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
|
this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
|
||||||
|
|
||||||
this.filterSegments = [];
|
this.filterSegments = [];
|
||||||
this.target.filters.forEach((f, index) => {
|
this.filters.forEach((f, index) => {
|
||||||
switch (index % 4) {
|
switch (index % 4) {
|
||||||
case 0:
|
case 0:
|
||||||
this.filterSegments.push(this.uiSegmentSrv.newKey(f));
|
this.filterSegments.push(this.uiSegmentSrv.newKey(f));
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { getAlignmentOptionsByMetric } from './functions';
|
||||||
|
import { ValueTypes, MetricKind } from './constants';
|
||||||
|
|
||||||
|
describe('functions', () => {
|
||||||
|
let result;
|
||||||
|
describe('getAlignmentOptionsByMetric', () => {
|
||||||
|
describe('when double and gauge is passed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
result = getAlignmentOptionsByMetric(ValueTypes.DOUBLE, MetricKind.GAUGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all alignment options except two', () => {
|
||||||
|
expect(result.length).toBe(9);
|
||||||
|
expect(result.map(o => o.value)).toEqual(
|
||||||
|
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when double and delta is passed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
result = getAlignmentOptionsByMetric(ValueTypes.DOUBLE, MetricKind.DELTA);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all alignment options except four', () => {
|
||||||
|
expect(result.length).toBe(9);
|
||||||
|
expect(result.map(o => o.value)).toEqual(
|
||||||
|
expect.not.arrayContaining([
|
||||||
|
'ALIGN_COUNT_TRUE',
|
||||||
|
'ALIGN_COUNT_FALSE',
|
||||||
|
'ALIGN_FRACTION_TRUE',
|
||||||
|
'ALIGN_INTERPOLATE',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,3 +46,21 @@ export const getLabelKeys = async (datasource, selectedMetricType) => {
|
|||||||
: [];
|
: [];
|
||||||
return labelKeys;
|
return labelKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAlignmentPickerData = ({ valueType, metricKind, perSeriesAligner }, templateSrv) => {
|
||||||
|
const options = getAlignmentOptionsByMetric(valueType, metricKind).map(option => ({
|
||||||
|
...option,
|
||||||
|
label: option.text,
|
||||||
|
}));
|
||||||
|
const alignOptions = [
|
||||||
|
{
|
||||||
|
label: 'Alignment options',
|
||||||
|
expanded: true,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (!options.some(o => o.value === templateSrv.replace(perSeriesAligner))) {
|
||||||
|
perSeriesAligner = options.length > 0 ? options[0].value : '';
|
||||||
|
}
|
||||||
|
return { alignOptions, perSeriesAligner };
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user