mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataLinks: enable access to labels & field names (#18918)
* POC: trying to see if there is a way to support objects in template interpolations * Added support for nested objects, and arrays * Added accessor cache * fixed unit tests * First take * Use links supplier in graph * Add field's index to cache items * Get field index from field cache * CHange FiledCacheItem to FieldWithIndex * Add refId to TimeSeries class * Make field link supplier work with _series, _field and _value vars * use field link supplier in graph * Fix yaxis settings * Update dashboard schema version and add migration for data links variables * Update snapshots * Update build in data link variables * FieldCache - idx -> index * Add current query results to panel editor * WIP Updated data links dropdown to display new variables * Fix build * Update variables syntac in field display, update migration * Field links supplier: review updates * Add data frame view and field name to TimeSeries for later inspection * Retrieve data frame from TimeSeries when clicking on plot graph * Use data frame's index instead of view * Retrieve data frame by index instead of view on TimeSeries * Update data links prism regex * Fix typecheck * Add value variables to suggestions list * UI update * Rename field to config in DisplayProcessorOptions * Proces single value of a field instead of entire data frame * Updated font size from 10px to 12px for auto complete * Replace fieldName with fieldIndex in TimeSeries * Don't use .entries() for iterating in field cache * Don't use FieldCache when retrieving field for datalinks in graph * Add value calculation variable to data links (#19031) * Add support for labels with dots in the name (#19033) * Docs update * Use field name instead of removed series.fieldName * Add test dashboard * Typos fix * Make visualization tab subscribe to query results * Added tags to dashboard so it shows up in lists * minor docs fix * Update singlestat-ish variables suggestions to contain series variables * Decrease suggestions update debounce * Enable whitespace characters(new line, space) in links and strip them when processing the data link * minor data links UI update * DataLinks: Add __from and __to variables suggestions to data links (#19093) * Add from and to variables suggestions to data links * Update docs * UI update and added info text * Change ESC global bind to bind (doesn't capture ESC on input) * Close datalinks suggestions on ESC * Remove unnecessary fragment
This commit is contained in:
parent
fc10bd7b8e
commit
fd21e0ba14
510
devenv/dev-dashboards/feature-templating/testdata-datalinks.json
Normal file
510
devenv/dev-dashboards/feature-templating/testdata-datalinks.json
Normal file
@ -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,
|
||||||
|
"iteration": 1568372030444,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"content": "## Data link variables overview\n\nThis dashboard presents variables that one can use when creating *data links*. All links redirect to this dashboard and this panel represents the values that were interpolated in the link that was clicked.\n\n\n#### Series variables\n1. **Name:** <span style=\"color: orange;\">$seriesName</span>\n2. **label.datacenter:** <span style=\"color: orange;\">$labelDatacenter</span>\n3. **label.datacenter.region:** <span style=\"color: orange;\">$labelDatacenterRegion</span>\n\n#### Field variables\n1. **Name:** <span style=\"color: orange;\">$fieldName</span>\n\n#### Value variables\n1. **Time:** <span style=\"color: orange;\">$valueTime</span>\n2. **Numeric:** <span style=\"color: orange;\">$valueNumeric</span>\n3. **Text:** <span style=\"color: orange;\">$valueText</span>\n4. **Calc:** <span style=\"color: orange;\">$valueCalc</span>\n\n",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 16,
|
||||||
|
"w": 6,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"mode": "markdown",
|
||||||
|
"options": {},
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "",
|
||||||
|
"transparent": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"fill": 1,
|
||||||
|
"fillGradient": 0,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 9,
|
||||||
|
"x": 6,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"options": {
|
||||||
|
"dataLinks": [
|
||||||
|
{
|
||||||
|
"targetBlank": false,
|
||||||
|
"title": "Drill it down",
|
||||||
|
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source?var-seriesName=${__series.name}&var-labelDatacenter=${__series.labels.datacenter}&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}&var-valueTime=${__value.time}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"alias": "Foo datacenter",
|
||||||
|
"labels": "datacenter=foo,datacenter.region=us-east-1",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "Bar datacenter",
|
||||||
|
"labels": "datacenter=bar,datacenter.region=us-east-2",
|
||||||
|
"refId": "B",
|
||||||
|
"scenarioId": "random_walk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Multiple series",
|
||||||
|
"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,
|
||||||
|
"fill": 1,
|
||||||
|
"fillGradient": 0,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 9,
|
||||||
|
"x": 15,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"options": {
|
||||||
|
"dataLinks": [
|
||||||
|
{
|
||||||
|
"targetBlank": false,
|
||||||
|
"title": "Drill it down",
|
||||||
|
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source?var-seriesName=${__series.name}&var-valueTime=${__value.time}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}&var-fieldName=${__field.name}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"alias": "Foo datacenter",
|
||||||
|
"labels": "datacenter=foo,datacenter.region=us-east-1",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk_table",
|
||||||
|
"stringInput": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Multiple fields",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cacheTimeout": null,
|
||||||
|
"datasource": "-- Dashboard --",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 9,
|
||||||
|
"x": 6,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"links": [],
|
||||||
|
"options": {
|
||||||
|
"displayMode": "lcd",
|
||||||
|
"fieldOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"defaults": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"targetBlank": true,
|
||||||
|
"title": "Drill it down!",
|
||||||
|
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source\n?var-fieldName=${__field.name}\n&var-labelDatacenter=${__series.labels.datacenter}\n&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}\n&var-valueNumeric=${__value.numeric}\n&var-valueText=${__value.text}\n&var-valueCalc=${__value.calc}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"op": "=",
|
||||||
|
"text": "N/A",
|
||||||
|
"type": 1,
|
||||||
|
"value": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max": 100,
|
||||||
|
"min": 0,
|
||||||
|
"nullValueMode": "connected",
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "${__series.name} - $__calc",
|
||||||
|
"unit": "none"
|
||||||
|
},
|
||||||
|
"override": {},
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"orientation": "horizontal"
|
||||||
|
},
|
||||||
|
"pluginVersion": "6.4.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"panelId": 2,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Value reducers 1",
|
||||||
|
"type": "bargauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "-- Dashboard --",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 9,
|
||||||
|
"x": 15,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"fieldOptions": {
|
||||||
|
"calcs": ["mean"],
|
||||||
|
"defaults": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"title": "Drill it down",
|
||||||
|
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source?var-fieldName=${__field.name}&var-labelDatacenter=${__series.labels.datacenter}&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}&var-valueCalc=${__value.calc}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mappings": [],
|
||||||
|
"max": 100,
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "${__series.name} - $__calc"
|
||||||
|
},
|
||||||
|
"override": {},
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"orientation": "auto",
|
||||||
|
"showThresholdLabels": false,
|
||||||
|
"showThresholdMarkers": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "6.4.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"panelId": 2,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Value reducers 2",
|
||||||
|
"type": "gauge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 20,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": ["gdev", "templating"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": "Series name",
|
||||||
|
"name": "seriesName",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "labelDatacenter",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "labelDatacenterRegion",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "valueTime",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "valueNumeric",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "valueText",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "valueCalc",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"hide": 2,
|
||||||
|
"label": null,
|
||||||
|
"name": "fieldName",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "textbox"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Datalinks - variables",
|
||||||
|
"uid": "wfTJJL5Wz",
|
||||||
|
"version": 1
|
||||||
|
}
|
@ -192,7 +192,7 @@ Panel time overrides & timeshift are described in more detail [here]({{< relref
|
|||||||
|
|
||||||
> Only available in Grafana v6.3+.
|
> Only available in Grafana v6.3+.
|
||||||
|
|
||||||
Data link in graph settings allows adding dynamic links to the visualization. Those links can link to either other dashboard or to an external URL.
|
Data link allows adding dynamic links to the visualization. Those links can link to either other dashboard or to an external URL.
|
||||||
|
|
||||||
{{< docs-imagebox img="/img/docs/data_link.png" max-width= "800px" >}}
|
{{< docs-imagebox img="/img/docs/data_link.png" max-width= "800px" >}}
|
||||||
|
|
||||||
@ -208,14 +208,40 @@ available suggestions:
|
|||||||
{{< docs-imagebox img="/img/docs/data_link_typeahead.png" max-width= "800px" >}}
|
{{< docs-imagebox img="/img/docs/data_link_typeahead.png" max-width= "800px" >}}
|
||||||
|
|
||||||
|
|
||||||
Available built-in variables are:
|
#### Built-in variables
|
||||||
|
|
||||||
1. ``__all_variables`` - will add all current dashboard's variables to the URL
|
``__url_time_range`` - current dashboard's time range (i.e. ``?from=now-6h&to=now``)
|
||||||
2. ``__url_time_range`` - will add current dashboard's time range to the URL (i.e. ``?from=now-6h&to=now``)
|
``__from`` - current dashboard's time range from value
|
||||||
3. ``__series_name`` - will add series name as a query param in the URL (i.e. ``?series=B-series``)
|
``__to`` - current dashboard's time range to value
|
||||||
4. ``__value_time`` - will add datapoint's timestamp (Unix ms epoch) to the URL (i.e. ``?time=1560268814105``)
|
|
||||||
|
#### Series variables
|
||||||
|
Series specific variables are available under ``__series`` namespace:
|
||||||
|
|
||||||
|
``__series.name`` - series name to the URL
|
||||||
|
|
||||||
|
``__series.labels.<LABEL>`` - label's value to the URL. If your label contains dots use ``__series.labels["<LABEL>"]`` syntax
|
||||||
|
|
||||||
|
#### Field variables
|
||||||
|
Field specific variables are available under ``__field`` namespace:
|
||||||
|
|
||||||
|
``__field.name`` - field name to the URL
|
||||||
|
|
||||||
|
#### Value variables
|
||||||
|
Value specific variables are available under ``__value`` namespace:
|
||||||
|
|
||||||
|
``__value.time`` - value's timestamp (Unix ms epoch) to the URL (i.e. ``?time=1560268814105``)
|
||||||
|
|
||||||
|
``__value.raw`` - raw value
|
||||||
|
|
||||||
|
``__value.numeric`` - numeric representation of a value
|
||||||
|
|
||||||
|
``__value.text`` - text representation of a value
|
||||||
|
|
||||||
|
``__value.calc`` - calculation name if the value is result of calculation
|
||||||
|
|
||||||
|
|
||||||
#### Template variables in data links
|
|
||||||
|
#### Template variables
|
||||||
|
|
||||||
When linking to another dashboard that uses template variables, you can use ``var-myvar=${myvar}`` syntax (where ``myvar`` is a name of template variable)
|
When linking to another dashboard that uses template variables, you can use ``var-myvar=${myvar}`` syntax (where ``myvar`` is a name of template variable)
|
||||||
to use current dashboard's variable value.
|
to use current dashboard's variable value. If you want to add all of the current dashboard's variables to the URL use ``__all_variables`` variable.
|
||||||
|
@ -20,7 +20,8 @@ export class FieldCache {
|
|||||||
index: idx,
|
index: idx,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (const [index, field] of data.fields.entries()) {
|
for (let i = 0; i < data.fields.length; i++) {
|
||||||
|
const field = data.fields[i];
|
||||||
// Make sure it has a type
|
// Make sure it has a type
|
||||||
if (field.type === FieldType.other) {
|
if (field.type === FieldType.other) {
|
||||||
const t = guessFieldTypeForField(field);
|
const t = guessFieldTypeForField(field);
|
||||||
@ -33,13 +34,13 @@ export class FieldCache {
|
|||||||
}
|
}
|
||||||
this.fieldByType[field.type].push({
|
this.fieldByType[field.type].push({
|
||||||
...field,
|
...field,
|
||||||
index,
|
index: i,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.fieldByName[field.name]) {
|
if (this.fieldByName[field.name]) {
|
||||||
console.warn('Duplicate field names in DataFrame: ', field.name);
|
console.warn('Duplicate field names in DataFrame: ', field.name);
|
||||||
} else {
|
} else {
|
||||||
this.fieldByName[field.name] = { ...field, index };
|
this.fieldByName[field.name] = { ...field, index: i };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,13 @@ import React, { useState, ChangeEvent, useContext } from 'react';
|
|||||||
import { DataLink } from '@grafana/data';
|
import { DataLink } from '@grafana/data';
|
||||||
import { FormField, Switch } from '../index';
|
import { FormField, Switch } from '../index';
|
||||||
import { VariableSuggestion } from './DataLinkSuggestions';
|
import { VariableSuggestion } from './DataLinkSuggestions';
|
||||||
import { css, cx } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { ThemeContext } from '../../themes/index';
|
import { ThemeContext } from '../../themes/index';
|
||||||
import { DataLinkInput } from './DataLinkInput';
|
import { DataLinkInput } from './DataLinkInput';
|
||||||
|
|
||||||
interface DataLinkEditorProps {
|
interface DataLinkEditorProps {
|
||||||
index: number;
|
index: number;
|
||||||
|
isLast: boolean;
|
||||||
value: DataLink;
|
value: DataLink;
|
||||||
suggestions: VariableSuggestion[];
|
suggestions: VariableSuggestion[];
|
||||||
onChange: (index: number, link: DataLink) => void;
|
onChange: (index: number, link: DataLink) => void;
|
||||||
@ -15,7 +16,7 @@ interface DataLinkEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
||||||
({ index, value, onChange, onRemove, suggestions }) => {
|
({ index, value, onChange, onRemove, suggestions, isLast }) => {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
const [title, setTitle] = useState(value.title);
|
const [title, setTitle] = useState(value.title);
|
||||||
|
|
||||||
@ -38,46 +39,48 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
|||||||
onChange(index, { ...value, targetBlank: !value.targetBlank });
|
onChange(index, { ...value, targetBlank: !value.targetBlank });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const listItemStyle = css`
|
||||||
<div
|
margin-bottom: ${theme.spacing.sm};
|
||||||
className={cx(
|
`;
|
||||||
'gf-form gf-form--inline',
|
|
||||||
css`
|
|
||||||
> * {
|
|
||||||
margin-right: ${theme.spacing.xs};
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
label="Title"
|
|
||||||
value={title}
|
|
||||||
onChange={onTitleChange}
|
|
||||||
onBlur={onTitleBlur}
|
|
||||||
inputWidth={15}
|
|
||||||
labelWidth={5}
|
|
||||||
placeholder="Show details"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
const infoTextStyle = css`
|
||||||
|
padding-bottom: ${theme.spacing.md};
|
||||||
|
margin-left: 66px;
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={listItemStyle}>
|
||||||
|
<div className="gf-form gf-form--inline">
|
||||||
|
<FormField
|
||||||
|
className="gf-form--grow"
|
||||||
|
label="Title"
|
||||||
|
value={title}
|
||||||
|
onChange={onTitleChange}
|
||||||
|
onBlur={onTitleBlur}
|
||||||
|
inputWidth={0}
|
||||||
|
labelWidth={5}
|
||||||
|
placeholder="Show details"
|
||||||
|
/>
|
||||||
|
<Switch label="Open in new tab" checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
|
||||||
|
<button className="gf-form-label gf-form-label--btn" onClick={onRemoveClick} title="Remove link">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
label="URL"
|
label="URL"
|
||||||
labelWidth={4}
|
labelWidth={5}
|
||||||
inputEl={<DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />}
|
inputEl={<DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />}
|
||||||
className={css`
|
className={css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
{isLast && (
|
||||||
<Switch label="Open in new tab" checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
|
<div className={infoTextStyle}>
|
||||||
|
With data links you can reference data variables like series name, labels and values. Type CMD+Space,
|
||||||
<div className="gf-form">
|
CTRL+Space, or $ to open variable suggestions.
|
||||||
<button className="gf-form-label gf-form-label--btn" onClick={onRemoveClick}>
|
</div>
|
||||||
<i className="fa fa-times" />
|
)}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo, useCallback, useContext } from 'react';
|
import React, { useState, useMemo, useCallback, useContext } from 'react';
|
||||||
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
|
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
|
||||||
import { makeValue, ThemeContext } from '../../index';
|
import { makeValue, ThemeContext, DataLinkBuiltInVars } from '../../index';
|
||||||
import { SelectionReference } from './SelectionReference';
|
import { SelectionReference } from './SelectionReference';
|
||||||
import { Portal } from '../index';
|
import { Portal } from '../index';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -77,10 +77,10 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useDebounce(updateUsedSuggestions, 500, [linkUrl]);
|
useDebounce(updateUsedSuggestions, 250, [linkUrl]);
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Backspace') {
|
if (event.key === 'Backspace' || event.key === 'Escape') {
|
||||||
setShowingSuggestions(false);
|
setShowingSuggestions(false);
|
||||||
setSuggestionsIndex(0);
|
setSuggestionsIndex(0);
|
||||||
}
|
}
|
||||||
@ -111,7 +111,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
|||||||
setShowingSuggestions(true);
|
setShowingSuggestions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter' && showingSuggestions) {
|
||||||
// Preventing entering a new line
|
// Preventing entering a new line
|
||||||
// As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289
|
// As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289
|
||||||
return false;
|
return false;
|
||||||
@ -134,7 +134,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
|||||||
|
|
||||||
const change = linkUrl.change();
|
const change = linkUrl.change();
|
||||||
|
|
||||||
if (item.origin === VariableOrigin.BuiltIn) {
|
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
|
||||||
change.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
|
change.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
|
||||||
} else {
|
} else {
|
||||||
change.insertText(`var-${item.value}=$\{${item.value}}`);
|
change.insertText(`var-${item.value}=$\{${item.value}}`);
|
||||||
@ -167,7 +167,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
|||||||
modifiers={{
|
modifiers={{
|
||||||
preventOverflow: { enabled: true, boundariesElement: 'window' },
|
preventOverflow: { enabled: true, boundariesElement: 'window' },
|
||||||
arrow: { enabled: false },
|
arrow: { enabled: false },
|
||||||
offset: { offset: 200 }, // width of the suggestions menu
|
offset: { offset: 250 }, // width of the suggestions menu
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ ref, style, placement }) => {
|
{({ ref, style, placement }) => {
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
|
import _ from 'lodash';
|
||||||
import React, { useRef, useContext, useMemo } from 'react';
|
import React, { useRef, useContext, useMemo } from 'react';
|
||||||
import useClickAway from 'react-use/lib/useClickAway';
|
import useClickAway from 'react-use/lib/useClickAway';
|
||||||
import { List } from '../index';
|
import { List } from '../index';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
export enum VariableOrigin {
|
export enum VariableOrigin {
|
||||||
BuiltIn = 'builtin',
|
Series = 'series',
|
||||||
|
Field = 'field',
|
||||||
|
Value = 'value',
|
||||||
|
BuiltIn = 'built-in',
|
||||||
Template = 'template',
|
Template = 'template',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VariableSuggestion {
|
export interface VariableSuggestion {
|
||||||
value: string;
|
value: string;
|
||||||
|
label: string;
|
||||||
documentation?: string;
|
documentation?: string;
|
||||||
origin: VariableOrigin;
|
origin: VariableOrigin;
|
||||||
}
|
}
|
||||||
@ -71,16 +77,34 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
theme.type
|
theme.type
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const separatorColor = selectThemeVariant(
|
||||||
|
{
|
||||||
|
light: tinycolor(wrapperBg.toString())
|
||||||
|
.darken(10)
|
||||||
|
.toString(),
|
||||||
|
dark: tinycolor(wrapperBg.toString())
|
||||||
|
.lighten(10)
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
theme.type
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
list: css`
|
||||||
|
border-bottom: 1px solid ${separatorColor};
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
background: ${wrapperBg};
|
background: ${wrapperBg};
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 200px;
|
width: 250px;
|
||||||
box-shadow: 0 5px 10px 0 ${wrapperShadow};
|
box-shadow: 0 5px 10px 0 ${wrapperShadow};
|
||||||
`,
|
`,
|
||||||
item: css`
|
item: css`
|
||||||
background: none;
|
background: none;
|
||||||
padding: 4px 8px;
|
padding: 2px 8px;
|
||||||
color: ${itemColor};
|
color: ${itemColor};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -89,9 +113,6 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
`,
|
`,
|
||||||
label: css`
|
label: css`
|
||||||
color: ${theme.colors.textWeak};
|
color: ${theme.colors.textWeak};
|
||||||
font-size: ${theme.typography.size.sm};
|
|
||||||
line-height: ${theme.typography.lineHeight.lg};
|
|
||||||
padding: ${theme.spacing.sm};
|
|
||||||
`,
|
`,
|
||||||
activeItem: css`
|
activeItem: css`
|
||||||
background: ${itemBgActive};
|
background: ${itemBgActive};
|
||||||
@ -101,11 +122,11 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
`,
|
`,
|
||||||
itemValue: css`
|
itemValue: css`
|
||||||
font-family: ${theme.typography.fontFamily.monospace};
|
font-family: ${theme.typography.fontFamily.monospace};
|
||||||
|
font-size: ${theme.typography.size.sm};
|
||||||
`,
|
`,
|
||||||
itemDocs: css`
|
itemDocs: css`
|
||||||
margin-top: ${theme.spacing.xs};
|
margin-top: ${theme.spacing.xs};
|
||||||
color: ${itemDocsColor};
|
color: ${itemDocsColor};
|
||||||
font-size: ${theme.typography.size.sm};
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -119,34 +140,35 @@ export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ sugges
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateSuggestions = useMemo(() => {
|
const groupedSuggestions = useMemo(() => {
|
||||||
return suggestions.filter(suggestion => suggestion.origin === VariableOrigin.Template);
|
return _.groupBy(suggestions, s => s.origin);
|
||||||
}, [suggestions]);
|
|
||||||
|
|
||||||
const builtInSuggestions = useMemo(() => {
|
|
||||||
return suggestions.filter(suggestion => suggestion.origin === VariableOrigin.BuiltIn);
|
|
||||||
}, [suggestions]);
|
}, [suggestions]);
|
||||||
|
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={styles.wrapper}>
|
<div ref={ref} className={styles.wrapper}>
|
||||||
{templateSuggestions.length > 0 && (
|
{Object.keys(groupedSuggestions).map((key, i) => {
|
||||||
<DataLinkSuggestionsList
|
const indexOffset =
|
||||||
{...otherProps}
|
i === 0
|
||||||
suggestions={templateSuggestions}
|
? 0
|
||||||
label="Template variables"
|
: Object.keys(groupedSuggestions).reduce((acc, current, index) => {
|
||||||
activeIndex={otherProps.activeIndex}
|
if (index >= i) {
|
||||||
activeIndexOffset={0}
|
return acc;
|
||||||
/>
|
}
|
||||||
)}
|
return acc + groupedSuggestions[current].length;
|
||||||
{builtInSuggestions.length > 0 && (
|
}, 0);
|
||||||
<DataLinkSuggestionsList
|
|
||||||
{...otherProps}
|
return (
|
||||||
suggestions={builtInSuggestions}
|
<DataLinkSuggestionsList
|
||||||
label="Built-in variables"
|
{...otherProps}
|
||||||
activeIndexOffset={templateSuggestions.length}
|
suggestions={groupedSuggestions[key]}
|
||||||
/>
|
label={`${_.capitalize(key)}`}
|
||||||
)}
|
activeIndex={otherProps.activeIndex}
|
||||||
|
activeIndexOffset={indexOffset}
|
||||||
|
key={key}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -165,8 +187,8 @@ const DataLinkSuggestionsList: React.FC<DataLinkSuggestionsListProps> = React.me
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.label}>{label}</div>
|
|
||||||
<List
|
<List
|
||||||
|
className={styles.list}
|
||||||
items={suggestions}
|
items={suggestions}
|
||||||
renderItem={(item, index) => {
|
renderItem={(item, index) => {
|
||||||
return (
|
return (
|
||||||
@ -175,9 +197,11 @@ const DataLinkSuggestionsList: React.FC<DataLinkSuggestionsListProps> = React.me
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSuggestionSelect(item);
|
onSuggestionSelect(item);
|
||||||
}}
|
}}
|
||||||
|
title={item.documentation}
|
||||||
>
|
>
|
||||||
<div className={styles.itemValue}>{item.value}</div>
|
<span className={styles.itemValue}>
|
||||||
{item.documentation && <div className={styles.itemDocs}>{item.documentation}</div>}
|
<span className={styles.label}>{label}</span> {item.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -19,7 +19,7 @@ interface DataLinksEditorProps {
|
|||||||
|
|
||||||
Prism.languages['links'] = {
|
Prism.languages['links'] = {
|
||||||
builtInVariable: {
|
builtInVariable: {
|
||||||
pattern: /(\${\w+})/,
|
pattern: /(\${\S+?})/,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,6 +57,7 @@ export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, on
|
|||||||
<DataLinkEditor
|
<DataLinkEditor
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
index={index}
|
index={index}
|
||||||
|
isLast={index === value.length - 1}
|
||||||
value={link}
|
value={link}
|
||||||
onChange={onLinkChanged}
|
onChange={onLinkChanged}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
|
@ -2,6 +2,7 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react';
|
|||||||
import { FormLabel } from '../FormLabel/FormLabel';
|
import { FormLabel } from '../FormLabel/FormLabel';
|
||||||
import { PopoverContent } from '../Tooltip/Tooltip';
|
import { PopoverContent } from '../Tooltip/Tooltip';
|
||||||
import { cx } from 'emotion';
|
import { cx } from 'emotion';
|
||||||
|
|
||||||
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
label: string;
|
label: string;
|
||||||
tooltip?: PopoverContent;
|
tooltip?: PopoverContent;
|
||||||
@ -33,7 +34,9 @@ export const FormField: FunctionComponent<Props> = ({
|
|||||||
<FormLabel width={labelWidth} tooltip={tooltip}>
|
<FormLabel width={labelWidth} tooltip={tooltip}>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
{inputEl || <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />}
|
{inputEl || (
|
||||||
|
<input type="text" className={`gf-form-input ${inputWidth ? `width-${inputWidth}` : ''}`} {...inputProps} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -36,10 +36,10 @@ export class AbstractList<T> extends React.PureComponent<AbstractListProps<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { items, renderItem, getItemKey } = this.props;
|
const { items, renderItem, getItemKey, className } = this.props;
|
||||||
const styles = this.getListStyles();
|
const styles = this.getListStyles();
|
||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className={cx(styles.list, className)}>
|
||||||
{items.map((item, i) => {
|
{items.map((item, i) => {
|
||||||
return (
|
return (
|
||||||
<li className={styles.item} key={getItemKey ? getItemKey(item) : i}>
|
<li className={styles.item} key={getItemKey ? getItemKey(item) : i}>
|
||||||
|
@ -70,9 +70,9 @@ export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMi
|
|||||||
<div>
|
<div>
|
||||||
Template Variables:
|
Template Variables:
|
||||||
<br />
|
<br />
|
||||||
{'$' + VAR_SERIES_NAME}
|
{'${' + VAR_SERIES_NAME + '}'}
|
||||||
<br />
|
<br />
|
||||||
{'$' + VAR_FIELD_NAME}
|
{'${' + VAR_FIELD_NAME + '}'}
|
||||||
<br />
|
<br />
|
||||||
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
|
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +35,7 @@ export interface PanelProps<T = any> {
|
|||||||
export interface PanelEditorProps<T = any> {
|
export interface PanelEditorProps<T = any> {
|
||||||
options: T;
|
options: T;
|
||||||
onOptionsChange: (options: T) => void;
|
onOptionsChange: (options: T) => void;
|
||||||
|
data: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelModel<TOptions = any> {
|
export interface PanelModel<TOptions = any> {
|
||||||
|
@ -3,9 +3,17 @@ import { LinkModelSupplier } from '@grafana/data';
|
|||||||
|
|
||||||
export const DataLinkBuiltInVars = {
|
export const DataLinkBuiltInVars = {
|
||||||
keepTime: '__url_time_range',
|
keepTime: '__url_time_range',
|
||||||
|
timeRangeFrom: '__from',
|
||||||
|
timeRangeTo: '__to',
|
||||||
includeVars: '__all_variables',
|
includeVars: '__all_variables',
|
||||||
seriesName: '__series_name',
|
seriesName: '__series.name',
|
||||||
valueTime: '__value_time',
|
fieldName: '__field.name',
|
||||||
|
valueTime: '__value.time',
|
||||||
|
valueNumeric: '__value.numeric',
|
||||||
|
valueText: '__value.text',
|
||||||
|
valueRaw: '__value.raw',
|
||||||
|
// name of the calculation represented by the value
|
||||||
|
valueCalc: '__value.calc',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,10 +18,10 @@ describe('Process simple display values', () => {
|
|||||||
getDisplayProcessor(),
|
getDisplayProcessor(),
|
||||||
|
|
||||||
// Add a simple option that is not used (uses a different base class)
|
// Add a simple option that is not used (uses a different base class)
|
||||||
getDisplayProcessor({ field: { min: 0, max: 100 } }),
|
getDisplayProcessor({ config: { min: 0, max: 100 } }),
|
||||||
|
|
||||||
// Add a simple option that is not used (uses a different base class)
|
// Add a simple option that is not used (uses a different base class)
|
||||||
getDisplayProcessor({ field: { unit: 'locale' } }),
|
getDisplayProcessor({ config: { unit: 'locale' } }),
|
||||||
];
|
];
|
||||||
|
|
||||||
it('support null', () => {
|
it('support null', () => {
|
||||||
@ -102,7 +102,7 @@ describe('Format value', () => {
|
|||||||
it('should return if value isNaN', () => {
|
it('should return if value isNaN', () => {
|
||||||
const valueMappings: ValueMapping[] = [];
|
const valueMappings: ValueMapping[] = [];
|
||||||
const value = 'N/A';
|
const value = 'N/A';
|
||||||
const instance = getDisplayProcessor({ field: { mappings: valueMappings } });
|
const instance = getDisplayProcessor({ config: { mappings: valueMappings } });
|
||||||
|
|
||||||
const result = instance(value);
|
const result = instance(value);
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ describe('Format value', () => {
|
|||||||
const valueMappings: ValueMapping[] = [];
|
const valueMappings: ValueMapping[] = [];
|
||||||
const value = '6';
|
const value = '6';
|
||||||
|
|
||||||
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
|
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||||
|
|
||||||
const result = instance(value);
|
const result = instance(value);
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ describe('Format value', () => {
|
|||||||
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
||||||
];
|
];
|
||||||
const value = '10';
|
const value = '10';
|
||||||
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
|
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||||
|
|
||||||
const result = instance(value);
|
const result = instance(value);
|
||||||
|
|
||||||
@ -135,20 +135,20 @@ describe('Format value', () => {
|
|||||||
|
|
||||||
it('should set auto decimals, 1 significant', () => {
|
it('should set auto decimals, 1 significant', () => {
|
||||||
const value = 3.23;
|
const value = 3.23;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: null } });
|
const instance = getDisplayProcessor({ config: { decimals: null } });
|
||||||
expect(instance(value).text).toEqual('3.2');
|
expect(instance(value).text).toEqual('3.2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set auto decimals, 2 significant', () => {
|
it('should set auto decimals, 2 significant', () => {
|
||||||
const value = 0.0245;
|
const value = 0.0245;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: null } });
|
const instance = getDisplayProcessor({ config: { decimals: null } });
|
||||||
|
|
||||||
expect(instance(value).text).toEqual('0.025');
|
expect(instance(value).text).toEqual('0.025');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use override decimals', () => {
|
it('should use override decimals', () => {
|
||||||
const value = 100030303;
|
const value = 100030303;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: 2, unit: 'bytes' } });
|
const instance = getDisplayProcessor({ config: { decimals: 2, unit: 'bytes' } });
|
||||||
expect(instance(value).text).toEqual('95.40 MiB');
|
expect(instance(value).text).toEqual('95.40 MiB');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ describe('Format value', () => {
|
|||||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
];
|
];
|
||||||
const value = '11';
|
const value = '11';
|
||||||
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
|
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||||
|
|
||||||
expect(instance(value).text).toEqual('1-20');
|
expect(instance(value).text).toEqual('1-20');
|
||||||
});
|
});
|
||||||
@ -169,25 +169,25 @@ describe('Format value', () => {
|
|||||||
|
|
||||||
it('with value 1000 and unit short', () => {
|
it('with value 1000 and unit short', () => {
|
||||||
const value = 1000;
|
const value = 1000;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: null, unit: 'short' } });
|
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||||
expect(instance(value).text).toEqual('1.000 K');
|
expect(instance(value).text).toEqual('1.000 K');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with value 1200 and unit short', () => {
|
it('with value 1200 and unit short', () => {
|
||||||
const value = 1200;
|
const value = 1200;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: null, unit: 'short' } });
|
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||||
expect(instance(value).text).toEqual('1.200 K');
|
expect(instance(value).text).toEqual('1.200 K');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with value 1250 and unit short', () => {
|
it('with value 1250 and unit short', () => {
|
||||||
const value = 1250;
|
const value = 1250;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: null, unit: 'short' } });
|
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||||
expect(instance(value).text).toEqual('1.250 K');
|
expect(instance(value).text).toEqual('1.250 K');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with value 10000000 and unit short', () => {
|
it('with value 10000000 and unit short', () => {
|
||||||
const value = 1000000;
|
const value = 1000000;
|
||||||
const instance = getDisplayProcessor({ field: { decimals: null, unit: 'short' } });
|
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||||
expect(instance(value).text).toEqual('1.000 Mil');
|
expect(instance(value).text).toEqual('1.000 Mil');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,7 @@ import { getColorFromHexRgbOrName } from './namedColorsPalette';
|
|||||||
import { GrafanaTheme, GrafanaThemeType } from '../types/index';
|
import { GrafanaTheme, GrafanaThemeType } from '../types/index';
|
||||||
|
|
||||||
interface DisplayProcessorOptions {
|
interface DisplayProcessorOptions {
|
||||||
field?: FieldConfig;
|
config?: FieldConfig;
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
isUtc?: boolean;
|
isUtc?: boolean;
|
||||||
@ -27,7 +27,7 @@ interface DisplayProcessorOptions {
|
|||||||
|
|
||||||
export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor {
|
export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor {
|
||||||
if (options && !_.isEmpty(options)) {
|
if (options && !_.isEmpty(options)) {
|
||||||
const field = options.field ? options.field : {};
|
const field = options.config ? options.config : {};
|
||||||
const formatFunc = getValueFormat(field.unit || 'none');
|
const formatFunc = getValueFormat(field.unit || 'none');
|
||||||
|
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
@ -17,7 +17,6 @@ import toString from 'lodash/toString';
|
|||||||
import { GrafanaTheme, InterpolateFunction } from '../types/index';
|
import { GrafanaTheme, InterpolateFunction } from '../types/index';
|
||||||
import { getDisplayProcessor } from './displayProcessor';
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
import { getFlotPairs } from './flotPairs';
|
import { getFlotPairs } from './flotPairs';
|
||||||
import { DataLinkBuiltInVars } from '../utils/dataLinks';
|
|
||||||
|
|
||||||
export interface FieldDisplayOptions {
|
export interface FieldDisplayOptions {
|
||||||
values?: boolean; // If true show each row value
|
values?: boolean; // If true show each row value
|
||||||
@ -28,8 +27,8 @@ export interface FieldDisplayOptions {
|
|||||||
override: FieldConfig; // Set these values regardless of the source
|
override: FieldConfig; // Set these values regardless of the source
|
||||||
}
|
}
|
||||||
// TODO: use built in variables, same as for data links?
|
// TODO: use built in variables, same as for data links?
|
||||||
export const VAR_SERIES_NAME = '__series_name';
|
export const VAR_SERIES_NAME = '__series.name';
|
||||||
export const VAR_FIELD_NAME = '__field_name';
|
export const VAR_FIELD_NAME = '__field.name';
|
||||||
export const VAR_CALC = '__calc';
|
export const VAR_CALC = '__calc';
|
||||||
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
|
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ function getTitleTemplate(title: string | undefined, stats: string[], data?: Dat
|
|||||||
parts.push('$' + VAR_CALC);
|
parts.push('$' + VAR_CALC);
|
||||||
}
|
}
|
||||||
if (data.length > 1) {
|
if (data.length > 1) {
|
||||||
parts.push('$' + VAR_SERIES_NAME);
|
parts.push('${' + VAR_SERIES_NAME + '}');
|
||||||
}
|
}
|
||||||
if (fieldCount > 1 || !parts.length) {
|
if (fieldCount > 1 || !parts.length) {
|
||||||
parts.push('$' + VAR_FIELD_NAME);
|
parts.push('$' + VAR_FIELD_NAME);
|
||||||
@ -70,8 +69,8 @@ export interface FieldDisplay {
|
|||||||
|
|
||||||
// Expose to the original values for delayed inspection (DataLinks etc)
|
// Expose to the original values for delayed inspection (DataLinks etc)
|
||||||
view?: DataFrameView;
|
view?: DataFrameView;
|
||||||
column?: number; // The field column index
|
colIndex?: number; // The field column index
|
||||||
row?: number; // only filled in when the value is from a row (ie, not a reduction)
|
rowIndex?: number; // only filled in when the value is from a row (ie, not a reduction)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetFieldDisplayValuesOptions {
|
export interface GetFieldDisplayValuesOptions {
|
||||||
@ -106,7 +105,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedVars[DataLinkBuiltInVars.seriesName] = { text: 'Series', value: series.name };
|
scopedVars['__series'] = { text: 'Series', value: { name: series.name } };
|
||||||
|
|
||||||
const { timeField } = getTimeField(series);
|
const { timeField } = getTimeField(series);
|
||||||
const view = new DataFrameView(series);
|
const view = new DataFrameView(series);
|
||||||
@ -125,15 +124,14 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
name = `Field[${s}]`;
|
name = `Field[${s}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedVars[VAR_FIELD_NAME] = { text: 'Field', value: name };
|
scopedVars['__field'] = { text: 'Field', value: { name } };
|
||||||
|
|
||||||
const display = getDisplayProcessor({
|
const display = getDisplayProcessor({
|
||||||
field: config,
|
config,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = config.title ? config.title : defaultTitle;
|
const title = config.title ? config.title : defaultTitle;
|
||||||
|
|
||||||
// Show all rows
|
// Show all rows
|
||||||
if (fieldOptions.values) {
|
if (fieldOptions.values) {
|
||||||
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||||
@ -158,8 +156,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
field: config,
|
field: config,
|
||||||
display: displayValue,
|
display: displayValue,
|
||||||
view,
|
view,
|
||||||
column: i,
|
colIndex: i,
|
||||||
row: j,
|
rowIndex: j,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (values.length >= limit) {
|
if (values.length >= limit) {
|
||||||
@ -187,12 +185,12 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
const displayValue = display(results[calc]);
|
const displayValue = display(results[calc]);
|
||||||
displayValue.title = replaceVariables(title, scopedVars);
|
displayValue.title = replaceVariables(title, scopedVars);
|
||||||
values.push({
|
values.push({
|
||||||
name,
|
name: calc,
|
||||||
field: config,
|
field: config,
|
||||||
display: displayValue,
|
display: displayValue,
|
||||||
sparkline,
|
sparkline,
|
||||||
view,
|
view,
|
||||||
column: i,
|
colIndex: i,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,7 +246,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel {
|
|||||||
hasUniqueLabels = true;
|
hasUniqueLabels = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeFieldIndex = fieldCache.getFirstFieldOfType(FieldType.time);
|
const timeField = fieldCache.getFirstFieldOfType(FieldType.time);
|
||||||
const stringField = fieldCache.getFirstFieldOfType(FieldType.string);
|
const stringField = fieldCache.getFirstFieldOfType(FieldType.string);
|
||||||
const logLevelField = fieldCache.getFieldByName('level');
|
const logLevelField = fieldCache.getFieldByName('level');
|
||||||
|
|
||||||
@ -256,7 +256,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let j = 0; j < series.length; j++) {
|
for (let j = 0; j < series.length; j++) {
|
||||||
const ts = timeFieldIndex.values.get(j);
|
const ts = timeField.values.get(j);
|
||||||
const time = dateTime(ts);
|
const time = dateTime(ts);
|
||||||
const timeEpochMs = time.valueOf();
|
const timeEpochMs = time.valueOf();
|
||||||
const timeFromNow = time.fromNow();
|
const timeFromNow = time.fromNow();
|
||||||
|
@ -46,7 +46,7 @@ export class KeybindingSrv {
|
|||||||
this.bind('g p', this.goToProfile);
|
this.bind('g p', this.goToProfile);
|
||||||
this.bind('s o', this.openSearch);
|
this.bind('s o', this.openSearch);
|
||||||
this.bind('f', this.openSearch);
|
this.bind('f', this.openSearch);
|
||||||
this.bindGlobal('esc', this.exit);
|
this.bind('esc', this.exit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -191,7 +191,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -315,7 +315,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -426,7 +426,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -521,7 +521,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
|
@ -232,7 +232,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -469,7 +469,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -706,7 +706,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@ -943,7 +943,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 19,
|
"schemaVersion": 20,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
|
@ -18,8 +18,10 @@ import { PanelModel } from '../state';
|
|||||||
import { DashboardModel } from '../state';
|
import { DashboardModel } from '../state';
|
||||||
import { VizPickerSearch } from './VizPickerSearch';
|
import { VizPickerSearch } from './VizPickerSearch';
|
||||||
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
||||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/ui';
|
import { PanelPlugin, PanelPluginMeta, PanelData } from '@grafana/ui';
|
||||||
import { PanelCtrl } from 'app/plugins/sdk';
|
import { PanelCtrl } from 'app/plugins/sdk';
|
||||||
|
import { Unsubscribable } from 'rxjs';
|
||||||
|
import { LoadingState } from '@grafana/data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -36,11 +38,13 @@ interface State {
|
|||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
hasBeenFocused: boolean;
|
hasBeenFocused: boolean;
|
||||||
|
data: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VisualizationTab extends PureComponent<Props, State> {
|
export class VisualizationTab extends PureComponent<Props, State> {
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
angularOptions: AngularComponent;
|
angularOptions: AngularComponent;
|
||||||
|
querySubscription: Unsubscribable;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -50,6 +54,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
hasBeenFocused: false,
|
hasBeenFocused: false,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
|
data: {
|
||||||
|
state: LoadingState.NotStarted,
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,16 +74,28 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.editor) {
|
if (plugin.editor) {
|
||||||
return <plugin.editor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
|
return (
|
||||||
|
<plugin.editor
|
||||||
|
data={this.state.data}
|
||||||
|
options={this.getReactPanelOptions()}
|
||||||
|
onOptionsChange={this.onPanelOptionsChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>Visualization has no options</p>;
|
return <p>Visualization has no options</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { panel } = this.props;
|
||||||
|
const queryRunner = panel.getQueryRunner();
|
||||||
if (this.shouldLoadAngularOptions()) {
|
if (this.shouldLoadAngularOptions()) {
|
||||||
this.loadAngularOptions();
|
this.loadAngularOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.querySubscription = queryRunner.getData().subscribe({
|
||||||
|
next: (data: PanelData) => this.setState({ data }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
@ -128,7 +128,7 @@ describe('DashboardModel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('dashboard schema version should be set to latest', () => {
|
it('dashboard schema version should be set to latest', () => {
|
||||||
expect(model.schemaVersion).toBe(19);
|
expect(model.schemaVersion).toBe(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('graph thresholds should be migrated', () => {
|
it('graph thresholds should be migrated', () => {
|
||||||
@ -441,6 +441,71 @@ describe('DashboardModel', () => {
|
|||||||
expect(model.panels[0].links[3].url).toBe(`/dashboard/db/my-other-dashboard`);
|
expect(model.panels[0].links[3].url).toBe(`/dashboard/db/my-other-dashboard`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when migrating variables', () => {
|
||||||
|
let model: any;
|
||||||
|
beforeEach(() => {
|
||||||
|
model = new DashboardModel({
|
||||||
|
panels: [
|
||||||
|
{
|
||||||
|
//graph panel
|
||||||
|
options: {
|
||||||
|
dataLinks: [
|
||||||
|
{
|
||||||
|
url: 'http://mylink.com?series=${__series_name}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'http://mylink.com?series=${__value_time}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// panel with field options
|
||||||
|
options: {
|
||||||
|
fieldOptions: {
|
||||||
|
defaults: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
url: 'http://mylink.com?series=${__series_name}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'http://mylink.com?series=${__value_time}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: '$__cell_0 * $__field_name * $__series_name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data links', () => {
|
||||||
|
it('should replace __series_name variable with __series.name', () => {
|
||||||
|
expect(model.panels[0].options.dataLinks[0].url).toBe('http://mylink.com?series=${__series.name}');
|
||||||
|
expect(model.panels[1].options.fieldOptions.defaults.links[0].url).toBe(
|
||||||
|
'http://mylink.com?series=${__series.name}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace __value_time variable with __value.time', () => {
|
||||||
|
expect(model.panels[0].options.dataLinks[1].url).toBe('http://mylink.com?series=${__value.time}');
|
||||||
|
expect(model.panels[1].options.fieldOptions.defaults.links[1].url).toBe(
|
||||||
|
'http://mylink.com?series=${__value.time}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('field display', () => {
|
||||||
|
it('should replace __series_name and __field_name variables with new syntax', () => {
|
||||||
|
expect(model.panels[1].options.fieldOptions.defaults.title).toBe(
|
||||||
|
'$__cell_0 * ${__field.name} * ${__series.name}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createRow(options: any, panelDescriptions: any[]) {
|
function createRow(options: any, panelDescriptions: any[]) {
|
||||||
|
@ -33,7 +33,7 @@ export class DashboardMigrator {
|
|||||||
let i, j, k, n;
|
let i, j, k, n;
|
||||||
const oldVersion = this.dashboard.schemaVersion;
|
const oldVersion = this.dashboard.schemaVersion;
|
||||||
const panelUpgrades = [];
|
const panelUpgrades = [];
|
||||||
this.dashboard.schemaVersion = 19;
|
this.dashboard.schemaVersion = 20;
|
||||||
|
|
||||||
if (oldVersion === this.dashboard.schemaVersion) {
|
if (oldVersion === this.dashboard.schemaVersion) {
|
||||||
return;
|
return;
|
||||||
@ -436,6 +436,33 @@ export class DashboardMigrator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 20) {
|
||||||
|
const updateLinks = (link: DataLink) => {
|
||||||
|
return {
|
||||||
|
...link,
|
||||||
|
url: updateVariablesSyntax(link.url),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
panelUpgrades.push((panel: any) => {
|
||||||
|
// For graph panel
|
||||||
|
if (panel.options && panel.options.dataLinks && _.isArray(panel.options.dataLinks)) {
|
||||||
|
panel.options.dataLinks = panel.options.dataLinks.map(updateLinks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For panel with fieldOptions
|
||||||
|
if (panel.options && panel.options.fieldOptions && panel.options.fieldOptions.defaults) {
|
||||||
|
if (panel.options.fieldOptions.defaults.links && _.isArray(panel.options.fieldOptions.defaults.links)) {
|
||||||
|
panel.options.fieldOptions.defaults.links = panel.options.fieldOptions.defaults.links.map(updateLinks);
|
||||||
|
}
|
||||||
|
if (panel.options.fieldOptions.defaults.title) {
|
||||||
|
panel.options.fieldOptions.defaults.title = updateVariablesSyntax(
|
||||||
|
panel.options.fieldOptions.defaults.title
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (panelUpgrades.length === 0) {
|
if (panelUpgrades.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -666,3 +693,26 @@ function upgradePanelLink(link: any): DataLink {
|
|||||||
targetBlank: link.targetBlank,
|
targetBlank: link.targetBlank,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateVariablesSyntax(text: string) {
|
||||||
|
const legacyVariableNamesRegex = /(__series_name)|(\$__series_name)|(__value_time)|(__field_name)|(\$__field_name)/g;
|
||||||
|
|
||||||
|
return text.replace(legacyVariableNamesRegex, (match, seriesName, seriesName1, valueTime, fieldName, fieldName1) => {
|
||||||
|
if (seriesName) {
|
||||||
|
return '__series.name';
|
||||||
|
}
|
||||||
|
if (seriesName1) {
|
||||||
|
return '${__series.name}';
|
||||||
|
}
|
||||||
|
if (valueTime) {
|
||||||
|
return '__value.time';
|
||||||
|
}
|
||||||
|
if (fieldName) {
|
||||||
|
return '__field.name';
|
||||||
|
}
|
||||||
|
if (fieldName1) {
|
||||||
|
return '${__field.name}';
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,8 +1,32 @@
|
|||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { FieldDisplay, DataLinkBuiltInVars } from '@grafana/ui';
|
import { FieldDisplay } from '@grafana/ui';
|
||||||
import { LinkModelSupplier, getTimeField, ScopedVars } from '@grafana/data';
|
import { LinkModelSupplier, getTimeField, Labels, ScopedVars, ScopedVar } from '@grafana/data';
|
||||||
import { getLinkSrv } from './link_srv';
|
import { getLinkSrv } from './link_srv';
|
||||||
|
|
||||||
|
interface SeriesVars {
|
||||||
|
name?: string;
|
||||||
|
labels?: Labels;
|
||||||
|
refId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldVars {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValueVars {
|
||||||
|
raw: any;
|
||||||
|
numeric: number;
|
||||||
|
text: string;
|
||||||
|
time?: number;
|
||||||
|
calc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataLinkScopedVars extends ScopedVars {
|
||||||
|
__series?: ScopedVar<SeriesVars>;
|
||||||
|
__field?: ScopedVar<FieldVars>;
|
||||||
|
__value?: ScopedVar<ValueVars>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link suppliers creates link models based on a link origin
|
* Link suppliers creates link models based on a link origin
|
||||||
*/
|
*/
|
||||||
@ -14,29 +38,53 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
getLinks: (_scopedVars?: any) => {
|
getLinks: (_scopedVars?: any) => {
|
||||||
const scopedVars: ScopedVars = {};
|
const scopedVars: DataLinkScopedVars = {};
|
||||||
// TODO, add values to scopedVars and/or pass objects to event listeners
|
|
||||||
if (value.view) {
|
if (value.view) {
|
||||||
scopedVars[DataLinkBuiltInVars.seriesName] = {
|
const { dataFrame } = value.view;
|
||||||
|
|
||||||
|
scopedVars['__series'] = {
|
||||||
|
value: {
|
||||||
|
name: dataFrame.name,
|
||||||
|
labels: dataFrame.labels,
|
||||||
|
refId: dataFrame.refId,
|
||||||
|
},
|
||||||
text: 'Series',
|
text: 'Series',
|
||||||
value: value.view.dataFrame.name,
|
|
||||||
};
|
};
|
||||||
const field = value.column ? value.view.dataFrame.fields[value.column] : undefined;
|
|
||||||
|
const field = value.colIndex !== undefined ? dataFrame.fields[value.colIndex] : undefined;
|
||||||
if (field) {
|
if (field) {
|
||||||
console.log('Full Field Info:', field);
|
console.log('Full Field Info:', field);
|
||||||
|
scopedVars['__field'] = {
|
||||||
|
value: {
|
||||||
|
name: field.name,
|
||||||
|
},
|
||||||
|
text: 'Field',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (value.row) {
|
|
||||||
const row = value.view.get(value.row);
|
|
||||||
console.log('ROW:', row);
|
|
||||||
const dataFrame = value.view.dataFrame;
|
|
||||||
|
|
||||||
|
if (value.rowIndex) {
|
||||||
const { timeField } = getTimeField(dataFrame);
|
const { timeField } = getTimeField(dataFrame);
|
||||||
if (timeField) {
|
scopedVars['__value'] = {
|
||||||
scopedVars[DataLinkBuiltInVars.valueTime] = {
|
value: {
|
||||||
text: 'Value time',
|
raw: field.values.get(value.rowIndex),
|
||||||
value: timeField.values.get(value.row),
|
numeric: value.display.numeric,
|
||||||
};
|
text: value.display.text,
|
||||||
}
|
time: timeField ? timeField.values.get(value.rowIndex) : undefined,
|
||||||
|
},
|
||||||
|
text: 'Value',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// calculation
|
||||||
|
scopedVars['__value'] = {
|
||||||
|
value: {
|
||||||
|
raw: value.display.numeric,
|
||||||
|
numeric: value.display.numeric,
|
||||||
|
text: value.display.text,
|
||||||
|
calc: value.name,
|
||||||
|
},
|
||||||
|
text: 'Value',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('VALUE', value);
|
console.log('VALUE', value);
|
||||||
|
@ -4,47 +4,118 @@ import templateSrv, { TemplateSrv } from 'app/features/templating/template_srv';
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
||||||
import { VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
|
import { VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
import { DataLink, KeyValue, deprecationWarning, LinkModel, ScopedVars } from '@grafana/data';
|
import { DataLink, KeyValue, deprecationWarning, LinkModel, DataFrame, ScopedVars } from '@grafana/data';
|
||||||
|
|
||||||
|
const timeRangeVars = [
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.keepTime}`,
|
||||||
|
label: 'Time range',
|
||||||
|
documentation: 'Adds current time range',
|
||||||
|
origin: VariableOrigin.BuiltIn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.timeRangeFrom}`,
|
||||||
|
label: 'Time range: from',
|
||||||
|
documentation: "Adds current time range's from value",
|
||||||
|
origin: VariableOrigin.BuiltIn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.timeRangeTo}`,
|
||||||
|
label: 'Time range: to',
|
||||||
|
documentation: "Adds current time range's to value",
|
||||||
|
origin: VariableOrigin.BuiltIn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldVars = [
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.fieldName}`,
|
||||||
|
label: 'Name',
|
||||||
|
documentation: 'Field name of the clicked datapoint (in ms epoch)',
|
||||||
|
origin: VariableOrigin.Field,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const valueVars = [
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.valueNumeric}`,
|
||||||
|
label: 'Numeric',
|
||||||
|
documentation: 'Numeric representation of selected value',
|
||||||
|
origin: VariableOrigin.Value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.valueText}`,
|
||||||
|
label: 'Text',
|
||||||
|
documentation: 'Text representation of selected value',
|
||||||
|
origin: VariableOrigin.Value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.valueRaw}`,
|
||||||
|
label: 'Raw',
|
||||||
|
documentation: 'Raw value',
|
||||||
|
origin: VariableOrigin.Value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const buildLabelPath = (label: string) => {
|
||||||
|
return label.indexOf('.') > -1 ? `["${label}"]` : `.${label}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||||
...templateSrv.variables.map(variable => ({
|
...templateSrv.variables.map(variable => ({
|
||||||
value: variable.name as string,
|
value: variable.name as string,
|
||||||
|
label: variable.name,
|
||||||
origin: VariableOrigin.Template,
|
origin: VariableOrigin.Template,
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
value: `${DataLinkBuiltInVars.includeVars}`,
|
value: `${DataLinkBuiltInVars.includeVars}`,
|
||||||
|
label: 'All variables',
|
||||||
documentation: 'Adds current variables',
|
documentation: 'Adds current variables',
|
||||||
origin: VariableOrigin.BuiltIn,
|
origin: VariableOrigin.Template,
|
||||||
},
|
|
||||||
{
|
|
||||||
value: `${DataLinkBuiltInVars.keepTime}`,
|
|
||||||
documentation: 'Adds current time range',
|
|
||||||
origin: VariableOrigin.BuiltIn,
|
|
||||||
},
|
},
|
||||||
|
...timeRangeVars,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getDataLinksVariableSuggestions = (): VariableSuggestion[] => [
|
const getSeriesVars = (dataFrames: DataFrame[]) => {
|
||||||
...getPanelLinksVariableSuggestions(),
|
const labels = _.flatten(dataFrames.map(df => Object.keys(df.labels || {})));
|
||||||
{
|
|
||||||
value: `${DataLinkBuiltInVars.seriesName}`,
|
return [
|
||||||
documentation: 'Adds series name',
|
{
|
||||||
origin: VariableOrigin.BuiltIn,
|
value: `${DataLinkBuiltInVars.seriesName}`,
|
||||||
},
|
label: 'Name',
|
||||||
{
|
documentation: 'Name of the series',
|
||||||
|
origin: VariableOrigin.Series,
|
||||||
|
},
|
||||||
|
...labels.map(label => ({
|
||||||
|
value: `__series.labels${buildLabelPath(label)}`,
|
||||||
|
label: `labels.${label}`,
|
||||||
|
documentation: `${label} label value`,
|
||||||
|
origin: VariableOrigin.Series,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
export const getDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
|
||||||
|
const seriesVars = getSeriesVars(dataFrames);
|
||||||
|
const valueTimeVar = {
|
||||||
value: `${DataLinkBuiltInVars.valueTime}`,
|
value: `${DataLinkBuiltInVars.valueTime}`,
|
||||||
|
label: 'Time',
|
||||||
documentation: 'Time value of the clicked datapoint (in ms epoch)',
|
documentation: 'Time value of the clicked datapoint (in ms epoch)',
|
||||||
origin: VariableOrigin.BuiltIn,
|
origin: VariableOrigin.Value,
|
||||||
},
|
};
|
||||||
];
|
|
||||||
|
|
||||||
export const getCalculationValueDataLinksVariableSuggestions = (): VariableSuggestion[] => [
|
return [...seriesVars, ...fieldVars, ...valueVars, valueTimeVar, ...getPanelLinksVariableSuggestions()];
|
||||||
...getPanelLinksVariableSuggestions(),
|
};
|
||||||
{
|
|
||||||
value: `${DataLinkBuiltInVars.seriesName}`,
|
export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
|
||||||
documentation: 'Adds series name',
|
const seriesVars = getSeriesVars(dataFrames);
|
||||||
origin: VariableOrigin.BuiltIn,
|
const valueCalcVar = {
|
||||||
},
|
value: `${DataLinkBuiltInVars.valueCalc}`,
|
||||||
];
|
label: 'Calculation name',
|
||||||
|
documentation: 'Name of the calculation the value is a result of',
|
||||||
|
origin: VariableOrigin.Value,
|
||||||
|
};
|
||||||
|
return [...seriesVars, ...fieldVars, ...valueVars, valueCalcVar, ...getPanelLinksVariableSuggestions()];
|
||||||
|
};
|
||||||
|
|
||||||
export interface LinkService {
|
export interface LinkService {
|
||||||
getDataLinkUIModel: <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => LinkModel<T>;
|
getDataLinkUIModel: <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => LinkModel<T>;
|
||||||
@ -83,16 +154,15 @@ export class LinkSrv implements LinkService {
|
|||||||
const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl());
|
const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl());
|
||||||
|
|
||||||
const info: LinkModel<T> = {
|
const info: LinkModel<T> = {
|
||||||
href: link.url,
|
href: link.url.replace(/\s|\n/g, ''),
|
||||||
title: this.templateSrv.replace(link.title || '', scopedVars),
|
title: this.templateSrv.replace(link.title || '', scopedVars),
|
||||||
target: link.targetBlank ? '_blank' : '_self',
|
target: link.targetBlank ? '_blank' : '_self',
|
||||||
origin,
|
origin,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
|
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
|
||||||
|
|
||||||
const variablesQuery = toUrlParams(params);
|
const variablesQuery = toUrlParams(params);
|
||||||
info.href = this.templateSrv.replace(link.url, {
|
info.href = this.templateSrv.replace(info.href, {
|
||||||
...scopedVars,
|
...scopedVars,
|
||||||
[DataLinkBuiltInVars.keepTime]: {
|
[DataLinkBuiltInVars.keepTime]: {
|
||||||
text: timeRangeUrl,
|
text: timeRangeUrl,
|
||||||
|
@ -105,11 +105,13 @@ describe('linkSrv', () => {
|
|||||||
linkSrv.getDataLinkUIModel(
|
linkSrv.getDataLinkUIModel(
|
||||||
{
|
{
|
||||||
title: 'Any title',
|
title: 'Any title',
|
||||||
url: `/d/1?var-test=$${DataLinkBuiltInVars.seriesName}`,
|
url: `/d/1?var-test=$\{${DataLinkBuiltInVars.seriesName}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[DataLinkBuiltInVars.seriesName]: {
|
__series: {
|
||||||
value: 'A-series',
|
value: {
|
||||||
|
name: 'A-series',
|
||||||
|
},
|
||||||
text: 'A-series',
|
text: 'A-series',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -122,12 +124,12 @@ describe('linkSrv', () => {
|
|||||||
linkSrv.getDataLinkUIModel(
|
linkSrv.getDataLinkUIModel(
|
||||||
{
|
{
|
||||||
title: 'Any title',
|
title: 'Any title',
|
||||||
url: `/d/1?time=$${DataLinkBuiltInVars.valueTime}`,
|
url: `/d/1?time=$\{${DataLinkBuiltInVars.valueTime}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[DataLinkBuiltInVars.valueTime]: {
|
__value: {
|
||||||
value: dataPointMock.datapoint[0],
|
value: { time: dataPointMock.datapoint[0] },
|
||||||
text: dataPointMock.datapoint[0],
|
text: 'Value',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
|
@ -24,6 +24,27 @@ describe('templateSrv', () => {
|
|||||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scoped vars should support objects', () => {
|
||||||
|
const target = _templateSrv.replace('${series.name} ${series.nested.field}', {
|
||||||
|
series: { value: { name: 'Server1', nested: { field: 'nested' } } },
|
||||||
|
});
|
||||||
|
expect(target).toBe('Server1 nested');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scoped vars should support objects with propert names with dot', () => {
|
||||||
|
const target = _templateSrv.replace('${series.name} ${series.nested["field.with.dot"]}', {
|
||||||
|
series: { value: { name: 'Server1', nested: { 'field.with.dot': 'nested' } } },
|
||||||
|
});
|
||||||
|
expect(target).toBe('Server1 nested');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scoped vars should support arrays of objects', () => {
|
||||||
|
const target = _templateSrv.replace('${series.rows[0].name} ${series.rows[1].name}', {
|
||||||
|
series: { value: { rows: [{ name: 'first' }, { name: 'second' }] } },
|
||||||
|
});
|
||||||
|
expect(target).toBe('first second');
|
||||||
|
});
|
||||||
|
|
||||||
it('should replace $test with scoped value', () => {
|
it('should replace $test with scoped value', () => {
|
||||||
const target = _templateSrv.replace('this.$test.filters', {
|
const target = _templateSrv.replace('this.$test.filters', {
|
||||||
test: { value: 'mupp', text: 'asd' },
|
test: { value: 'mupp', text: 'asd' },
|
||||||
|
@ -7,6 +7,10 @@ function luceneEscape(value: string) {
|
|||||||
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
|
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FieldAccessorCache {
|
||||||
|
[key: string]: (obj: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
export class TemplateSrv {
|
export class TemplateSrv {
|
||||||
variables: any[];
|
variables: any[];
|
||||||
|
|
||||||
@ -15,6 +19,7 @@ export class TemplateSrv {
|
|||||||
private grafanaVariables: any = {};
|
private grafanaVariables: any = {};
|
||||||
private builtIns: any = {};
|
private builtIns: any = {};
|
||||||
private timeRange: TimeRange = null;
|
private timeRange: TimeRange = null;
|
||||||
|
private fieldAccessorCache: FieldAccessorCache = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.builtIns['__interval'] = { text: '1s', value: '1s' };
|
this.builtIns['__interval'] = { text: '1s', value: '1s' };
|
||||||
@ -224,21 +229,44 @@ export class TemplateSrv {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFieldAccessor(fieldPath: string) {
|
||||||
|
const accessor = this.fieldAccessorCache[fieldPath];
|
||||||
|
if (accessor) {
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.fieldAccessorCache[fieldPath] = _.property(fieldPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariableValue(variableName: string, fieldPath: string | undefined, scopedVars: ScopedVars) {
|
||||||
|
const scopedVar = scopedVars[variableName];
|
||||||
|
if (!scopedVar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldPath) {
|
||||||
|
return this.getFieldAccessor(fieldPath)(scopedVar.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopedVar.value;
|
||||||
|
}
|
||||||
|
|
||||||
replace(target: string, scopedVars?: ScopedVars, format?: string | Function): any {
|
replace(target: string, scopedVars?: ScopedVars, format?: string | Function): any {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
let variable, systemValue, value, fmt;
|
|
||||||
this.regex.lastIndex = 0;
|
this.regex.lastIndex = 0;
|
||||||
|
|
||||||
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fmt3) => {
|
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
||||||
variable = this.index[var1 || var2 || var3];
|
const variableName = var1 || var2 || var3;
|
||||||
fmt = fmt2 || fmt3 || format;
|
const variable = this.index[variableName];
|
||||||
|
const fmt = fmt2 || fmt3 || format;
|
||||||
|
|
||||||
if (scopedVars) {
|
if (scopedVars) {
|
||||||
value = scopedVars[var1 || var2 || var3];
|
const value = this.getVariableValue(variableName, fieldPath, scopedVars);
|
||||||
if (value) {
|
if (value !== null && value !== undefined) {
|
||||||
return this.formatValue(value.value, fmt, variable);
|
return this.formatValue(value, fmt, variable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,12 +274,12 @@ export class TemplateSrv {
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
systemValue = this.grafanaVariables[variable.current.value];
|
const systemValue = this.grafanaVariables[variable.current.value];
|
||||||
if (systemValue) {
|
if (systemValue) {
|
||||||
return this.formatValue(systemValue, fmt, variable);
|
return this.formatValue(systemValue, fmt, variable);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = variable.current.value;
|
let value = variable.current.value;
|
||||||
if (this.isAllValue(value)) {
|
if (this.isAllValue(value)) {
|
||||||
value = this.getAllValue(variable);
|
value = this.getAllValue(variable);
|
||||||
// skip formatting of custom all values
|
// skip formatting of custom all values
|
||||||
|
@ -7,7 +7,7 @@ import { assignModelProperties } from 'app/core/utils/model_utils';
|
|||||||
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
|
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
|
||||||
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
|
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
|
||||||
*/
|
*/
|
||||||
export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g;
|
export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::(\w+))?}/g;
|
||||||
|
|
||||||
// Helper function since lastIndex is not reset
|
// Helper function since lastIndex is not reset
|
||||||
export const variableRegexExec = (variableString: string) => {
|
export const variableRegexExec = (variableString: string) => {
|
||||||
|
@ -68,8 +68,8 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
|||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
|
||||||
const suggestions = fieldOptions.values
|
const suggestions = fieldOptions.values
|
||||||
? getDataLinksVariableSuggestions()
|
? getDataLinksVariableSuggestions(this.props.data.series)
|
||||||
: getCalculationValueDataLinksVariableSuggestions();
|
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
|
||||||
const labelWidth = 6;
|
const labelWidth = 6;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,9 +72,10 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
|||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
|
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
|
||||||
const suggestions = fieldOptions.values
|
const suggestions = fieldOptions.values
|
||||||
? getDataLinksVariableSuggestions()
|
? getDataLinksVariableSuggestions(this.props.data.series)
|
||||||
: getCalculationValueDataLinksVariableSuggestions();
|
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -12,7 +12,7 @@ export interface FlotDataPoint {
|
|||||||
export class GraphContextMenuCtrl {
|
export class GraphContextMenuCtrl {
|
||||||
private source?: FlotDataPoint | null;
|
private source?: FlotDataPoint | null;
|
||||||
private scope?: any;
|
private scope?: any;
|
||||||
menuItems: ContextMenuItem[];
|
menuItemsSupplier?: () => ContextMenuItem[];
|
||||||
scrollContextElement: HTMLElement | null;
|
scrollContextElement: HTMLElement | null;
|
||||||
position: {
|
position: {
|
||||||
x: number;
|
x: number;
|
||||||
@ -23,7 +23,6 @@ export class GraphContextMenuCtrl {
|
|||||||
|
|
||||||
constructor($scope: any) {
|
constructor($scope: any) {
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
this.menuItems = [];
|
|
||||||
this.scope = $scope;
|
this.scope = $scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,11 +69,7 @@ export class GraphContextMenuCtrl {
|
|||||||
return this.source;
|
return this.source;
|
||||||
};
|
};
|
||||||
|
|
||||||
setMenuItems = (items: ContextMenuItem[]) => {
|
setMenuItemsSupplier = (menuItemsSupplier: () => ContextMenuItem[]) => {
|
||||||
this.menuItems = items;
|
this.menuItemsSupplier = menuItemsSupplier;
|
||||||
};
|
|
||||||
|
|
||||||
getMenuItems = () => {
|
|
||||||
return this.menuItems;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import GraphTooltip from './graph_tooltip';
|
|||||||
import { ThresholdManager } from './threshold_manager';
|
import { ThresholdManager } from './threshold_manager';
|
||||||
import { TimeRegionManager } from './time_region_manager';
|
import { TimeRegionManager } from './time_region_manager';
|
||||||
import { EventManager } from 'app/features/annotations/all';
|
import { EventManager } from 'app/features/annotations/all';
|
||||||
import { LinkService, LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
|
||||||
import { convertToHistogramData } from './histogram';
|
import { convertToHistogramData } from './histogram';
|
||||||
import { alignYLevel } from './align_yaxes';
|
import { alignYLevel } from './align_yaxes';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
@ -25,12 +24,13 @@ import ReactDOM from 'react-dom';
|
|||||||
import { GraphLegendProps, Legend } from './Legend/Legend';
|
import { GraphLegendProps, Legend } from './Legend/Legend';
|
||||||
|
|
||||||
import { GraphCtrl } from './module';
|
import { GraphCtrl } from './module';
|
||||||
import { getValueFormat, ContextMenuItem, ContextMenuGroup, DataLinkBuiltInVars } from '@grafana/ui';
|
import { getValueFormat, ContextMenuGroup, FieldDisplay, ContextMenuItem, getDisplayProcessor } from '@grafana/ui';
|
||||||
import { provideTheme } from 'app/core/utils/ConfigProvider';
|
import { provideTheme, getCurrentTheme } from 'app/core/utils/ConfigProvider';
|
||||||
import { DataLink, toUtc } from '@grafana/data';
|
import { toUtc, LinkModelSupplier, DataFrameView } from '@grafana/data';
|
||||||
import { GraphContextMenuCtrl, FlotDataPoint } from './GraphContextMenuCtrl';
|
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { ContextSrv } from 'app/core/services/context_srv';
|
import { ContextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
|
|
||||||
const LegendWithThemeProvider = provideTheme(Legend);
|
const LegendWithThemeProvider = provideTheme(Legend);
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class GraphElement {
|
|||||||
timeRegionManager: TimeRegionManager;
|
timeRegionManager: TimeRegionManager;
|
||||||
legendElem: HTMLElement;
|
legendElem: HTMLElement;
|
||||||
|
|
||||||
constructor(private scope: any, private elem: JQuery, private timeSrv: TimeSrv, private linkSrv: LinkService) {
|
constructor(private scope: any, private elem: JQuery, private timeSrv: TimeSrv) {
|
||||||
this.ctrl = scope.ctrl;
|
this.ctrl = scope.ctrl;
|
||||||
this.contextMenu = scope.ctrl.contextMenuCtrl;
|
this.contextMenu = scope.ctrl.contextMenuCtrl;
|
||||||
this.dashboard = this.ctrl.dashboard;
|
this.dashboard = this.ctrl.dashboard;
|
||||||
@ -175,53 +175,48 @@ class GraphElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getContextMenuItems = (flotPosition: { x: number; y: number }, item?: FlotDataPoint): ContextMenuGroup[] => {
|
getContextMenuItemsSupplier = (
|
||||||
const dataLinks: DataLink[] = this.panel.options.dataLinks || [];
|
flotPosition: { x: number; y: number },
|
||||||
|
linksSupplier?: LinkModelSupplier<FieldDisplay>
|
||||||
|
): (() => ContextMenuGroup[]) => {
|
||||||
|
return () => {
|
||||||
|
// Fixed context menu items
|
||||||
|
const items: ContextMenuGroup[] = [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Add annotation',
|
||||||
|
icon: 'gicon gicon-annotation',
|
||||||
|
onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const items: ContextMenuGroup[] = [
|
if (!linksSupplier) {
|
||||||
{
|
return items;
|
||||||
items: [
|
}
|
||||||
{
|
|
||||||
label: 'Add annotation',
|
|
||||||
icon: 'gicon gicon-annotation',
|
|
||||||
onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return item
|
const dataLinks = [
|
||||||
? [
|
{
|
||||||
...items,
|
items: linksSupplier.getLinks(this.panel.scopedVars).map<ContextMenuItem>(link => {
|
||||||
{
|
return {
|
||||||
items: [
|
label: link.title,
|
||||||
...dataLinks.map<ContextMenuItem>(link => {
|
url: link.href,
|
||||||
const linkUiModel = this.linkSrv.getDataLinkUIModel(
|
target: link.target,
|
||||||
link,
|
icon: `fa ${link.target === '_self' ? 'fa-link' : 'fa-external-link'}`,
|
||||||
{
|
};
|
||||||
...this.panel.scopedVars,
|
}),
|
||||||
[DataLinkBuiltInVars.seriesName]: { value: item.series.alias, text: item.series.alias },
|
},
|
||||||
[DataLinkBuiltInVars.valueTime]: { value: item.datapoint[0], text: item.datapoint[0] },
|
];
|
||||||
},
|
|
||||||
item
|
return [...items, ...dataLinks];
|
||||||
);
|
};
|
||||||
return {
|
|
||||||
label: linkUiModel.title,
|
|
||||||
url: linkUiModel.href,
|
|
||||||
target: linkUiModel.target,
|
|
||||||
icon: `fa ${linkUiModel.target === '_self' ? 'fa-link' : 'fa-external-link'}`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: items;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onPlotClick(event: JQueryEventObject, pos: any, item: any) {
|
onPlotClick(event: JQueryEventObject, pos: any, item: any) {
|
||||||
const scrollContextElement = this.elem.closest('.view') ? this.elem.closest('.view').get()[0] : null;
|
const scrollContextElement = this.elem.closest('.view') ? this.elem.closest('.view').get()[0] : null;
|
||||||
const contextMenuSourceItem = item;
|
const contextMenuSourceItem = item;
|
||||||
let contextMenuItems: ContextMenuItem[];
|
|
||||||
|
|
||||||
if (this.panel.xaxis.mode !== 'time') {
|
if (this.panel.xaxis.mode !== 'time') {
|
||||||
// Skip if panel in histogram or series mode
|
// Skip if panel in histogram or series mode
|
||||||
@ -239,12 +234,40 @@ class GraphElement {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.tooltip.clear(this.plot);
|
this.tooltip.clear(this.plot);
|
||||||
contextMenuItems = this.getContextMenuItems(pos, item) as ContextMenuItem[];
|
let linksSupplier: LinkModelSupplier<FieldDisplay>;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
// pickup y-axis index to know which field's config to apply
|
||||||
|
const yAxisConfig = this.panel.yaxes[item.series.yaxis.n === 2 ? 1 : 0];
|
||||||
|
const fieldConfig = {
|
||||||
|
decimals: yAxisConfig.decimals,
|
||||||
|
links: this.panel.options.dataLinks || [],
|
||||||
|
};
|
||||||
|
const dataFrame = this.ctrl.dataList[item.series.dataFrameIndex];
|
||||||
|
const field = dataFrame.fields[item.series.fieldIndex];
|
||||||
|
|
||||||
|
const fieldDisplay = getDisplayProcessor({
|
||||||
|
config: fieldConfig,
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
})(field.values.get(item.dataIndex));
|
||||||
|
|
||||||
|
linksSupplier = this.panel.options.dataLinks
|
||||||
|
? getFieldLinksSupplier({
|
||||||
|
display: fieldDisplay,
|
||||||
|
name: field.name,
|
||||||
|
view: new DataFrameView(dataFrame),
|
||||||
|
rowIndex: item.dataIndex,
|
||||||
|
colIndex: item.series.fieldIndex,
|
||||||
|
field: fieldConfig,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this.scope.$apply(() => {
|
this.scope.$apply(() => {
|
||||||
// Setting nearest CustomScrollbar element as a scroll context for graph context menu
|
// Setting nearest CustomScrollbar element as a scroll context for graph context menu
|
||||||
this.contextMenu.setScrollContextElement(scrollContextElement);
|
this.contextMenu.setScrollContextElement(scrollContextElement);
|
||||||
this.contextMenu.setSource(contextMenuSourceItem);
|
this.contextMenu.setSource(contextMenuSourceItem);
|
||||||
this.contextMenu.setMenuItems(contextMenuItems);
|
this.contextMenu.setMenuItemsSupplier(this.getContextMenuItemsSupplier(pos, linksSupplier) as any);
|
||||||
this.contextMenu.toggleMenu(pos);
|
this.contextMenu.toggleMenu(pos);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -363,7 +386,6 @@ class GraphElement {
|
|||||||
this.thresholdManager.addFlotOptions(options, this.panel);
|
this.thresholdManager.addFlotOptions(options, this.panel);
|
||||||
this.timeRegionManager.addFlotOptions(options, this.panel);
|
this.timeRegionManager.addFlotOptions(options, this.panel);
|
||||||
this.eventManager.addFlotEvents(this.annotations, options);
|
this.eventManager.addFlotEvents(this.annotations, options);
|
||||||
|
|
||||||
this.sortedSeries = this.sortSeries(this.data, this.panel);
|
this.sortedSeries = this.sortSeries(this.data, this.panel);
|
||||||
this.callPlot(options, true);
|
this.callPlot(options, true);
|
||||||
}
|
}
|
||||||
@ -855,12 +877,12 @@ class GraphElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
function graphDirective(timeSrv: TimeSrv, popoverSrv: any, contextSrv: ContextSrv, linkSrv: LinkSrv) {
|
function graphDirective(timeSrv: TimeSrv, popoverSrv: any, contextSrv: ContextSrv) {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
template: '',
|
template: '',
|
||||||
link: (scope: any, elem: JQuery) => {
|
link: (scope: any, elem: JQuery) => {
|
||||||
return new GraphElement(scope, elem, timeSrv, linkSrv);
|
return new GraphElement(scope, elem, timeSrv);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
subTabIndex: number;
|
subTabIndex: number;
|
||||||
processor: DataProcessor;
|
processor: DataProcessor;
|
||||||
contextMenuCtrl: GraphContextMenuCtrl;
|
contextMenuCtrl: GraphContextMenuCtrl;
|
||||||
linkVariableSuggestions: VariableSuggestion[] = getDataLinksVariableSuggestions();
|
linkVariableSuggestions: VariableSuggestion[] = [];
|
||||||
|
|
||||||
panelDefaults: any = {
|
panelDefaults: any = {
|
||||||
// datasource name, null = default datasource
|
// datasource name, null = default datasource
|
||||||
@ -216,6 +216,8 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
range: this.range,
|
range: this.range,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.linkVariableSuggestions = getDataLinksVariableSuggestions(data);
|
||||||
|
|
||||||
this.dataWarning = null;
|
this.dataWarning = null;
|
||||||
const datapointsCount = this.seriesList.reduce((prev, series) => {
|
const datapointsCount = this.seriesList.reduce((prev, series) => {
|
||||||
return prev + series.datapoints.length;
|
return prev + series.datapoints.length;
|
||||||
@ -337,6 +339,10 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
formatDate = (date: DateTimeInput, format?: string) => {
|
formatDate = (date: DateTimeInput, format?: string) => {
|
||||||
return this.dashboard.formatDate.apply(this.dashboard, [date, format]);
|
return this.dashboard.formatDate.apply(this.dashboard, [date, format]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getDataFrameByRefId = (refId: string) => {
|
||||||
|
return this.dataList.filter(dataFrame => dataFrame.refId === refId)[0];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GraphCtrl, GraphCtrl as PanelCtrl };
|
export { GraphCtrl, GraphCtrl as PanelCtrl };
|
||||||
|
@ -121,7 +121,7 @@ describe('grafanaGraph', () => {
|
|||||||
$.plot = ctrl.plot = jest.fn();
|
$.plot = ctrl.plot = jest.fn();
|
||||||
scope.ctrl = ctrl;
|
scope.ctrl = ctrl;
|
||||||
|
|
||||||
link = graphDirective({} as any, {}, {} as any, {} as any).link(scope, {
|
link = graphDirective({} as any, {}, {} as any).link(scope, {
|
||||||
width: () => 500,
|
width: () => 500,
|
||||||
mouseleave: () => {},
|
mouseleave: () => {},
|
||||||
bind: () => {},
|
bind: () => {},
|
||||||
|
@ -8,7 +8,7 @@ const template = `
|
|||||||
</div>
|
</div>
|
||||||
<div ng-if="ctrl.contextMenuCtrl.isVisible">
|
<div ng-if="ctrl.contextMenuCtrl.isVisible">
|
||||||
<graph-context-menu
|
<graph-context-menu
|
||||||
items="ctrl.contextMenuCtrl.menuItems"
|
items="ctrl.contextMenuCtrl.menuItemsSupplier()"
|
||||||
onClose="ctrl.onContextMenuClose"
|
onClose="ctrl.onContextMenuClose"
|
||||||
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
|
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
|
||||||
formatSourceDate="ctrl.formatDate"
|
formatSourceDate="ctrl.formatDate"
|
||||||
|
@ -21,7 +21,7 @@ export const getGraphSeriesModel = (
|
|||||||
const graphs: GraphSeriesXY[] = [];
|
const graphs: GraphSeriesXY[] = [];
|
||||||
|
|
||||||
const displayProcessor = getDisplayProcessor({
|
const displayProcessor = getDisplayProcessor({
|
||||||
field: {
|
config: {
|
||||||
decimals: legendOptions.decimals,
|
decimals: legendOptions.decimals,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onOptionsChange, options } = this.props;
|
const { onOptionsChange, options, data } = this.props;
|
||||||
const { fieldOptions } = options;
|
const { fieldOptions } = options;
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
|
|||||||
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
|
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
|
||||||
</PanelOptionsGroup>
|
</PanelOptionsGroup>
|
||||||
|
|
||||||
<PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
|
<PieChartOptionsBox data={data} onOptionsChange={onOptionsChange} options={options} />
|
||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
|
||||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||||
|
@ -238,7 +238,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const processor = getDisplayProcessor({
|
const processor = getDisplayProcessor({
|
||||||
field: {
|
config: {
|
||||||
...fieldInfo.field.config,
|
...fieldInfo.field.config,
|
||||||
unit: panel.format,
|
unit: panel.format,
|
||||||
decimals: panel.decimals,
|
decimals: panel.decimals,
|
||||||
|
@ -70,8 +70,8 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
|||||||
const { fieldOptions } = options;
|
const { fieldOptions } = options;
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
const suggestions = fieldOptions.values
|
const suggestions = fieldOptions.values
|
||||||
? getDataLinksVariableSuggestions()
|
? getDataLinksVariableSuggestions(this.props.data.series)
|
||||||
: getCalculationValueDataLinksVariableSuggestions();
|
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
Loading…
Reference in New Issue
Block a user