mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'grafana-ui/move-spectrum' into tooling/storybook-poc
This commit is contained in:
commit
2991b64b60
@ -367,7 +367,7 @@ jobs:
|
||||
command: './scripts/build/publish.sh --enterprise'
|
||||
- run:
|
||||
name: Load GPG private key
|
||||
comand: './scripts/build/load-signing-key.sh'
|
||||
command: './scripts/build/load-signing-key.sh'
|
||||
- run:
|
||||
name: Update Debian repository
|
||||
command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
|
||||
@ -430,7 +430,7 @@ jobs:
|
||||
command: './scripts/build/publish.sh'
|
||||
- run:
|
||||
name: Load GPG private key
|
||||
comand: './scripts/build/load-signing-key.sh'
|
||||
command: './scripts/build/load-signing-key.sh'
|
||||
- run:
|
||||
name: Update Debian repository
|
||||
command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
|
||||
|
20
CHANGELOG.md
20
CHANGELOG.md
@ -2,6 +2,7 @@
|
||||
|
||||
### New Features
|
||||
* **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)
|
||||
|
||||
### Minor
|
||||
@ -11,18 +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)
|
||||
* **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)
|
||||
* **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)
|
||||
* **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)
|
||||
* **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)
|
||||
* **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
|
||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
||||
|
||||
# 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)
|
||||
|
||||
* **Datasource admin**: Fix for issue creating new data source when same name exists [#14467](https://github.com/grafana/grafana/issues/14467)
|
||||
|
@ -4,6 +4,6 @@ providers:
|
||||
- name: 'gdev dashboards'
|
||||
folder: 'gdev dashboards'
|
||||
type: file
|
||||
updateIntervalSeconds: 15
|
||||
options:
|
||||
path: devenv/dev-dashboards
|
||||
|
||||
|
1674
devenv/dev-dashboards-without-uid/panel_tests_graph.json
Normal file
1674
devenv/dev-dashboards-without-uid/panel_tests_graph.json
Normal file
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
|
||||
}
|
3342
devenv/dev-dashboards-without-uid/panel_tests_polystat.json
Normal file
3342
devenv/dev-dashboards-without-uid/panel_tests_polystat.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"iteration": 1542304484522,
|
||||
"iteration": 1545263815779,
|
||||
"links": [
|
||||
{
|
||||
"icon": "external link",
|
||||
@ -66,6 +66,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -168,6 +169,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -270,6 +272,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -372,6 +375,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -474,6 +478,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -576,6 +581,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2249,6 +2255,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2366,6 +2373,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2483,6 +2491,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2600,6 +2609,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2717,6 +2727,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2834,6 +2845,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2951,6 +2963,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3068,6 +3081,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3185,6 +3199,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3302,6 +3317,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3419,6 +3435,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3536,6 +3553,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3667,6 +3685,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3780,6 +3799,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3893,6 +3913,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4006,6 +4027,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4119,6 +4141,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4232,6 +4255,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4345,6 +4369,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4458,6 +4483,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4571,6 +4597,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4684,6 +4711,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4797,6 +4825,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4910,6 +4939,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -5008,6 +5038,512 @@
|
||||
"x": 0,
|
||||
"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,
|
||||
"panels": [
|
||||
{
|
||||
@ -5042,6 +5578,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -5193,6 +5730,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -5328,8 +5866,8 @@
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "gdev-elasticsearch-v2-metrics",
|
||||
"value": "gdev-elasticsearch-v2-metrics"
|
||||
"text": "gdev-elasticsearch-v5-metrics",
|
||||
"value": "gdev-elasticsearch-v5-metrics"
|
||||
},
|
||||
"hide": 0,
|
||||
"label": "Version One",
|
||||
@ -5343,8 +5881,8 @@
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"text": "gdev-elasticsearch-v5-metrics",
|
||||
"value": "gdev-elasticsearch-v5-metrics"
|
||||
"text": "gdev-elasticsearch-v6-metrics",
|
||||
"value": "gdev-elasticsearch-v6-metrics"
|
||||
},
|
||||
"hide": 0,
|
||||
"label": "Version Two",
|
||||
@ -5359,7 +5897,7 @@
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-3h",
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
@ -5390,5 +5928,5 @@
|
||||
"timezone": "",
|
||||
"title": "Datasource tests - Elasticsearch comparison",
|
||||
"uid": "fuFWehBmk",
|
||||
"version": 10
|
||||
"version": 4
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
+++
|
||||
title = "Explore"
|
||||
keywords = ["explore", "loki", "logs"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Explore"
|
||||
@ -8,7 +9,11 @@ parent = "features"
|
||||
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.
|
||||
|
||||
|
@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"role": "Viewer",
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "5.4.2",
|
||||
"testing": "5.4.2"
|
||||
"stable": "5.4.3",
|
||||
"testing": "5.4.3"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'vendor/spectrum';
|
||||
import '../../vendor/spectrum';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
|
@ -12,10 +12,7 @@ export class Portal extends PureComponent<Props> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const {
|
||||
className,
|
||||
root = document.body
|
||||
} = this.props;
|
||||
const { className, root = document.body } = this.props;
|
||||
|
||||
if (className) {
|
||||
this.node.classList.add(className);
|
||||
|
@ -5,6 +5,7 @@ import { OptionProps } from 'react-select/lib/components/Option';
|
||||
|
||||
// @ts-ignore
|
||||
const model: OptionProps<any> = {
|
||||
data: jest.fn(),
|
||||
cx: jest.fn(),
|
||||
clearValue: jest.fn(),
|
||||
getStyles: jest.fn(),
|
||||
|
@ -2,7 +2,11 @@ import React, { PureComponent } from 'react';
|
||||
import { GroupProps } from 'react-select/lib/components/Group';
|
||||
|
||||
interface ExtendedGroupProps extends GroupProps<any> {
|
||||
data: any;
|
||||
data: {
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
options: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -15,8 +19,10 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.selectProps) {
|
||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
||||
if (this.props.data.expanded) {
|
||||
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)) {
|
||||
this.setState({ expanded: true });
|
||||
|
@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
.gf-form-select-box__menu-list {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.tag-filter .gf-form-select-box__menu {
|
||||
|
@ -5,3 +5,5 @@
|
||||
@import 'Select/Select';
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
@import 'ColorPicker/ColorPicker';
|
||||
|
@ -1 +1,3 @@
|
||||
@import 'vendor/spectrum';
|
||||
@import 'components/index';
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -69,7 +70,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
|
||||
parsedDashboards, err := cr.parseConfigs(file)
|
||||
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 {
|
||||
|
@ -118,6 +118,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
||||
if fr.Cfg.DisableDeletion {
|
||||
return
|
||||
@ -180,7 +181,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
||||
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{
|
||||
ExternalId: path,
|
||||
Name: fr.Cfg.Name,
|
||||
|
@ -166,6 +166,36 @@ func TestDashboardFileReader(t *testing.T) {
|
||||
_, err := NewDashboardFileReader(cfg, logger)
|
||||
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() {
|
||||
@ -256,7 +286,9 @@ func (ffi FakeFileInfo) Sys() interface{} {
|
||||
}
|
||||
|
||||
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
||||
mock := fakeDashboardProvisioningService{}
|
||||
mock := fakeDashboardProvisioningService{
|
||||
provisioned: map[string][]*models.DashboardProvisioning{},
|
||||
}
|
||||
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
|
||||
return &mock
|
||||
}
|
||||
@ -265,17 +297,26 @@ func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
||||
|
||||
type fakeDashboardProvisioningService struct {
|
||||
inserted []*dashboards.SaveDashboardDTO
|
||||
provisioned []*models.DashboardProvisioning
|
||||
provisioned map[string][]*models.DashboardProvisioning
|
||||
getDashboard []*models.Dashboard
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error
|
||||
func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -292,7 +292,7 @@ func (a *MetricAggregation) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// PipelineAggregation represents a metric aggregation
|
||||
type PipelineAggregation struct {
|
||||
BucketPath string
|
||||
BucketPath interface{}
|
||||
Settings map[string]interface{}
|
||||
}
|
||||
|
||||
|
@ -268,7 +268,7 @@ type AggBuilder interface {
|
||||
Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder
|
||||
GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) 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)
|
||||
}
|
||||
|
||||
@ -438,7 +438,7 @@ func (b *aggBuilderImpl) Metric(key, metricType, field string, fn func(a *Metric
|
||||
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{
|
||||
BucketPath: bucketPath,
|
||||
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
|
||||
type MetricAgg struct {
|
||||
Field string `json:"field"`
|
||||
Hide bool `json:"hide"`
|
||||
ID string `json:"id"`
|
||||
PipelineAggregate string `json:"pipelineAgg"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Meta *simplejson.Json `json:"meta"`
|
||||
Type string `json:"type"`
|
||||
Field string `json:"field"`
|
||||
Hide bool `json:"hide"`
|
||||
ID string `json:"id"`
|
||||
PipelineAggregate string `json:"pipelineAgg"`
|
||||
PipelineVariables map[string]string `json:"pipelineVariables"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Meta *simplejson.Json `json:"meta"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var metricAggType = map[string]string{
|
||||
@ -45,6 +46,7 @@ var metricAggType = map[string]string{
|
||||
"cardinality": "Unique Count",
|
||||
"moving_avg": "Moving Average",
|
||||
"derivative": "Derivative",
|
||||
"bucket_script": "Bucket Script",
|
||||
"raw_document": "Raw Document",
|
||||
}
|
||||
|
||||
@ -60,8 +62,13 @@ var extendedStats = map[string]string{
|
||||
}
|
||||
|
||||
var pipelineAggType = map[string]string{
|
||||
"moving_avg": "moving_avg",
|
||||
"derivative": "derivative",
|
||||
"moving_avg": "moving_avg",
|
||||
"derivative": "derivative",
|
||||
"bucket_script": "bucket_script",
|
||||
}
|
||||
|
||||
var pipelineAggWithMultipleBucketPathsType = map[string]string{
|
||||
"bucket_script": "bucket_script",
|
||||
}
|
||||
|
||||
func isPipelineAgg(metricType string) bool {
|
||||
@ -71,6 +78,13 @@ func isPipelineAgg(metricType string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isPipelineAggWithMultipleBucketPaths(metricType string) bool {
|
||||
if _, ok := pipelineAggWithMultipleBucketPathsType[metricType]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func describeMetric(metricType, field string) string {
|
||||
text := metricAggType[metricType]
|
||||
if metricType == countType {
|
||||
|
@ -260,6 +260,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
|
||||
|
||||
newSeries.Tags["metric"] = metric.Type
|
||||
newSeries.Tags["field"] = metric.Field
|
||||
newSeries.Tags["metricId"] = metric.ID
|
||||
for _, v := range esAgg.Get("buckets").MustArray() {
|
||||
bucket := simplejson.NewFromAny(v)
|
||||
key := castToNullFloat(bucket.Get("key"))
|
||||
@ -459,20 +460,42 @@ func (rp *responseParser) getSeriesName(series *tsdb.TimeSeries, target *Query,
|
||||
}
|
||||
// todo, if field and pipelineAgg
|
||||
if field != "" && isPipelineAgg(metricType) {
|
||||
found := false
|
||||
for _, metric := range target.Metrics {
|
||||
if metric.ID == field {
|
||||
metricName += " " + describeMetric(metric.Type, field)
|
||||
found = true
|
||||
if isPipelineAggWithMultipleBucketPaths(metricType) {
|
||||
metricID := ""
|
||||
if v, ok := series.Tags["metricId"]; ok {
|
||||
metricID = v
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
metricName += " " + field
|
||||
}
|
||||
|
||||
delete(series.Tags, "metricId")
|
||||
|
||||
if len(series.Tags) == 0 {
|
||||
return metricName
|
||||
}
|
||||
|
@ -787,6 +787,84 @@ func TestResponseParser(t *testing.T) {
|
||||
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() {
|
||||
// targets := map[string]string{
|
||||
// "A": `{
|
||||
|
@ -94,26 +94,56 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) {
|
||||
}
|
||||
|
||||
if isPipelineAgg(m.Type) {
|
||||
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"
|
||||
if isPipelineAggWithMultipleBucketPaths(m.Type) {
|
||||
if len(m.PipelineVariables) > 0 {
|
||||
bucketPaths := map[string]interface{}{}
|
||||
for name, pipelineAgg := range m.PipelineVariables {
|
||||
if _, err := strconv.Atoi(pipelineAgg); err == nil {
|
||||
var appliedAgg *MetricAgg
|
||||
for _, pipelineMetric := range q.Metrics {
|
||||
if pipelineMetric.ID == pipelineAgg {
|
||||
appliedAgg = pipelineMetric
|
||||
break
|
||||
}
|
||||
}
|
||||
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()
|
||||
})
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} 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 {
|
||||
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.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap())
|
||||
metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap())
|
||||
|
||||
metric.Type, err = metricJSON.Get("type").String()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
return result, nil
|
||||
|
@ -543,6 +543,77 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
|
||||
plAgg := derivativeAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
|
||||
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 { 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 PageHeader from './components/PageHeader/PageHeader';
|
||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||
import { SearchResult } from './components/search/SearchResult';
|
||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||
import { MetricSelect } from './components/Select/MetricSelect';
|
||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||
import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
|
||||
|
||||
@ -29,4 +32,28 @@ export function registerAngularDirectives() {
|
||||
'onColorChange',
|
||||
'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' }],
|
||||
]);
|
||||
}
|
||||
|
90
public/app/core/components/Select/MetricSelect.tsx
Normal file
90
public/app/core/components/Select/MetricSelect.tsx
Normal file
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoa
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
// Components
|
||||
import { EditorTabBody, EditorToolbarView } from '../dashboard/dashgrid/EditorTabBody';
|
||||
import { EditorTabBody, EditorToolbarView } from '../dashboard/panel_editor/EditorTabBody';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import StateHistory from './StateHistory';
|
||||
import 'app/features/alerting/AlertTabCtrl';
|
||||
|
@ -11,3 +11,4 @@ import './alerting/NotificationsListCtrl';
|
||||
import './manage-dashboards';
|
||||
import './teams/CreateTeamCtrl';
|
||||
import './profile/all';
|
||||
import './datasources/settings/HttpSettingsCtrl';
|
||||
|
@ -9,7 +9,7 @@ import { AddPanelPanel } from './AddPanelPanel';
|
||||
import { getPanelPluginNotFound } from './PanelPluginNotFound';
|
||||
import { DashboardRow } from './DashboardRow';
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
import { PanelEditor } from '../panel_editor/PanelEditor';
|
||||
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
@ -1,71 +0,0 @@
|
||||
import React, { KeyboardEvent, Component } from 'react';
|
||||
|
||||
interface State {
|
||||
selected: number;
|
||||
}
|
||||
|
||||
export interface KeyboardNavigationProps {
|
||||
onKeyDown: (evt: KeyboardEvent<EventTarget>, maxSelectedIndex: number, onEnterAction: () => void) => void;
|
||||
onMouseEnter: (select: number) => void;
|
||||
selected: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
render: (injectProps: any) => void;
|
||||
}
|
||||
|
||||
class KeyboardNavigation extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selected: 0,
|
||||
};
|
||||
}
|
||||
|
||||
goToNext = (maxSelectedIndex: number) => {
|
||||
const nextIndex = this.state.selected >= maxSelectedIndex ? 0 : this.state.selected + 1;
|
||||
this.setState({
|
||||
selected: nextIndex,
|
||||
});
|
||||
};
|
||||
|
||||
goToPrev = (maxSelectedIndex: number) => {
|
||||
const nextIndex = this.state.selected <= 0 ? maxSelectedIndex : this.state.selected - 1;
|
||||
this.setState({
|
||||
selected: nextIndex,
|
||||
});
|
||||
};
|
||||
|
||||
onKeyDown = (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: any) => {
|
||||
if (evt.key === 'ArrowDown') {
|
||||
evt.preventDefault();
|
||||
this.goToNext(maxSelectedIndex);
|
||||
}
|
||||
if (evt.key === 'ArrowUp') {
|
||||
evt.preventDefault();
|
||||
this.goToPrev(maxSelectedIndex);
|
||||
}
|
||||
if (evt.key === 'Enter' && onEnterAction) {
|
||||
onEnterAction();
|
||||
}
|
||||
};
|
||||
|
||||
onMouseEnter = (mouseEnterIndex: number) => {
|
||||
this.setState({
|
||||
selected: mouseEnterIndex,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const injectProps = {
|
||||
onKeyDown: this.onKeyDown,
|
||||
onMouseEnter: this.onMouseEnter,
|
||||
selected: this.state.selected,
|
||||
};
|
||||
|
||||
return <>{this.props.render({ ...injectProps })}</>;
|
||||
}
|
||||
}
|
||||
|
||||
export default KeyboardNavigation;
|
@ -19,6 +19,8 @@ import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
|
||||
import variables from 'sass/_variables.scss';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
@ -122,8 +124,8 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
timeSeries={timeSeries}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||
width={width}
|
||||
height={height - PANEL_HEADER_HEIGHT}
|
||||
width={width - 2 * variables.panelHorizontalPadding }
|
||||
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
|
||||
renderCounter={renderCounter}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,31 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export interface AttachedPanel {
|
||||
destroy();
|
||||
}
|
||||
|
||||
export class PanelLoader {
|
||||
/** @ngInject */
|
||||
constructor(private $compile, private $rootScope) {}
|
||||
|
||||
load(elem, panel, dashboard): AttachedPanel {
|
||||
const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
|
||||
const panelScope = this.$rootScope.$new();
|
||||
panelScope.panel = panel;
|
||||
panelScope.dashboard = dashboard;
|
||||
|
||||
const compiledElem = this.$compile(template)(panelScope);
|
||||
const rootNode = angular.element(elem);
|
||||
rootNode.append(compiledElem);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
panelScope.$destroy();
|
||||
compiledElem.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('panelLoader', PanelLoader);
|
26
public/app/features/datasources/settings/HttpSettingsCtrl.ts
Normal file
26
public/app/features/datasources/settings/HttpSettingsCtrl.ts
Normal file
@ -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];
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
@ -1,7 +1,6 @@
|
||||
import './plugin_edit_ctrl';
|
||||
import './plugin_page_ctrl';
|
||||
import './import_list/import_list';
|
||||
import './ds_edit_ctrl';
|
||||
import './datasource_srv';
|
||||
import './plugin_component';
|
||||
import './VariableQueryComponentLoader';
|
||||
import './variableQueryEditorLoader';
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { coreModule } from 'app/core/core';
|
||||
import { store } from 'app/store/store';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { buildNavModel } from './state/navModel';
|
||||
|
||||
export class DataSourceDashboardsCtrl {
|
||||
datasourceMeta: any;
|
||||
navModel: any;
|
||||
current: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $routeParams) {
|
||||
const state = store.getState();
|
||||
this.navModel = getNavModel(state.navIndex, 'datasources');
|
||||
|
||||
if (this.$routeParams.id) {
|
||||
this.getDatasourceById(this.$routeParams.id);
|
||||
}
|
||||
}
|
||||
|
||||
getDatasourceById(id) {
|
||||
this.backendSrv
|
||||
.get('/api/datasources/' + id)
|
||||
.then(ds => {
|
||||
this.current = ds;
|
||||
})
|
||||
.then(this.getPluginInfo.bind(this));
|
||||
}
|
||||
|
||||
updateNav() {
|
||||
this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-dashboards');
|
||||
}
|
||||
|
||||
getPluginInfo() {
|
||||
return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
|
||||
this.datasourceMeta = pluginInfo;
|
||||
this.updateNav();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DataSourceDashboardsCtrl', DataSourceDashboardsCtrl);
|
@ -1,223 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import { coreModule, appEvents } from 'app/core/core';
|
||||
import { store } from 'app/store/store';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { buildNavModel } from './state/navModel';
|
||||
|
||||
let datasourceTypes = [];
|
||||
|
||||
const defaults = {
|
||||
name: '',
|
||||
type: 'graphite',
|
||||
url: '',
|
||||
access: 'proxy',
|
||||
jsonData: {},
|
||||
secureJsonFields: {},
|
||||
secureJsonData: {},
|
||||
};
|
||||
|
||||
let datasourceCreated = false;
|
||||
|
||||
export class DataSourceEditCtrl {
|
||||
isNew: boolean;
|
||||
datasources: any[];
|
||||
current: any;
|
||||
types: any;
|
||||
testing: any;
|
||||
datasourceMeta: any;
|
||||
editForm: any;
|
||||
gettingStarted: boolean;
|
||||
navModel: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) {
|
||||
const state = store.getState();
|
||||
this.navModel = getNavModel(state.navIndex, 'datasources');
|
||||
this.datasources = [];
|
||||
|
||||
this.loadDatasourceTypes().then(() => {
|
||||
if (this.$routeParams.id) {
|
||||
this.getDatasourceById(this.$routeParams.id);
|
||||
} else {
|
||||
this.initNewDatasourceModel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initNewDatasourceModel() {
|
||||
this.isNew = true;
|
||||
this.current = _.cloneDeep(defaults);
|
||||
|
||||
// We are coming from getting started
|
||||
if (this.$location.search().gettingstarted) {
|
||||
this.gettingStarted = true;
|
||||
this.current.isDefault = true;
|
||||
}
|
||||
|
||||
this.typeChanged();
|
||||
}
|
||||
|
||||
loadDatasourceTypes() {
|
||||
if (datasourceTypes.length > 0) {
|
||||
this.types = datasourceTypes;
|
||||
return this.$q.when(null);
|
||||
}
|
||||
|
||||
return this.backendSrv.get('/api/plugins', { enabled: 1, type: 'datasource' }).then(plugins => {
|
||||
datasourceTypes = plugins;
|
||||
this.types = plugins;
|
||||
});
|
||||
}
|
||||
|
||||
getDatasourceById(id) {
|
||||
this.backendSrv.get('/api/datasources/' + id).then(ds => {
|
||||
this.isNew = false;
|
||||
this.current = ds;
|
||||
|
||||
if (datasourceCreated) {
|
||||
datasourceCreated = false;
|
||||
this.testDatasource();
|
||||
}
|
||||
|
||||
return this.typeChanged();
|
||||
});
|
||||
}
|
||||
|
||||
userChangedType() {
|
||||
// reset model but keep name & default flag
|
||||
this.current = _.defaults(
|
||||
{
|
||||
id: this.current.id,
|
||||
name: this.current.name,
|
||||
isDefault: this.current.isDefault,
|
||||
type: this.current.type,
|
||||
},
|
||||
_.cloneDeep(defaults)
|
||||
);
|
||||
this.typeChanged();
|
||||
}
|
||||
|
||||
updateNav() {
|
||||
this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-settings');
|
||||
}
|
||||
|
||||
typeChanged() {
|
||||
return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
|
||||
this.datasourceMeta = pluginInfo;
|
||||
this.updateNav();
|
||||
});
|
||||
}
|
||||
|
||||
updateFrontendSettings() {
|
||||
return this.backendSrv.get('/api/frontend/settings').then(settings => {
|
||||
config.datasources = settings.datasources;
|
||||
config.defaultDatasource = settings.defaultDatasource;
|
||||
this.datasourceSrv.init();
|
||||
});
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
return this.datasourceSrv.get(this.current.name).then(datasource => {
|
||||
if (!datasource.testDatasource) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.testing = { done: false, status: 'error' };
|
||||
|
||||
// make test call in no backend cache context
|
||||
return this.backendSrv
|
||||
.withNoBackendCache(() => {
|
||||
return datasource
|
||||
.testDatasource()
|
||||
.then(result => {
|
||||
this.testing.message = result.message;
|
||||
this.testing.status = result.status;
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.statusText) {
|
||||
this.testing.message = 'HTTP Error ' + err.statusText;
|
||||
} else {
|
||||
this.testing.message = err.message;
|
||||
}
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.testing.done = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveChanges() {
|
||||
if (!this.editForm.$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current.id) {
|
||||
return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => {
|
||||
this.current = result.datasource;
|
||||
this.updateNav();
|
||||
return this.updateFrontendSettings().then(() => {
|
||||
return this.testDatasource();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return this.backendSrv.post('/api/datasources', this.current).then(result => {
|
||||
this.current = result.datasource;
|
||||
this.updateFrontendSettings();
|
||||
|
||||
datasourceCreated = true;
|
||||
this.$location.path('datasources/edit/' + result.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
this.backendSrv.delete('/api/datasources/' + this.current.id).then(() => {
|
||||
this.$location.path('datasources');
|
||||
});
|
||||
}
|
||||
|
||||
delete(s) {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete this datasource?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-trash',
|
||||
onConfirm: () => {
|
||||
this.confirmDelete();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl);
|
||||
|
||||
coreModule.directive('datasourceHttpSettings', () => {
|
||||
return {
|
||||
scope: {
|
||||
current: '=',
|
||||
suggestUrl: '@',
|
||||
noDirectAccess: '@',
|
||||
},
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_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];
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
@ -88,6 +88,7 @@ export class ElasticResponse {
|
||||
datapoints: [],
|
||||
metric: metric.type,
|
||||
field: metric.field,
|
||||
metricId: metric.id,
|
||||
props: props,
|
||||
};
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
@ -240,7 +241,7 @@ export class ElasticResponse {
|
||||
return metricName;
|
||||
}
|
||||
if (group === 'field') {
|
||||
return series.field;
|
||||
return series.field || '';
|
||||
}
|
||||
|
||||
return match;
|
||||
@ -248,11 +249,27 @@ export class ElasticResponse {
|
||||
}
|
||||
|
||||
if (series.field && queryDef.isPipelineAgg(series.metric)) {
|
||||
const appliedAgg = _.find(target.metrics, { id: series.field });
|
||||
if (appliedAgg) {
|
||||
metricName += ' ' + queryDef.describeMetric(appliedAgg);
|
||||
if (series.metric && queryDef.isPipelineAggWithMultipleBucketPaths(series.metric)) {
|
||||
const agg = _.find(target.metrics, { id: series.metricId });
|
||||
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 {
|
||||
metricName = 'Unset';
|
||||
const appliedAgg = _.find(target.metrics, { id: series.field });
|
||||
if (appliedAgg) {
|
||||
metricName += ' ' + queryDef.describeMetric(appliedAgg);
|
||||
} else {
|
||||
metricName = 'Unset';
|
||||
}
|
||||
}
|
||||
} else if (series.field) {
|
||||
metricName += ' ' + series.field;
|
||||
|
@ -35,11 +35,20 @@ export class ElasticMetricAggCtrl {
|
||||
$scope.isFirst = $scope.index === 0;
|
||||
$scope.isSingle = metricAggs.length === 1;
|
||||
$scope.settingsLinkText = '';
|
||||
$scope.variablesLinkText = '';
|
||||
$scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
|
||||
|
||||
if (queryDef.isPipelineAgg($scope.agg.type)) {
|
||||
$scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
|
||||
$scope.agg.field = $scope.agg.pipelineAgg;
|
||||
if (queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type)) {
|
||||
$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);
|
||||
if (pipelineOptions.length > 0) {
|
||||
@ -119,6 +128,10 @@ export class ElasticMetricAggCtrl {
|
||||
$scope.updatePipelineAggOptions();
|
||||
};
|
||||
|
||||
$scope.toggleVariables = () => {
|
||||
$scope.showVariables = !$scope.showVariables;
|
||||
};
|
||||
|
||||
$scope.onChangeInternal = () => {
|
||||
$scope.onChange();
|
||||
};
|
||||
@ -152,6 +165,7 @@ export class ElasticMetricAggCtrl {
|
||||
$scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
|
||||
}
|
||||
|
||||
$scope.showVariables = queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type);
|
||||
$scope.updatePipelineAggOptions();
|
||||
$scope.onChange();
|
||||
};
|
||||
|
@ -13,7 +13,17 @@
|
||||
<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 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 class="gf-form gf-form--grow">
|
||||
@ -36,6 +46,20 @@
|
||||
</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 offset-width-7" ng-if="agg.type === 'derivative'">
|
||||
<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>
|
||||
</label>
|
||||
<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>
|
||||
|
@ -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.timeField = this.timeField;
|
||||
|
||||
let i, nestedAggs, metric;
|
||||
let i, j, pv, nestedAggs, metric;
|
||||
const query = {
|
||||
size: 0,
|
||||
query: {
|
||||
@ -269,17 +269,42 @@ export class ElasticQueryBuilder {
|
||||
let metricAgg = null;
|
||||
|
||||
if (queryDef.isPipelineAgg(metric.type)) {
|
||||
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 };
|
||||
if (queryDef.isPipelineAggWithMultipleBucketPaths(metric.type)) {
|
||||
if (metric.pipelineVariables) {
|
||||
metricAgg = {
|
||||
buckets_path: {},
|
||||
};
|
||||
|
||||
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 {
|
||||
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 {
|
||||
metricAgg = { field: metric.field };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import './bucket_agg';
|
||||
import './metric_agg';
|
||||
import './pipeline_variables';
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
@ -70,6 +71,9 @@ export class ElasticQueryCtrl extends QueryCtrl {
|
||||
if (aggDef.requiresField) {
|
||||
text += metric.field;
|
||||
}
|
||||
if (aggDef.supportsMultipleBucketPaths) {
|
||||
text += metric.settings.script.replace(new RegExp('params.', 'g'), '');
|
||||
}
|
||||
text += '), ';
|
||||
});
|
||||
|
||||
|
@ -64,6 +64,14 @@ export const metricAggTypes = [
|
||||
isPipelineAgg: true,
|
||||
minVersion: 2,
|
||||
},
|
||||
{
|
||||
text: 'Bucket Script',
|
||||
value: 'bucket_script',
|
||||
requiresField: false,
|
||||
isPipelineAgg: true,
|
||||
supportsMultipleBucketPaths: true,
|
||||
minVersion: 2,
|
||||
},
|
||||
{ text: 'Raw Document', value: 'raw_document', requiresField: false },
|
||||
];
|
||||
|
||||
@ -128,6 +136,7 @@ export const pipelineOptions = {
|
||||
{ text: 'minimize', default: false },
|
||||
],
|
||||
derivative: [{ text: 'unit', default: undefined }],
|
||||
bucket_script: [],
|
||||
};
|
||||
|
||||
export const movingAvgModelSettings = {
|
||||
@ -171,6 +180,14 @@ export function isPipelineAgg(metricType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isPipelineAggWithMultipleBucketPaths(metricType) {
|
||||
if (metricType) {
|
||||
return metricAggTypes.find(t => t.value === metricType && t.supportsMultipleBucketPaths) !== undefined;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPipelineAggOptions(targets) {
|
||||
const result = [];
|
||||
_.each(targets.metrics, metric => {
|
||||
|
@ -665,4 +665,70 @@ describe('ElasticResponse', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const query = builder.build({
|
||||
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('using esversion undefined', () => {
|
||||
test('should not get pipeline aggs', () => {
|
||||
@ -80,13 +98,13 @@ describe('ElasticQueryDef', () => {
|
||||
|
||||
describe('using esversion 2', () => {
|
||||
test('should get pipeline aggs', () => {
|
||||
expect(queryDef.getMetricAggTypes(2).length).toBe(11);
|
||||
expect(queryDef.getMetricAggTypes(2).length).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('using esversion 5', () => {
|
||||
test('should get pipeline aggs', () => {
|
||||
expect(queryDef.getMetricAggTypes(5).length).toBe(11);
|
||||
expect(queryDef.getMetricAggTypes(5).length).toBe(12);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,31 +1,18 @@
|
||||
import _ from 'lodash';
|
||||
import './query_filter_ctrl';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
export class StackdriverAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
annotation: any;
|
||||
datasource: any;
|
||||
|
||||
defaultDropdownValue = 'Select Metric';
|
||||
defaultServiceValue = 'All Services';
|
||||
|
||||
defaults = {
|
||||
project: {
|
||||
id: 'default',
|
||||
name: 'loading project...',
|
||||
},
|
||||
metricType: this.defaultDropdownValue,
|
||||
service: this.defaultServiceValue,
|
||||
metric: '',
|
||||
filters: [],
|
||||
metricKind: '',
|
||||
valueType: '',
|
||||
};
|
||||
templateSrv: TemplateSrv;
|
||||
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
constructor(templateSrv) {
|
||||
this.templateSrv = templateSrv;
|
||||
this.annotation.target = this.annotation.target || {};
|
||||
this.annotation.target.refId = 'annotationQuery';
|
||||
_.defaultsDeep(this.annotation.target, this.defaults);
|
||||
this.onQueryChange = this.onQueryChange.bind(this);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
115
public/app/plugins/datasource/stackdriver/components/Filter.tsx
Normal file
115
public/app/plugins/datasource/stackdriver/components/Filter.tsx
Normal file
@ -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%' }} />;
|
||||
}
|
||||
}
|
115
public/app/plugins/datasource/stackdriver/components/Help.tsx
Normal file
115
public/app/plugins/datasource/stackdriver/components/Help.tsx
Normal file
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
195
public/app/plugins/datasource/stackdriver/components/Metrics.tsx
Normal file
195
public/app/plugins/datasource/stackdriver/components/Metrics.tsx
Normal file
@ -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);
|
||||
}
|
||||
|
||||
async handleQueryTypeChange(event) {
|
||||
async onQueryTypeChange(event) {
|
||||
const state: any = {
|
||||
selectedQueryType: 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.ResourceTypes:
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<SimpleSelect
|
||||
value={this.state.selectedService}
|
||||
options={this.insertTemplateVariables(this.state.services)}
|
||||
@ -155,12 +155,12 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
||||
label="Label Key"
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
case MetricFindQueryTypes.Aligners:
|
||||
case MetricFindQueryTypes.Aggregations:
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<SimpleSelect
|
||||
value={this.state.selectedService}
|
||||
options={this.insertTemplateVariables(this.state.services)}
|
||||
@ -173,7 +173,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
||||
onValueChange={e => this.onMetricTypeChange(e)}
|
||||
label="Metric Type"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
@ -182,15 +182,15 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<SimpleSelect
|
||||
value={this.state.selectedQueryType}
|
||||
options={this.queryTypes}
|
||||
onValueChange={e => this.handleQueryTypeChange(e)}
|
||||
onValueChange={e => this.onQueryTypeChange(e)}
|
||||
label="Query Type"
|
||||
/>
|
||||
{this.renderQueryTypeSwitch(this.state.selectedQueryType)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 />,
|
||||
]
|
||||
`;
|
@ -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 _ from 'lodash';
|
||||
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
|
||||
import { MetricDescriptor } from './types';
|
||||
|
||||
export default class StackdriverDatasource {
|
||||
id: number;
|
||||
@ -28,21 +29,15 @@ export default class StackdriverDatasource {
|
||||
return !target.hide && target.metricType;
|
||||
})
|
||||
.map(t => {
|
||||
if (!t.hasOwnProperty('aggregation')) {
|
||||
t.aggregation = {
|
||||
crossSeriesReducer: 'REDUCE_MEAN',
|
||||
groupBys: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
refId: t.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
datasourceId: this.id,
|
||||
metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
|
||||
primaryAggregation: this.templateSrv.replace(t.aggregation.crossSeriesReducer, options.scopedVars || {}),
|
||||
perSeriesAligner: this.templateSrv.replace(t.aggregation.perSeriesAligner, options.scopedVars || {}),
|
||||
alignmentPeriod: this.templateSrv.replace(t.aggregation.alignmentPeriod, options.scopedVars || {}),
|
||||
groupBys: this.interpolateGroupBys(t.aggregation.groupBys, options.scopedVars),
|
||||
primaryAggregation: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
|
||||
perSeriesAligner: this.templateSrv.replace(t.perSeriesAligner, options.scopedVars || {}),
|
||||
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
|
||||
groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
|
||||
view: t.view || 'FULL',
|
||||
filters: (t.filters || []).map(f => {
|
||||
return this.templateSrv.replace(f, options.scopedVars || {});
|
||||
@ -75,9 +70,7 @@ export default class StackdriverDatasource {
|
||||
refId: refId,
|
||||
datasourceId: this.id,
|
||||
metricType: this.templateSrv.replace(metricType),
|
||||
aggregation: {
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
},
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
view: 'HEADERS',
|
||||
},
|
||||
],
|
||||
@ -261,7 +254,7 @@ export default class StackdriverDatasource {
|
||||
}
|
||||
}
|
||||
|
||||
async getMetricTypes(projectName: string) {
|
||||
async getMetricTypes(projectName: string): Promise<MetricDescriptor[]> {
|
||||
try {
|
||||
if (this.metricTypes.length === 0) {
|
||||
const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
|
||||
@ -273,6 +266,7 @@ export default class StackdriverDatasource {
|
||||
m.service = service;
|
||||
m.serviceShortName = serviceShortName;
|
||||
m.displayName = m.displayName || m.type;
|
||||
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
@ -5,13 +5,13 @@ export class FilterSegments {
|
||||
filterSegments: any[];
|
||||
removeSegment: any;
|
||||
|
||||
constructor(private uiSegmentSrv, private target, private getFilterKeysFunc, private getFilterValuesFunc) {}
|
||||
constructor(private uiSegmentSrv, private filters, private getFilterKeysFunc, private getFilterValuesFunc) {}
|
||||
|
||||
buildSegmentModel() {
|
||||
this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
|
||||
|
||||
this.filterSegments = [];
|
||||
this.target.filters.forEach((f, index) => {
|
||||
this.filters.forEach((f, index) => {
|
||||
switch (index % 4) {
|
||||
case 0:
|
||||
this.filterSegments.push(this.uiSegmentSrv.newKey(f));
|
||||
|
38
public/app/plugins/datasource/stackdriver/functions.test.ts
Normal file
38
public/app/plugins/datasource/stackdriver/functions.test.ts
Normal file
@ -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;
|
||||
};
|
||||
|
||||
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 };
|
||||
};
|
||||
|
@ -1,37 +1,6 @@
|
||||
<stackdriver-filter target="ctrl.annotation.target" refresh="ctrl.refresh()" datasource="ctrl.datasource"
|
||||
default-dropdown-value="ctrl.defaultDropdownValue" default-service-value="ctrl.defaultServiceValue" hide-group-bys="true"></stackdriver-filter>
|
||||
|
||||
<div class="gf-form gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword width-9">Title</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.annotation.target.title" />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword width-9">Text</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.annotation.target.text" />
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form grafana-info-box" style="padding: 0">
|
||||
<pre class="gf-form-pre alert alert-info" style="margin-right: 0"><h5>Annotation Query Format</h5>
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
<code ng-non-bindable>{{metric.type}} has value: {{metric.value}}</code>
|
||||
|
||||
Example Result: <code ng-non-bindable>monitoring.googleapis.com/uptime_check/http_status has this value: 502</code>
|
||||
|
||||
<label>Patterns:</label>
|
||||
<code ng-non-bindable>{{metric.value}}</code> = value of the metric/point
|
||||
<code ng-non-bindable>{{metric.type}}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
|
||||
<code ng-non-bindable>{{metric.name}}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||
<code ng-non-bindable>{{metric.service}}</code> = service part of metric e.g. compute
|
||||
|
||||
<code ng-non-bindable>{{metric.label.label_name}}</code> = Metric label metadata e.g. metric.label.instance_name
|
||||
<code ng-non-bindable>{{resource.label.label_name}}</code> = Resource label metadata e.g. resource.label.zone
|
||||
</pre>
|
||||
</div>
|
||||
<stackdriver-annotation-query-editor
|
||||
target="ctrl.annotation.target"
|
||||
on-query-change="(ctrl.onQueryChange)"
|
||||
datasource="ctrl.datasource"
|
||||
template-srv="ctrl.templateSrv"
|
||||
></stackdriver-annotation-query-editor>
|
||||
|
@ -1,46 +0,0 @@
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Aggregation</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<gf-form-dropdown model="ctrl.target.aggregation.crossSeriesReducer" get-options="ctrl.aggOptions" class="gf-form width-12"
|
||||
disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label gf-form-label--grow">
|
||||
<a ng-click="ctrl.target.showAggregationOptions = !ctrl.target.showAggregationOptions">
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.target.showAggregationOptions"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.target.showAggregationOptions"></i>
|
||||
Advanced Options
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.target.showAggregationOptions">
|
||||
<div class="gf-form offset-width-9">
|
||||
<label class="gf-form-label query-keyword width-12">Aligner</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<gf-form-dropdown model="ctrl.target.aggregation.perSeriesAligner" get-options="ctrl.alignOptions" class="gf-form width-12"
|
||||
disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Alignment Period</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<gf-form-dropdown model="ctrl.target.aggregation.alignmentPeriod" get-options="ctrl.alignmentPeriods" class="gf-form width-12"
|
||||
disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label ng-if="alignmentPeriod" class="gf-form-label gf-form-label--grow">
|
||||
{{ctrl.formatAlignmentText()}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
@ -1,73 +1,10 @@
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
|
||||
<stackdriver-filter target="ctrl.target" refresh="ctrl.refresh()" datasource="ctrl.datasource" default-dropdown-value="ctrl.defaultDropdownValue"
|
||||
default-service-value="ctrl.defaultServiceValue"></stackdriver-filter>
|
||||
<stackdriver-aggregation target="ctrl.target" alignment-period="ctrl.lastQueryMeta.alignmentPeriod" refresh="ctrl.refresh()"></stackdriver-aggregation>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword width-9">Alias By</span>
|
||||
<input type="text" class="gf-form-input width-30" ng-model="ctrl.target.aliasBy" ng-change="ctrl.refresh()"
|
||||
ng-model-options="{ debounce: 500 }" />
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9 query-keyword">Project</span>
|
||||
<input class="gf-form-input" disabled type="text" ng-model='ctrl.target.defaultProject' css-class="min-width-12" />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
Show Help
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuery = !ctrl.showLastQuery">
|
||||
Raw Query
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showLastQuery"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showLastQuery"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuery">
|
||||
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
|
||||
</div>
|
||||
<div class="gf-form grafana-info-box" style="padding: 0" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info" style="margin-right: 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 ng-non-bindable>{{metric.name}} - {{metric.label.instance_name}}</code><br />
|
||||
Result: <code ng-non-bindable>cpu/usage_time - server1-europe-west-1</code><br /><br />
|
||||
|
||||
<strong>Patterns</strong><br />
|
||||
<ul>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.type}}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.name}}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.service}}</code> = service part of metric e.g. compute
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.label.label_name}}</code> = Metric label metadata e.g.
|
||||
metric.label.instance_name
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{resource.label.label_name}}</code> = Resource label metadata e.g. resource.label.zone
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryError">
|
||||
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
|
||||
</div>
|
||||
</query-editor-row>
|
||||
<stackdriver-query-editor
|
||||
target="ctrl.target"
|
||||
events="ctrl.panelCtrl.events"
|
||||
datasource="ctrl.datasource"
|
||||
template-srv="ctrl.templateSrv"
|
||||
on-query-change="(ctrl.onQueryChange)"
|
||||
on-execute-query="(ctrl.onExecuteQuery)"
|
||||
></stackdriver-query-editor>
|
||||
</query-editor-row>
|
||||
|
@ -1,29 +1,3 @@
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9 query-keyword">Service</span>
|
||||
<select
|
||||
class="gf-form-input width-12"
|
||||
ng-model="ctrl.service"
|
||||
ng-options="f.value as f.text for f in ctrl.services"
|
||||
ng-change="ctrl.onServiceChange(ctrl.service)"
|
||||
></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9 query-keyword">Metric</span>
|
||||
<gf-form-dropdown
|
||||
model="ctrl.metricType"
|
||||
get-options="ctrl.metrics"
|
||||
class="min-width-20"
|
||||
disabled
|
||||
type="text"
|
||||
allow-custom="true"
|
||||
lookup-text="true"
|
||||
css-class="min-width-12"
|
||||
on-change="ctrl.onMetricTypeChange()"
|
||||
></gf-form-dropdown>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword width-9">Filter</span>
|
||||
@ -37,7 +11,7 @@
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-hide="ctrl.$scope.hideGroupBys">
|
||||
<div class="gf-form-inline" ng-hide="ctrl.hideGroupBys">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword width-9">Group By</span>
|
||||
<div class="gf-form" ng-repeat="segment in ctrl.groupBySegments">
|
||||
|
@ -1,80 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
import * as options from './constants';
|
||||
import { getAlignmentOptionsByMetric, getAggregationOptionsByMetric } from './functions';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export class StackdriverAggregation {
|
||||
constructor() {
|
||||
return {
|
||||
templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.aggregation.html',
|
||||
controller: 'StackdriverAggregationCtrl',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
target: '=',
|
||||
alignmentPeriod: '<',
|
||||
refresh: '&',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class StackdriverAggregationCtrl {
|
||||
alignmentPeriods: any[];
|
||||
aggOptions: any[];
|
||||
alignOptions: any[];
|
||||
target: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private templateSrv) {
|
||||
this.$scope.ctrl = this;
|
||||
this.target = $scope.target;
|
||||
this.alignmentPeriods = options.alignmentPeriods;
|
||||
this.aggOptions = options.aggOptions;
|
||||
this.alignOptions = options.alignOptions;
|
||||
this.setAggOptions();
|
||||
this.setAlignOptions();
|
||||
const self = this;
|
||||
$scope.$on('metricTypeChanged', () => {
|
||||
self.setAggOptions();
|
||||
self.setAlignOptions();
|
||||
});
|
||||
}
|
||||
|
||||
setAlignOptions() {
|
||||
this.alignOptions = getAlignmentOptionsByMetric(this.target.valueType, this.target.metricKind);
|
||||
if (!this.alignOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner))) {
|
||||
this.target.aggregation.perSeriesAligner = this.alignOptions.length > 0 ? this.alignOptions[0].value : '';
|
||||
}
|
||||
}
|
||||
|
||||
setAggOptions() {
|
||||
this.aggOptions = getAggregationOptionsByMetric(this.target.valueType, this.target.metricKind);
|
||||
|
||||
if (!this.aggOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.crossSeriesReducer))) {
|
||||
this.deselectAggregationOption('REDUCE_NONE');
|
||||
}
|
||||
|
||||
if (this.target.aggregation.groupBys.length > 0) {
|
||||
this.aggOptions = this.aggOptions.filter(o => o.value !== 'REDUCE_NONE');
|
||||
this.deselectAggregationOption('REDUCE_NONE');
|
||||
}
|
||||
}
|
||||
|
||||
formatAlignmentText() {
|
||||
const selectedAlignment = this.alignOptions.find(
|
||||
ap => ap.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner)
|
||||
);
|
||||
return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${
|
||||
selectedAlignment ? selectedAlignment.text : ''
|
||||
})`;
|
||||
}
|
||||
|
||||
deselectAggregationOption(notValidOptionValue: string) {
|
||||
const newValue = this.aggOptions.find(o => o.value !== notValidOptionValue);
|
||||
this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : '';
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('stackdriverAggregation', StackdriverAggregation);
|
||||
coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);
|
@ -1,97 +1,26 @@
|
||||
import _ from 'lodash';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import './query_aggregation_ctrl';
|
||||
import './query_filter_ctrl';
|
||||
|
||||
export interface QueryMeta {
|
||||
alignmentPeriod: string;
|
||||
rawQuery: string;
|
||||
rawQueryString: string;
|
||||
metricLabels: { [key: string]: string[] };
|
||||
resourceLabels: { [key: string]: string[] };
|
||||
}
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { Target } from './types';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
export class StackdriverQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
target: {
|
||||
defaultProject: string;
|
||||
unit: string;
|
||||
metricType: string;
|
||||
service: string;
|
||||
refId: string;
|
||||
aggregation: {
|
||||
crossSeriesReducer: string;
|
||||
alignmentPeriod: string;
|
||||
perSeriesAligner: string;
|
||||
groupBys: string[];
|
||||
};
|
||||
filters: string[];
|
||||
aliasBy: string;
|
||||
metricKind: any;
|
||||
valueType: any;
|
||||
};
|
||||
|
||||
defaultDropdownValue = 'Select Metric';
|
||||
defaultServiceValue = 'All Services';
|
||||
|
||||
defaults = {
|
||||
defaultProject: 'loading project...',
|
||||
metricType: this.defaultDropdownValue,
|
||||
service: this.defaultServiceValue,
|
||||
metric: '',
|
||||
unit: '',
|
||||
aggregation: {
|
||||
crossSeriesReducer: 'REDUCE_MEAN',
|
||||
alignmentPeriod: 'stackdriver-auto',
|
||||
perSeriesAligner: 'ALIGN_MEAN',
|
||||
groupBys: [],
|
||||
},
|
||||
filters: [],
|
||||
showAggregationOptions: false,
|
||||
aliasBy: '',
|
||||
metricKind: '',
|
||||
valueType: '',
|
||||
};
|
||||
|
||||
showHelp: boolean;
|
||||
showLastQuery: boolean;
|
||||
lastQueryMeta: QueryMeta;
|
||||
lastQueryError?: string;
|
||||
templateSrv: TemplateSrv;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector) {
|
||||
constructor($scope, $injector, templateSrv) {
|
||||
super($scope, $injector);
|
||||
_.defaultsDeep(this.target, this.defaults);
|
||||
this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
|
||||
this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
|
||||
this.templateSrv = templateSrv;
|
||||
this.onQueryChange = this.onQueryChange.bind(this);
|
||||
this.onExecuteQuery = this.onExecuteQuery.bind(this);
|
||||
}
|
||||
|
||||
onDataReceived(dataList) {
|
||||
this.lastQueryError = null;
|
||||
this.lastQueryMeta = null;
|
||||
|
||||
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
|
||||
if (anySeriesFromQuery) {
|
||||
this.lastQueryMeta = anySeriesFromQuery.meta;
|
||||
this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery);
|
||||
}
|
||||
onQueryChange(target: Target) {
|
||||
Object.assign(this.target, target);
|
||||
}
|
||||
|
||||
onDataError(err) {
|
||||
if (err.data && err.data.results) {
|
||||
const queryRes = err.data.results[this.target.refId];
|
||||
if (queryRes && queryRes.error) {
|
||||
this.lastQueryMeta = queryRes.meta;
|
||||
this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery);
|
||||
|
||||
let jsonBody;
|
||||
try {
|
||||
jsonBody = JSON.parse(queryRes.error);
|
||||
} catch {
|
||||
}
|
||||
|
||||
this.lastQueryError = jsonBody.error.message;
|
||||
}
|
||||
}
|
||||
onExecuteQuery() {
|
||||
this.$scope.ctrl.refresh();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
import { FilterSegments } from './filter_segments';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { FilterSegments, DefaultFilterValue } from './filter_segments';
|
||||
|
||||
export class StackdriverFilter {
|
||||
/** @ngInject */
|
||||
@ -10,13 +9,15 @@ export class StackdriverFilter {
|
||||
templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
|
||||
controller: 'StackdriverFilterCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
target: '=',
|
||||
datasource: '=',
|
||||
refresh: '&',
|
||||
defaultDropdownValue: '<',
|
||||
defaultServiceValue: '<',
|
||||
labelData: '<',
|
||||
loading: '<',
|
||||
groupBys: '<',
|
||||
filters: '<',
|
||||
filtersChanged: '&',
|
||||
groupBysChanged: '&',
|
||||
hideGroupBys: '<',
|
||||
},
|
||||
};
|
||||
@ -24,46 +25,28 @@ export class StackdriverFilter {
|
||||
}
|
||||
|
||||
export class StackdriverFilterCtrl {
|
||||
metricLabels: { [key: string]: string[] };
|
||||
resourceLabels: { [key: string]: string[] };
|
||||
resourceTypes: string[];
|
||||
|
||||
defaultRemoveGroupByValue = '-- remove group by --';
|
||||
resourceTypeValue = 'resource.type';
|
||||
loadLabelsPromise: Promise<any>;
|
||||
|
||||
service: string;
|
||||
metricType: string;
|
||||
metricDescriptors: any[];
|
||||
metrics: any[];
|
||||
services: any[];
|
||||
groupBySegments: any[];
|
||||
filterSegments: FilterSegments;
|
||||
removeSegment: any;
|
||||
target: any;
|
||||
datasource: any;
|
||||
filters: string[];
|
||||
groupBys: string[];
|
||||
hideGroupBys: boolean;
|
||||
labelData: any;
|
||||
loading: Promise<any>;
|
||||
filtersChanged: (filters) => void;
|
||||
groupBysChanged: (groupBys) => void;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private uiSegmentSrv, private templateSrv, private $rootScope) {
|
||||
this.datasource = $scope.datasource;
|
||||
this.target = $scope.target;
|
||||
this.metricType = $scope.defaultDropdownValue;
|
||||
this.service = $scope.defaultServiceValue;
|
||||
|
||||
this.metricDescriptors = [];
|
||||
this.metrics = [];
|
||||
this.services = [];
|
||||
|
||||
this.getCurrentProject()
|
||||
.then(this.loadMetricDescriptors.bind(this))
|
||||
.then(this.getLabels.bind(this));
|
||||
|
||||
this.initSegments($scope.hideGroupBys);
|
||||
constructor(private $scope, private uiSegmentSrv, private templateSrv) {
|
||||
this.$scope.ctrl = this;
|
||||
this.initSegments(this.hideGroupBys);
|
||||
}
|
||||
|
||||
initSegments(hideGroupBys: boolean) {
|
||||
if (!hideGroupBys) {
|
||||
this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => {
|
||||
this.groupBySegments = this.groupBys.map(groupBy => {
|
||||
return this.uiSegmentSrv.getSegmentForValue(groupBy);
|
||||
});
|
||||
this.ensurePlusButton(this.groupBySegments);
|
||||
@ -73,133 +56,17 @@ export class StackdriverFilterCtrl {
|
||||
|
||||
this.filterSegments = new FilterSegments(
|
||||
this.uiSegmentSrv,
|
||||
this.target,
|
||||
this.filters,
|
||||
this.getFilterKeys.bind(this),
|
||||
this.getFilterValues.bind(this)
|
||||
);
|
||||
this.filterSegments.buildSegmentModel();
|
||||
}
|
||||
|
||||
async getCurrentProject() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') {
|
||||
this.target.defaultProject = await this.datasource.getDefaultProject();
|
||||
}
|
||||
resolve(this.target.defaultProject);
|
||||
} catch (error) {
|
||||
appEvents.emit('ds-request-error', error);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadMetricDescriptors() {
|
||||
if (this.target.defaultProject !== 'loading project...') {
|
||||
this.metricDescriptors = await this.datasource.getMetricTypes(this.target.defaultProject);
|
||||
this.services = this.getServicesList();
|
||||
this.metrics = this.getMetricsList();
|
||||
return this.metricDescriptors;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getServicesList() {
|
||||
const defaultValue = { value: this.$scope.defaultServiceValue, text: this.$scope.defaultServiceValue };
|
||||
const services = this.metricDescriptors.map(m => {
|
||||
return {
|
||||
value: m.service,
|
||||
text: m.serviceShortName,
|
||||
};
|
||||
});
|
||||
|
||||
if (services.find(m => m.value === this.target.service)) {
|
||||
this.service = this.target.service;
|
||||
}
|
||||
|
||||
return services.length > 0 ? [defaultValue, ..._.uniqBy(services, 'value')] : [];
|
||||
}
|
||||
|
||||
getMetricsList() {
|
||||
const metrics = this.metricDescriptors.map(m => {
|
||||
return {
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
serviceShortName: m.serviceShortName,
|
||||
text: m.displayName,
|
||||
title: m.description,
|
||||
};
|
||||
});
|
||||
|
||||
let result;
|
||||
if (this.target.service === this.$scope.defaultServiceValue) {
|
||||
result = metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` }));
|
||||
} else {
|
||||
result = metrics.filter(m => m.service === this.target.service);
|
||||
}
|
||||
|
||||
if (result.find(m => m.value === this.templateSrv.replace(this.target.metricType))) {
|
||||
this.metricType = this.target.metricType;
|
||||
} else if (result.length > 0) {
|
||||
this.metricType = this.target.metricType = result[0].value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getLabels() {
|
||||
this.loadLabelsPromise = new Promise(async resolve => {
|
||||
try {
|
||||
const { meta } = await this.datasource.getLabels(this.target.metricType, this.target.refId);
|
||||
this.metricLabels = meta.metricLabels;
|
||||
this.resourceLabels = meta.resourceLabels;
|
||||
this.resourceTypes = meta.resourceTypes;
|
||||
resolve();
|
||||
} catch (error) {
|
||||
if (error.data && error.data.message) {
|
||||
console.log(error.data.message);
|
||||
} else {
|
||||
console.log(error);
|
||||
}
|
||||
appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.target.metricType]);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onServiceChange() {
|
||||
this.target.service = this.service;
|
||||
this.metrics = this.getMetricsList();
|
||||
this.setMetricType();
|
||||
this.getLabels();
|
||||
if (!this.metrics.find(m => m.value === this.target.metricType)) {
|
||||
this.target.metricType = this.$scope.defaultDropdownValue;
|
||||
} else {
|
||||
this.$scope.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async onMetricTypeChange() {
|
||||
this.setMetricType();
|
||||
this.$scope.refresh();
|
||||
this.getLabels();
|
||||
}
|
||||
|
||||
setMetricType() {
|
||||
this.target.metricType = this.metricType;
|
||||
const { valueType, metricKind, unit } = this.metricDescriptors.find(
|
||||
m => m.type === this.templateSrv.replace(this.metricType)
|
||||
);
|
||||
this.target.unit = unit;
|
||||
this.target.valueType = valueType;
|
||||
this.target.metricKind = metricKind;
|
||||
this.$rootScope.$broadcast('metricTypeChanged');
|
||||
}
|
||||
|
||||
async createLabelKeyElements() {
|
||||
await this.loadLabelsPromise;
|
||||
await this.loading;
|
||||
|
||||
let elements = Object.keys(this.metricLabels || {}).map(l => {
|
||||
let elements = Object.keys(this.labelData.metricLabels || {}).map(l => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: `metric.label.${l}`,
|
||||
expandable: false,
|
||||
@ -208,7 +75,7 @@ export class StackdriverFilterCtrl {
|
||||
|
||||
elements = [
|
||||
...elements,
|
||||
...Object.keys(this.resourceLabels || {}).map(l => {
|
||||
...Object.keys(this.labelData.resourceLabels || {}).map(l => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: `resource.label.${l}`,
|
||||
expandable: false,
|
||||
@ -216,7 +83,7 @@ export class StackdriverFilterCtrl {
|
||||
}),
|
||||
];
|
||||
|
||||
if (this.resourceTypes && this.resourceTypes.length > 0) {
|
||||
if (this.labelData.resourceTypes && this.labelData.resourceTypes.length > 0) {
|
||||
elements = [
|
||||
...elements,
|
||||
this.uiSegmentSrv.newSegment({
|
||||
@ -229,10 +96,10 @@ export class StackdriverFilterCtrl {
|
||||
return elements;
|
||||
}
|
||||
|
||||
async getFilterKeys(segment, removeText?: string) {
|
||||
async getFilterKeys(segment, removeText: string) {
|
||||
let elements = await this.createLabelKeyElements();
|
||||
|
||||
if (this.target.filters.indexOf(this.resourceTypeValue) !== -1) {
|
||||
if (this.filters.indexOf(this.resourceTypeValue) !== -1) {
|
||||
elements = elements.filter(e => e.value !== this.resourceTypeValue);
|
||||
}
|
||||
|
||||
@ -241,21 +108,24 @@ export class StackdriverFilterCtrl {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.removeSegment.value = removeText;
|
||||
return [...elements, this.removeSegment];
|
||||
return segment.type === 'plus-button'
|
||||
? elements
|
||||
: [
|
||||
...elements,
|
||||
this.uiSegmentSrv.newSegment({ fake: true, value: removeText || this.defaultRemoveGroupByValue }),
|
||||
];
|
||||
}
|
||||
|
||||
async getGroupBys(segment) {
|
||||
let elements = await this.createLabelKeyElements();
|
||||
|
||||
elements = elements.filter(e => this.target.aggregation.groupBys.indexOf(e.value) === -1);
|
||||
elements = elements.filter(e => this.groupBys.indexOf(e.value) === -1);
|
||||
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
|
||||
if (noValueOrPlusButton && elements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.removeSegment.value = this.defaultRemoveGroupByValue;
|
||||
return [...elements, this.removeSegment];
|
||||
return segment.type === 'plus-button' ? elements : [...elements, this.removeSegment];
|
||||
}
|
||||
|
||||
groupByChanged(segment, index) {
|
||||
@ -272,43 +142,45 @@ export class StackdriverFilterCtrl {
|
||||
return memo;
|
||||
};
|
||||
|
||||
this.target.aggregation.groupBys = this.groupBySegments.reduce(reducer, []);
|
||||
const groupBys = this.groupBySegments.reduce(reducer, []);
|
||||
this.groupBysChanged({ groupBys });
|
||||
this.ensurePlusButton(this.groupBySegments);
|
||||
this.$rootScope.$broadcast('metricTypeChanged');
|
||||
this.$scope.refresh();
|
||||
}
|
||||
|
||||
async getFilters(segment, index) {
|
||||
const hasNoFilterKeys = this.metricLabels && Object.keys(this.metricLabels).length === 0;
|
||||
await this.loading;
|
||||
const hasNoFilterKeys = this.labelData.metricLabels && Object.keys(this.labelData.metricLabels).length === 0;
|
||||
return this.filterSegments.getFilters(segment, index, hasNoFilterKeys);
|
||||
}
|
||||
|
||||
getFilterValues(index) {
|
||||
const filterKey = this.templateSrv.replace(this.filterSegments.filterSegments[index - 2].value);
|
||||
if (!filterKey || !this.metricLabels || Object.keys(this.metricLabels).length === 0) {
|
||||
if (!filterKey || !this.labelData.metricLabels || Object.keys(this.labelData.metricLabels).length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7);
|
||||
|
||||
if (filterKey.startsWith('metric.label.') && this.metricLabels.hasOwnProperty(shortKey)) {
|
||||
return this.metricLabels[shortKey];
|
||||
if (filterKey.startsWith('metric.label.') && this.labelData.metricLabels.hasOwnProperty(shortKey)) {
|
||||
return this.labelData.metricLabels[shortKey];
|
||||
}
|
||||
|
||||
if (filterKey.startsWith('resource.label.') && this.resourceLabels.hasOwnProperty(shortKey)) {
|
||||
return this.resourceLabels[shortKey];
|
||||
if (filterKey.startsWith('resource.label.') && this.labelData.resourceLabels.hasOwnProperty(shortKey)) {
|
||||
return this.labelData.resourceLabels[shortKey];
|
||||
}
|
||||
|
||||
if (filterKey === this.resourceTypeValue) {
|
||||
return this.resourceTypes;
|
||||
return this.labelData.resourceTypes;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
filterSegmentUpdated(segment, index) {
|
||||
this.target.filters = this.filterSegments.filterSegmentUpdated(segment, index);
|
||||
this.$scope.refresh();
|
||||
const filters = this.filterSegments.filterSegmentUpdated(segment, index);
|
||||
if (!filters.some(f => f === DefaultFilterValue)) {
|
||||
this.filtersChanged({ filters });
|
||||
}
|
||||
}
|
||||
|
||||
ensurePlusButton(segments) {
|
||||
|
@ -82,7 +82,6 @@ describe('StackdriverDataSource', () => {
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
aggregation: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,74 +0,0 @@
|
||||
import { StackdriverAggregationCtrl } from '../query_aggregation_ctrl';
|
||||
|
||||
describe('StackdriverAggregationCtrl', () => {
|
||||
let ctrl;
|
||||
describe('aggregation and alignment options', () => {
|
||||
describe('when new query result is returned from the server', () => {
|
||||
describe('and result is double and gauge and no group by is used', () => {
|
||||
beforeEach(async () => {
|
||||
ctrl = new StackdriverAggregationCtrl(
|
||||
{
|
||||
$on: () => {},
|
||||
target: {
|
||||
valueType: 'DOUBLE',
|
||||
metricKind: 'GAUGE',
|
||||
aggregation: { crossSeriesReducer: '', groupBys: [] },
|
||||
},
|
||||
},
|
||||
{
|
||||
replace: s => s,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should populate all aggregate options except two', () => {
|
||||
ctrl.setAggOptions();
|
||||
expect(ctrl.aggOptions.length).toBe(11);
|
||||
expect(ctrl.aggOptions.map(o => o.value)).toEqual(
|
||||
expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should populate all alignment options except two', () => {
|
||||
ctrl.setAlignOptions();
|
||||
expect(ctrl.alignOptions.length).toBe(9);
|
||||
expect(ctrl.alignOptions.map(o => o.value)).toEqual(
|
||||
expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and result is double and gauge and a group by is used', () => {
|
||||
beforeEach(async () => {
|
||||
ctrl = new StackdriverAggregationCtrl(
|
||||
{
|
||||
$on: () => {},
|
||||
target: {
|
||||
valueType: 'DOUBLE',
|
||||
metricKind: 'GAUGE',
|
||||
aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
replace: s => s,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should populate all aggregate options except three', () => {
|
||||
ctrl.setAggOptions();
|
||||
expect(ctrl.aggOptions.length).toBe(10);
|
||||
expect(ctrl.aggOptions.map(o => o.value)).toEqual(
|
||||
expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE', 'REDUCE_NONE'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should select some other reducer than REDUCE_NONE', () => {
|
||||
ctrl.setAggOptions();
|
||||
expect(ctrl.target.aggregation.crossSeriesReducer).not.toBe('');
|
||||
expect(ctrl.target.aggregation.crossSeriesReducer).not.toBe('REDUCE_NONE');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -5,6 +5,7 @@ import { DefaultRemoveFilterValue, DefaultFilterValue } from '../filter_segments
|
||||
describe('StackdriverQueryFilterCtrl', () => {
|
||||
let ctrl;
|
||||
let result;
|
||||
let groupByChangedMock;
|
||||
|
||||
describe('when initializing query editor', () => {
|
||||
beforeEach(() => {
|
||||
@ -32,10 +33,10 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
|
||||
describe('when labels are fetched', () => {
|
||||
beforeEach(async () => {
|
||||
ctrl.metricLabels = { 'metric-key-1': ['metric-value-1'] };
|
||||
ctrl.resourceLabels = { 'resource-key-1': ['resource-value-1'] };
|
||||
ctrl.labelData.metricLabels = { 'metric-key-1': ['metric-value-1'] };
|
||||
ctrl.labelData.resourceLabels = { 'resource-key-1': ['resource-value-1'] };
|
||||
|
||||
result = await ctrl.getGroupBys();
|
||||
result = await ctrl.getGroupBys({ type: '' });
|
||||
});
|
||||
|
||||
it('should populate group bys segments', () => {
|
||||
@ -48,17 +49,17 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
|
||||
describe('when a group by label is selected', () => {
|
||||
beforeEach(async () => {
|
||||
ctrl.metricLabels = {
|
||||
ctrl.labelData.metricLabels = {
|
||||
'metric-key-1': ['metric-value-1'],
|
||||
'metric-key-2': ['metric-value-2'],
|
||||
};
|
||||
ctrl.resourceLabels = {
|
||||
ctrl.labelData.resourceLabels = {
|
||||
'resource-key-1': ['resource-value-1'],
|
||||
'resource-key-2': ['resource-value-2'],
|
||||
};
|
||||
ctrl.target.aggregation.groupBys = ['metric.label.metric-key-1', 'resource.label.resource-key-1'];
|
||||
ctrl.groupBys = ['metric.label.metric-key-1', 'resource.label.resource-key-1'];
|
||||
|
||||
result = await ctrl.getGroupBys();
|
||||
result = await ctrl.getGroupBys({ type: '' });
|
||||
});
|
||||
|
||||
it('should not be used to populate group bys segments', () => {
|
||||
@ -71,6 +72,8 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
|
||||
describe('when a group by is selected', () => {
|
||||
beforeEach(() => {
|
||||
groupByChangedMock = jest.fn();
|
||||
ctrl.groupBysChanged = groupByChangedMock;
|
||||
const removeSegment = { fake: true, value: '-- remove group by --' };
|
||||
const segment = { value: 'groupby1' };
|
||||
ctrl.groupBySegments = [segment, removeSegment];
|
||||
@ -78,12 +81,14 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
});
|
||||
|
||||
it('should be added to group bys list', () => {
|
||||
expect(ctrl.target.aggregation.groupBys.length).toBe(1);
|
||||
expect(groupByChangedMock).toHaveBeenCalledWith({ groupBys: ['groupby1'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a selected group by is removed', () => {
|
||||
beforeEach(() => {
|
||||
groupByChangedMock = jest.fn();
|
||||
ctrl.groupBysChanged = groupByChangedMock;
|
||||
const removeSegment = { fake: true, value: '-- remove group by --' };
|
||||
const segment = { value: 'groupby1' };
|
||||
ctrl.groupBySegments = [segment, removeSegment];
|
||||
@ -91,7 +96,7 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
});
|
||||
|
||||
it('should be added to group bys list', () => {
|
||||
expect(ctrl.target.aggregation.groupBys.length).toBe(0);
|
||||
expect(groupByChangedMock).toHaveBeenCalledWith({ groupBys: [] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -130,11 +135,11 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
|
||||
describe('when values for a key filter part are fetched', () => {
|
||||
beforeEach(async () => {
|
||||
ctrl.metricLabels = {
|
||||
ctrl.labelData.metricLabels = {
|
||||
'metric-key-1': ['metric-value-1'],
|
||||
'metric-key-2': ['metric-value-2'],
|
||||
};
|
||||
ctrl.resourceLabels = {
|
||||
ctrl.labelData.resourceLabels = {
|
||||
'resource-key-1': ['resource-value-1'],
|
||||
'resource-key-2': ['resource-value-2'],
|
||||
};
|
||||
@ -155,11 +160,11 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
|
||||
describe('when values for a value filter part are fetched', () => {
|
||||
beforeEach(async () => {
|
||||
ctrl.metricLabels = {
|
||||
ctrl.labelData.metricLabels = {
|
||||
'metric-key-1': ['metric-value-1'],
|
||||
'metric-key-2': ['metric-value-2'],
|
||||
};
|
||||
ctrl.resourceLabels = {
|
||||
ctrl.labelData.resourceLabels = {
|
||||
'resource-key-1': ['resource-value-1'],
|
||||
'resource-key-2': ['resource-value-2'],
|
||||
};
|
||||
@ -198,6 +203,7 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when has one existing filter', () => {
|
||||
describe('and user clicks on key segment', () => {
|
||||
beforeEach(() => {
|
||||
@ -213,7 +219,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
];
|
||||
ctrl.filterSegmentUpdated(existingKeySegment, 0);
|
||||
});
|
||||
|
||||
it('should not add any new segments', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
|
||||
@ -229,7 +234,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
ctrl.filterSegments.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment];
|
||||
ctrl.filterSegmentUpdated(existingValueSegment, 2);
|
||||
});
|
||||
|
||||
it('should ensure that plus segment exists', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
|
||||
@ -238,7 +242,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and user clicks on value segment and value is equal to fake value', () => {
|
||||
beforeEach(() => {
|
||||
const existingKeySegment = { value: 'filterkey1', type: 'key' };
|
||||
@ -247,7 +250,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
ctrl.filterSegments.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment];
|
||||
ctrl.filterSegmentUpdated(existingValueSegment, 2);
|
||||
});
|
||||
|
||||
it('should not add plus segment', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(3);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
|
||||
@ -269,13 +271,11 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
];
|
||||
ctrl.filterSegmentUpdated(existingKeySegment, 0);
|
||||
});
|
||||
|
||||
it('should remove filter segments', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(1);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('plus-button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and user removes key segment and there is a previous filter', () => {
|
||||
beforeEach(() => {
|
||||
const existingKeySegment1 = { value: DefaultRemoveFilterValue, type: 'key' };
|
||||
@ -296,7 +296,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
];
|
||||
ctrl.filterSegmentUpdated(existingKeySegment2, 4);
|
||||
});
|
||||
|
||||
it('should remove filter segments and the condition segment', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
|
||||
@ -305,7 +304,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and user removes key segment and there is a filter after it', () => {
|
||||
beforeEach(() => {
|
||||
const existingKeySegment1 = { value: DefaultRemoveFilterValue, type: 'key' };
|
||||
@ -326,7 +324,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
];
|
||||
ctrl.filterSegmentUpdated(existingKeySegment1, 0);
|
||||
});
|
||||
|
||||
it('should remove filter segments and the condition segment', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
|
||||
@ -335,7 +332,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and user clicks on plus button', () => {
|
||||
beforeEach(() => {
|
||||
const existingKeySegment = { value: 'filterkey1', type: 'key' };
|
||||
@ -350,7 +346,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
];
|
||||
ctrl.filterSegmentUpdated(plusSegment, 3);
|
||||
});
|
||||
|
||||
it('should condition segment and new filter segments', () => {
|
||||
expect(ctrl.filterSegments.filterSegments.length).toBe(7);
|
||||
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
|
||||
@ -367,13 +362,6 @@ describe('StackdriverQueryFilterCtrl', () => {
|
||||
});
|
||||
|
||||
function createCtrlWithFakes(existingFilters?: string[]) {
|
||||
StackdriverFilterCtrl.prototype.loadMetricDescriptors = () => {
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
StackdriverFilterCtrl.prototype.getLabels = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const fakeSegmentServer = {
|
||||
newKey: val => {
|
||||
return { value: val, type: 'key' };
|
||||
@ -403,40 +391,24 @@ function createCtrlWithFakes(existingFilters?: string[]) {
|
||||
},
|
||||
};
|
||||
const scope = {
|
||||
target: createTarget(existingFilters),
|
||||
hideGroupBys: false,
|
||||
groupBys: [],
|
||||
filters: existingFilters || [],
|
||||
labelData: {
|
||||
metricLabels: {},
|
||||
resourceLabels: {},
|
||||
resourceTypes: [],
|
||||
},
|
||||
filtersChanged: () => {},
|
||||
groupBysChanged: () => {},
|
||||
datasource: {
|
||||
getDefaultProject: () => {
|
||||
return 'project';
|
||||
},
|
||||
},
|
||||
defaultDropdownValue: 'Select Metric',
|
||||
defaultServiceValue: 'All Services',
|
||||
refresh: () => {},
|
||||
};
|
||||
|
||||
return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub(), { $broadcast: param => {} });
|
||||
}
|
||||
|
||||
function createTarget(existingFilters?: string[]) {
|
||||
return {
|
||||
project: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
unit: '',
|
||||
metricType: 'ametric',
|
||||
service: '',
|
||||
refId: 'A',
|
||||
aggregation: {
|
||||
crossSeriesReducer: '',
|
||||
alignmentPeriod: '',
|
||||
perSeriesAligner: '',
|
||||
groupBys: [],
|
||||
},
|
||||
filters: existingFilters || [],
|
||||
aliasBy: '',
|
||||
metricService: '',
|
||||
metricKind: '',
|
||||
valueType: '',
|
||||
};
|
||||
Object.assign(StackdriverFilterCtrl.prototype, scope);
|
||||
return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub());
|
||||
}
|
||||
|
@ -19,3 +19,50 @@ export interface VariableQueryData {
|
||||
metricTypes: Array<{ value: string; name: string }>;
|
||||
services: Array<{ value: string; name: string }>;
|
||||
}
|
||||
|
||||
export interface Target {
|
||||
defaultProject: string;
|
||||
unit: string;
|
||||
metricType: string;
|
||||
service: string;
|
||||
refId: string;
|
||||
crossSeriesReducer: string;
|
||||
alignmentPeriod: string;
|
||||
perSeriesAligner: string;
|
||||
groupBys: string[];
|
||||
filters: string[];
|
||||
aliasBy: string;
|
||||
metricKind: string;
|
||||
valueType: string;
|
||||
}
|
||||
|
||||
export interface AnnotationTarget {
|
||||
defaultProject: string;
|
||||
metricType: string;
|
||||
refId: string;
|
||||
filters: string[];
|
||||
metricKind: string;
|
||||
valueType: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface QueryMeta {
|
||||
alignmentPeriod: string;
|
||||
rawQuery: string;
|
||||
rawQueryString: string;
|
||||
metricLabels: { [key: string]: string[] };
|
||||
resourceLabels: { [key: string]: string[] };
|
||||
resourceTypes: string[];
|
||||
}
|
||||
|
||||
export interface MetricDescriptor {
|
||||
valueType: string;
|
||||
metricKind: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
service: string;
|
||||
serviceShortName: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
}
|
||||
|
@ -15,6 +15,13 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
});
|
||||
|
||||
return <Gauge timeSeries={vmSeries} {...this.props.options} width={width} height={height} />;
|
||||
return (
|
||||
<Gauge
|
||||
timeSeries={vmSeries}
|
||||
{...this.props.options}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
5
public/app/types/templates.ts
Normal file
5
public/app/types/templates.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Variable {
|
||||
name: string;
|
||||
type: string;
|
||||
current: any;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user