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+.
|
||||
|
||||
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" >}}
|
||||
|
||||
@ -208,14 +208,40 @@ available suggestions:
|
||||
{{< 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
|
||||
2. ``__url_time_range`` - will add current dashboard's time range to the URL (i.e. ``?from=now-6h&to=now``)
|
||||
3. ``__series_name`` - will add series name as a query param in the URL (i.e. ``?series=B-series``)
|
||||
4. ``__value_time`` - will add datapoint's timestamp (Unix ms epoch) to the URL (i.e. ``?time=1560268814105``)
|
||||
``__url_time_range`` - current dashboard's time range (i.e. ``?from=now-6h&to=now``)
|
||||
``__from`` - current dashboard's time range from value
|
||||
``__to`` - current dashboard's time range to value
|
||||
|
||||
#### 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)
|
||||
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,
|
||||
}));
|
||||
|
||||
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
|
||||
if (field.type === FieldType.other) {
|
||||
const t = guessFieldTypeForField(field);
|
||||
@ -33,13 +34,13 @@ export class FieldCache {
|
||||
}
|
||||
this.fieldByType[field.type].push({
|
||||
...field,
|
||||
index,
|
||||
index: i,
|
||||
});
|
||||
|
||||
if (this.fieldByName[field.name]) {
|
||||
console.warn('Duplicate field names in DataFrame: ', field.name);
|
||||
} 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 { FormField, Switch } from '../index';
|
||||
import { VariableSuggestion } from './DataLinkSuggestions';
|
||||
import { css, cx } from 'emotion';
|
||||
import { css } from 'emotion';
|
||||
import { ThemeContext } from '../../themes/index';
|
||||
import { DataLinkInput } from './DataLinkInput';
|
||||
|
||||
interface DataLinkEditorProps {
|
||||
index: number;
|
||||
isLast: boolean;
|
||||
value: DataLink;
|
||||
suggestions: VariableSuggestion[];
|
||||
onChange: (index: number, link: DataLink) => void;
|
||||
@ -15,7 +16,7 @@ interface DataLinkEditorProps {
|
||||
}
|
||||
|
||||
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
||||
({ index, value, onChange, onRemove, suggestions }) => {
|
||||
({ index, value, onChange, onRemove, suggestions, isLast }) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const [title, setTitle] = useState(value.title);
|
||||
|
||||
@ -38,46 +39,48 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
||||
onChange(index, { ...value, targetBlank: !value.targetBlank });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
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 listItemStyle = css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`;
|
||||
|
||||
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
|
||||
label="URL"
|
||||
labelWidth={4}
|
||||
labelWidth={5}
|
||||
inputEl={<DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />}
|
||||
className={css`
|
||||
width: 100%;
|
||||
`}
|
||||
/>
|
||||
|
||||
<Switch label="Open in new tab" checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
|
||||
|
||||
<div className="gf-form">
|
||||
<button className="gf-form-label gf-form-label--btn" onClick={onRemoveClick}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
{isLast && (
|
||||
<div className={infoTextStyle}>
|
||||
With data links you can reference data variables like series name, labels and values. Type CMD+Space,
|
||||
CTRL+Space, or $ to open variable suggestions.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo, useCallback, useContext } from 'react';
|
||||
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
|
||||
import { makeValue, ThemeContext } from '../../index';
|
||||
import { makeValue, ThemeContext, DataLinkBuiltInVars } from '../../index';
|
||||
import { SelectionReference } from './SelectionReference';
|
||||
import { Portal } from '../index';
|
||||
// @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) => {
|
||||
if (event.key === 'Backspace') {
|
||||
if (event.key === 'Backspace' || event.key === 'Escape') {
|
||||
setShowingSuggestions(false);
|
||||
setSuggestionsIndex(0);
|
||||
}
|
||||
@ -111,7 +111,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
||||
setShowingSuggestions(true);
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === 'Enter' && showingSuggestions) {
|
||||
// Preventing entering a new line
|
||||
// As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289
|
||||
return false;
|
||||
@ -134,7 +134,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
||||
|
||||
const change = linkUrl.change();
|
||||
|
||||
if (item.origin === VariableOrigin.BuiltIn) {
|
||||
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
|
||||
change.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
|
||||
} else {
|
||||
change.insertText(`var-${item.value}=$\{${item.value}}`);
|
||||
@ -167,7 +167,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: true, boundariesElement: 'window' },
|
||||
arrow: { enabled: false },
|
||||
offset: { offset: 200 }, // width of the suggestions menu
|
||||
offset: { offset: 250 }, // width of the suggestions menu
|
||||
}}
|
||||
>
|
||||
{({ ref, style, placement }) => {
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||
import { css, cx } from 'emotion';
|
||||
import _ from 'lodash';
|
||||
import React, { useRef, useContext, useMemo } from 'react';
|
||||
import useClickAway from 'react-use/lib/useClickAway';
|
||||
import { List } from '../index';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export enum VariableOrigin {
|
||||
BuiltIn = 'builtin',
|
||||
Series = 'series',
|
||||
Field = 'field',
|
||||
Value = 'value',
|
||||
BuiltIn = 'built-in',
|
||||
Template = 'template',
|
||||
}
|
||||
|
||||
export interface VariableSuggestion {
|
||||
value: string;
|
||||
label: string;
|
||||
documentation?: string;
|
||||
origin: VariableOrigin;
|
||||
}
|
||||
@ -71,16 +77,34 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
theme.type
|
||||
);
|
||||
|
||||
const separatorColor = selectThemeVariant(
|
||||
{
|
||||
light: tinycolor(wrapperBg.toString())
|
||||
.darken(10)
|
||||
.toString(),
|
||||
dark: tinycolor(wrapperBg.toString())
|
||||
.lighten(10)
|
||||
.toString(),
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
list: css`
|
||||
border-bottom: 1px solid ${separatorColor};
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
`,
|
||||
wrapper: css`
|
||||
background: ${wrapperBg};
|
||||
z-index: 1;
|
||||
width: 200px;
|
||||
width: 250px;
|
||||
box-shadow: 0 5px 10px 0 ${wrapperShadow};
|
||||
`,
|
||||
item: css`
|
||||
background: none;
|
||||
padding: 4px 8px;
|
||||
padding: 2px 8px;
|
||||
color: ${itemColor};
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
@ -89,9 +113,6 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
`,
|
||||
label: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
line-height: ${theme.typography.lineHeight.lg};
|
||||
padding: ${theme.spacing.sm};
|
||||
`,
|
||||
activeItem: css`
|
||||
background: ${itemBgActive};
|
||||
@ -101,11 +122,11 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
`,
|
||||
itemValue: css`
|
||||
font-family: ${theme.typography.fontFamily.monospace};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
itemDocs: css`
|
||||
margin-top: ${theme.spacing.xs};
|
||||
color: ${itemDocsColor};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
};
|
||||
};
|
||||
@ -119,34 +140,35 @@ export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ sugges
|
||||
}
|
||||
});
|
||||
|
||||
const templateSuggestions = useMemo(() => {
|
||||
return suggestions.filter(suggestion => suggestion.origin === VariableOrigin.Template);
|
||||
}, [suggestions]);
|
||||
|
||||
const builtInSuggestions = useMemo(() => {
|
||||
return suggestions.filter(suggestion => suggestion.origin === VariableOrigin.BuiltIn);
|
||||
const groupedSuggestions = useMemo(() => {
|
||||
return _.groupBy(suggestions, s => s.origin);
|
||||
}, [suggestions]);
|
||||
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<div ref={ref} className={styles.wrapper}>
|
||||
{templateSuggestions.length > 0 && (
|
||||
<DataLinkSuggestionsList
|
||||
{...otherProps}
|
||||
suggestions={templateSuggestions}
|
||||
label="Template variables"
|
||||
activeIndex={otherProps.activeIndex}
|
||||
activeIndexOffset={0}
|
||||
/>
|
||||
)}
|
||||
{builtInSuggestions.length > 0 && (
|
||||
<DataLinkSuggestionsList
|
||||
{...otherProps}
|
||||
suggestions={builtInSuggestions}
|
||||
label="Built-in variables"
|
||||
activeIndexOffset={templateSuggestions.length}
|
||||
/>
|
||||
)}
|
||||
{Object.keys(groupedSuggestions).map((key, i) => {
|
||||
const indexOffset =
|
||||
i === 0
|
||||
? 0
|
||||
: Object.keys(groupedSuggestions).reduce((acc, current, index) => {
|
||||
if (index >= i) {
|
||||
return acc;
|
||||
}
|
||||
return acc + groupedSuggestions[current].length;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<DataLinkSuggestionsList
|
||||
{...otherProps}
|
||||
suggestions={groupedSuggestions[key]}
|
||||
label={`${_.capitalize(key)}`}
|
||||
activeIndex={otherProps.activeIndex}
|
||||
activeIndexOffset={indexOffset}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -165,8 +187,8 @@ const DataLinkSuggestionsList: React.FC<DataLinkSuggestionsListProps> = React.me
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<List
|
||||
className={styles.list}
|
||||
items={suggestions}
|
||||
renderItem={(item, index) => {
|
||||
return (
|
||||
@ -175,9 +197,11 @@ const DataLinkSuggestionsList: React.FC<DataLinkSuggestionsListProps> = React.me
|
||||
onClick={() => {
|
||||
onSuggestionSelect(item);
|
||||
}}
|
||||
title={item.documentation}
|
||||
>
|
||||
<div className={styles.itemValue}>{item.value}</div>
|
||||
{item.documentation && <div className={styles.itemDocs}>{item.documentation}</div>}
|
||||
<span className={styles.itemValue}>
|
||||
<span className={styles.label}>{label}</span> {item.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -19,7 +19,7 @@ interface DataLinksEditorProps {
|
||||
|
||||
Prism.languages['links'] = {
|
||||
builtInVariable: {
|
||||
pattern: /(\${\w+})/,
|
||||
pattern: /(\${\S+?})/,
|
||||
},
|
||||
};
|
||||
|
||||
@ -57,6 +57,7 @@ export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, on
|
||||
<DataLinkEditor
|
||||
key={index.toString()}
|
||||
index={index}
|
||||
isLast={index === value.length - 1}
|
||||
value={link}
|
||||
onChange={onLinkChanged}
|
||||
onRemove={onRemove}
|
||||
|
@ -2,6 +2,7 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react';
|
||||
import { FormLabel } from '../FormLabel/FormLabel';
|
||||
import { PopoverContent } from '../Tooltip/Tooltip';
|
||||
import { cx } from 'emotion';
|
||||
|
||||
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
tooltip?: PopoverContent;
|
||||
@ -33,7 +34,9 @@ export const FormField: FunctionComponent<Props> = ({
|
||||
<FormLabel width={labelWidth} tooltip={tooltip}>
|
||||
{label}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -36,10 +36,10 @@ export class AbstractList<T> extends React.PureComponent<AbstractListProps<T>> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, renderItem, getItemKey } = this.props;
|
||||
const { items, renderItem, getItemKey, className } = this.props;
|
||||
const styles = this.getListStyles();
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
<ul className={cx(styles.list, className)}>
|
||||
{items.map((item, i) => {
|
||||
return (
|
||||
<li className={styles.item} key={getItemKey ? getItemKey(item) : i}>
|
||||
|
@ -70,9 +70,9 @@ export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMi
|
||||
<div>
|
||||
Template Variables:
|
||||
<br />
|
||||
{'$' + VAR_SERIES_NAME}
|
||||
{'${' + VAR_SERIES_NAME + '}'}
|
||||
<br />
|
||||
{'$' + VAR_FIELD_NAME}
|
||||
{'${' + VAR_FIELD_NAME + '}'}
|
||||
<br />
|
||||
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
|
||||
</div>
|
||||
|
@ -35,6 +35,7 @@ export interface PanelProps<T = any> {
|
||||
export interface PanelEditorProps<T = any> {
|
||||
options: T;
|
||||
onOptionsChange: (options: T) => void;
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
export interface PanelModel<TOptions = any> {
|
||||
|
@ -3,9 +3,17 @@ import { LinkModelSupplier } from '@grafana/data';
|
||||
|
||||
export const DataLinkBuiltInVars = {
|
||||
keepTime: '__url_time_range',
|
||||
timeRangeFrom: '__from',
|
||||
timeRangeTo: '__to',
|
||||
includeVars: '__all_variables',
|
||||
seriesName: '__series_name',
|
||||
valueTime: '__value_time',
|
||||
seriesName: '__series.name',
|
||||
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(),
|
||||
|
||||
// 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)
|
||||
getDisplayProcessor({ field: { unit: 'locale' } }),
|
||||
getDisplayProcessor({ config: { unit: 'locale' } }),
|
||||
];
|
||||
|
||||
it('support null', () => {
|
||||
@ -102,7 +102,7 @@ describe('Format value', () => {
|
||||
it('should return if value isNaN', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = 'N/A';
|
||||
const instance = getDisplayProcessor({ field: { mappings: valueMappings } });
|
||||
const instance = getDisplayProcessor({ config: { mappings: valueMappings } });
|
||||
|
||||
const result = instance(value);
|
||||
|
||||
@ -113,7 +113,7 @@ describe('Format value', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = '6';
|
||||
|
||||
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
|
||||
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||
|
||||
const result = instance(value);
|
||||
|
||||
@ -126,7 +126,7 @@ describe('Format value', () => {
|
||||
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
||||
];
|
||||
const value = '10';
|
||||
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
|
||||
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||
|
||||
const result = instance(value);
|
||||
|
||||
@ -135,20 +135,20 @@ describe('Format value', () => {
|
||||
|
||||
it('should set auto decimals, 1 significant', () => {
|
||||
const value = 3.23;
|
||||
const instance = getDisplayProcessor({ field: { decimals: null } });
|
||||
const instance = getDisplayProcessor({ config: { decimals: null } });
|
||||
expect(instance(value).text).toEqual('3.2');
|
||||
});
|
||||
|
||||
it('should set auto decimals, 2 significant', () => {
|
||||
const value = 0.0245;
|
||||
const instance = getDisplayProcessor({ field: { decimals: null } });
|
||||
const instance = getDisplayProcessor({ config: { decimals: null } });
|
||||
|
||||
expect(instance(value).text).toEqual('0.025');
|
||||
});
|
||||
|
||||
it('should use override decimals', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
@ -158,7 +158,7 @@ describe('Format value', () => {
|
||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, 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');
|
||||
});
|
||||
@ -169,25 +169,25 @@ describe('Format value', () => {
|
||||
|
||||
it('with value 1000 and unit short', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('with value 1200 and unit short', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('with value 1250 and unit short', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('with value 10000000 and unit short', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -18,7 +18,7 @@ import { getColorFromHexRgbOrName } from './namedColorsPalette';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/index';
|
||||
|
||||
interface DisplayProcessorOptions {
|
||||
field?: FieldConfig;
|
||||
config?: FieldConfig;
|
||||
|
||||
// Context
|
||||
isUtc?: boolean;
|
||||
@ -27,7 +27,7 @@ interface DisplayProcessorOptions {
|
||||
|
||||
export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor {
|
||||
if (options && !_.isEmpty(options)) {
|
||||
const field = options.field ? options.field : {};
|
||||
const field = options.config ? options.config : {};
|
||||
const formatFunc = getValueFormat(field.unit || 'none');
|
||||
|
||||
return (value: any) => {
|
||||
|
@ -17,7 +17,6 @@ import toString from 'lodash/toString';
|
||||
import { GrafanaTheme, InterpolateFunction } from '../types/index';
|
||||
import { getDisplayProcessor } from './displayProcessor';
|
||||
import { getFlotPairs } from './flotPairs';
|
||||
import { DataLinkBuiltInVars } from '../utils/dataLinks';
|
||||
|
||||
export interface FieldDisplayOptions {
|
||||
values?: boolean; // If true show each row value
|
||||
@ -28,8 +27,8 @@ export interface FieldDisplayOptions {
|
||||
override: FieldConfig; // Set these values regardless of the source
|
||||
}
|
||||
// TODO: use built in variables, same as for data links?
|
||||
export const VAR_SERIES_NAME = '__series_name';
|
||||
export const VAR_FIELD_NAME = '__field_name';
|
||||
export const VAR_SERIES_NAME = '__series.name';
|
||||
export const VAR_FIELD_NAME = '__field.name';
|
||||
export const VAR_CALC = '__calc';
|
||||
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);
|
||||
}
|
||||
if (data.length > 1) {
|
||||
parts.push('$' + VAR_SERIES_NAME);
|
||||
parts.push('${' + VAR_SERIES_NAME + '}');
|
||||
}
|
||||
if (fieldCount > 1 || !parts.length) {
|
||||
parts.push('$' + VAR_FIELD_NAME);
|
||||
@ -70,8 +69,8 @@ export interface FieldDisplay {
|
||||
|
||||
// Expose to the original values for delayed inspection (DataLinks etc)
|
||||
view?: DataFrameView;
|
||||
column?: number; // The field column index
|
||||
row?: number; // only filled in when the value is from a row (ie, not a reduction)
|
||||
colIndex?: number; // The field column index
|
||||
rowIndex?: number; // only filled in when the value is from a row (ie, not a reduction)
|
||||
}
|
||||
|
||||
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 view = new DataFrameView(series);
|
||||
@ -125,15 +124,14 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
name = `Field[${s}]`;
|
||||
}
|
||||
|
||||
scopedVars[VAR_FIELD_NAME] = { text: 'Field', value: name };
|
||||
scopedVars['__field'] = { text: 'Field', value: { name } };
|
||||
|
||||
const display = getDisplayProcessor({
|
||||
field: config,
|
||||
config,
|
||||
theme: options.theme,
|
||||
});
|
||||
|
||||
const title = config.title ? config.title : defaultTitle;
|
||||
|
||||
// Show all rows
|
||||
if (fieldOptions.values) {
|
||||
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||
@ -158,8 +156,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
field: config,
|
||||
display: displayValue,
|
||||
view,
|
||||
column: i,
|
||||
row: j,
|
||||
colIndex: i,
|
||||
rowIndex: j,
|
||||
});
|
||||
|
||||
if (values.length >= limit) {
|
||||
@ -187,12 +185,12 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
const displayValue = display(results[calc]);
|
||||
displayValue.title = replaceVariables(title, scopedVars);
|
||||
values.push({
|
||||
name,
|
||||
name: calc,
|
||||
field: config,
|
||||
display: displayValue,
|
||||
sparkline,
|
||||
view,
|
||||
column: i,
|
||||
colIndex: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel {
|
||||
hasUniqueLabels = true;
|
||||
}
|
||||
|
||||
const timeFieldIndex = fieldCache.getFirstFieldOfType(FieldType.time);
|
||||
const timeField = fieldCache.getFirstFieldOfType(FieldType.time);
|
||||
const stringField = fieldCache.getFirstFieldOfType(FieldType.string);
|
||||
const logLevelField = fieldCache.getFieldByName('level');
|
||||
|
||||
@ -256,7 +256,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel {
|
||||
}
|
||||
|
||||
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 timeEpochMs = time.valueOf();
|
||||
const timeFromNow = time.fromNow();
|
||||
|
@ -46,7 +46,7 @@ export class KeybindingSrv {
|
||||
this.bind('g p', this.goToProfile);
|
||||
this.bind('s o', 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,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -191,7 +191,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -315,7 +315,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -426,7 +426,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -521,7 +521,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
|
@ -232,7 +232,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -469,7 +469,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -706,7 +706,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -943,7 +943,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 19,
|
||||
"schemaVersion": 20,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
|
@ -18,8 +18,10 @@ import { PanelModel } from '../state';
|
||||
import { DashboardModel } from '../state';
|
||||
import { VizPickerSearch } from './VizPickerSearch';
|
||||
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 { Unsubscribable } from 'rxjs';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@ -36,11 +38,13 @@ interface State {
|
||||
searchQuery: string;
|
||||
scrollTop: number;
|
||||
hasBeenFocused: boolean;
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
export class VisualizationTab extends PureComponent<Props, State> {
|
||||
element: HTMLElement;
|
||||
angularOptions: AngularComponent;
|
||||
querySubscription: Unsubscribable;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -50,6 +54,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
hasBeenFocused: false,
|
||||
searchQuery: '',
|
||||
scrollTop: 0,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,16 +74,28 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { panel } = this.props;
|
||||
const queryRunner = panel.getQueryRunner();
|
||||
if (this.shouldLoadAngularOptions()) {
|
||||
this.loadAngularOptions();
|
||||
}
|
||||
|
||||
this.querySubscription = queryRunner.getData().subscribe({
|
||||
next: (data: PanelData) => this.setState({ data }),
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
|
@ -128,7 +128,7 @@ describe('DashboardModel', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@ -441,6 +441,71 @@ describe('DashboardModel', () => {
|
||||
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[]) {
|
||||
|
@ -33,7 +33,7 @@ export class DashboardMigrator {
|
||||
let i, j, k, n;
|
||||
const oldVersion = this.dashboard.schemaVersion;
|
||||
const panelUpgrades = [];
|
||||
this.dashboard.schemaVersion = 19;
|
||||
this.dashboard.schemaVersion = 20;
|
||||
|
||||
if (oldVersion === this.dashboard.schemaVersion) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -666,3 +693,26 @@ function upgradePanelLink(link: any): DataLink {
|
||||
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 { FieldDisplay, DataLinkBuiltInVars } from '@grafana/ui';
|
||||
import { LinkModelSupplier, getTimeField, ScopedVars } from '@grafana/data';
|
||||
import { FieldDisplay } from '@grafana/ui';
|
||||
import { LinkModelSupplier, getTimeField, Labels, ScopedVars, ScopedVar } from '@grafana/data';
|
||||
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
|
||||
*/
|
||||
@ -14,29 +38,53 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
|
||||
}
|
||||
return {
|
||||
getLinks: (_scopedVars?: any) => {
|
||||
const scopedVars: ScopedVars = {};
|
||||
// TODO, add values to scopedVars and/or pass objects to event listeners
|
||||
const scopedVars: DataLinkScopedVars = {};
|
||||
|
||||
if (value.view) {
|
||||
scopedVars[DataLinkBuiltInVars.seriesName] = {
|
||||
const { dataFrame } = value.view;
|
||||
|
||||
scopedVars['__series'] = {
|
||||
value: {
|
||||
name: dataFrame.name,
|
||||
labels: dataFrame.labels,
|
||||
refId: dataFrame.refId,
|
||||
},
|
||||
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) {
|
||||
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);
|
||||
if (timeField) {
|
||||
scopedVars[DataLinkBuiltInVars.valueTime] = {
|
||||
text: 'Value time',
|
||||
value: timeField.values.get(value.row),
|
||||
};
|
||||
}
|
||||
scopedVars['__value'] = {
|
||||
value: {
|
||||
raw: field.values.get(value.rowIndex),
|
||||
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 {
|
||||
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 { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
||||
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[] => [
|
||||
...templateSrv.variables.map(variable => ({
|
||||
value: variable.name as string,
|
||||
label: variable.name,
|
||||
origin: VariableOrigin.Template,
|
||||
})),
|
||||
{
|
||||
value: `${DataLinkBuiltInVars.includeVars}`,
|
||||
label: 'All variables',
|
||||
documentation: 'Adds current variables',
|
||||
origin: VariableOrigin.BuiltIn,
|
||||
},
|
||||
{
|
||||
value: `${DataLinkBuiltInVars.keepTime}`,
|
||||
documentation: 'Adds current time range',
|
||||
origin: VariableOrigin.BuiltIn,
|
||||
origin: VariableOrigin.Template,
|
||||
},
|
||||
...timeRangeVars,
|
||||
];
|
||||
|
||||
export const getDataLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
...getPanelLinksVariableSuggestions(),
|
||||
{
|
||||
value: `${DataLinkBuiltInVars.seriesName}`,
|
||||
documentation: 'Adds series name',
|
||||
origin: VariableOrigin.BuiltIn,
|
||||
},
|
||||
{
|
||||
const getSeriesVars = (dataFrames: DataFrame[]) => {
|
||||
const labels = _.flatten(dataFrames.map(df => Object.keys(df.labels || {})));
|
||||
|
||||
return [
|
||||
{
|
||||
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}`,
|
||||
label: 'Time',
|
||||
documentation: 'Time value of the clicked datapoint (in ms epoch)',
|
||||
origin: VariableOrigin.BuiltIn,
|
||||
},
|
||||
];
|
||||
origin: VariableOrigin.Value,
|
||||
};
|
||||
|
||||
export const getCalculationValueDataLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
...getPanelLinksVariableSuggestions(),
|
||||
{
|
||||
value: `${DataLinkBuiltInVars.seriesName}`,
|
||||
documentation: 'Adds series name',
|
||||
origin: VariableOrigin.BuiltIn,
|
||||
},
|
||||
];
|
||||
return [...seriesVars, ...fieldVars, ...valueVars, valueTimeVar, ...getPanelLinksVariableSuggestions()];
|
||||
};
|
||||
|
||||
export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
|
||||
const seriesVars = getSeriesVars(dataFrames);
|
||||
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 {
|
||||
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 info: LinkModel<T> = {
|
||||
href: link.url,
|
||||
href: link.url.replace(/\s|\n/g, ''),
|
||||
title: this.templateSrv.replace(link.title || '', scopedVars),
|
||||
target: link.targetBlank ? '_blank' : '_self',
|
||||
origin,
|
||||
};
|
||||
|
||||
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
|
||||
|
||||
const variablesQuery = toUrlParams(params);
|
||||
info.href = this.templateSrv.replace(link.url, {
|
||||
info.href = this.templateSrv.replace(info.href, {
|
||||
...scopedVars,
|
||||
[DataLinkBuiltInVars.keepTime]: {
|
||||
text: timeRangeUrl,
|
||||
|
@ -105,11 +105,13 @@ describe('linkSrv', () => {
|
||||
linkSrv.getDataLinkUIModel(
|
||||
{
|
||||
title: 'Any title',
|
||||
url: `/d/1?var-test=$${DataLinkBuiltInVars.seriesName}`,
|
||||
url: `/d/1?var-test=$\{${DataLinkBuiltInVars.seriesName}}`,
|
||||
},
|
||||
{
|
||||
[DataLinkBuiltInVars.seriesName]: {
|
||||
value: 'A-series',
|
||||
__series: {
|
||||
value: {
|
||||
name: 'A-series',
|
||||
},
|
||||
text: 'A-series',
|
||||
},
|
||||
},
|
||||
@ -122,12 +124,12 @@ describe('linkSrv', () => {
|
||||
linkSrv.getDataLinkUIModel(
|
||||
{
|
||||
title: 'Any title',
|
||||
url: `/d/1?time=$${DataLinkBuiltInVars.valueTime}`,
|
||||
url: `/d/1?time=$\{${DataLinkBuiltInVars.valueTime}}`,
|
||||
},
|
||||
{
|
||||
[DataLinkBuiltInVars.valueTime]: {
|
||||
value: dataPointMock.datapoint[0],
|
||||
text: dataPointMock.datapoint[0],
|
||||
__value: {
|
||||
value: { time: dataPointMock.datapoint[0] },
|
||||
text: 'Value',
|
||||
},
|
||||
},
|
||||
{}
|
||||
|
@ -24,6 +24,27 @@ describe('templateSrv', () => {
|
||||
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', () => {
|
||||
const target = _templateSrv.replace('this.$test.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
|
@ -7,6 +7,10 @@ function luceneEscape(value: string) {
|
||||
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
|
||||
}
|
||||
|
||||
interface FieldAccessorCache {
|
||||
[key: string]: (obj: any) => any;
|
||||
}
|
||||
|
||||
export class TemplateSrv {
|
||||
variables: any[];
|
||||
|
||||
@ -15,6 +19,7 @@ export class TemplateSrv {
|
||||
private grafanaVariables: any = {};
|
||||
private builtIns: any = {};
|
||||
private timeRange: TimeRange = null;
|
||||
private fieldAccessorCache: FieldAccessorCache = {};
|
||||
|
||||
constructor() {
|
||||
this.builtIns['__interval'] = { text: '1s', value: '1s' };
|
||||
@ -224,21 +229,44 @@ export class TemplateSrv {
|
||||
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 {
|
||||
if (!target) {
|
||||
return target;
|
||||
}
|
||||
|
||||
let variable, systemValue, value, fmt;
|
||||
this.regex.lastIndex = 0;
|
||||
|
||||
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fmt3) => {
|
||||
variable = this.index[var1 || var2 || var3];
|
||||
fmt = fmt2 || fmt3 || format;
|
||||
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
||||
const variableName = var1 || var2 || var3;
|
||||
const variable = this.index[variableName];
|
||||
const fmt = fmt2 || fmt3 || format;
|
||||
|
||||
if (scopedVars) {
|
||||
value = scopedVars[var1 || var2 || var3];
|
||||
if (value) {
|
||||
return this.formatValue(value.value, fmt, variable);
|
||||
const value = this.getVariableValue(variableName, fieldPath, scopedVars);
|
||||
if (value !== null && value !== undefined) {
|
||||
return this.formatValue(value, fmt, variable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,12 +274,12 @@ export class TemplateSrv {
|
||||
return match;
|
||||
}
|
||||
|
||||
systemValue = this.grafanaVariables[variable.current.value];
|
||||
const systemValue = this.grafanaVariables[variable.current.value];
|
||||
if (systemValue) {
|
||||
return this.formatValue(systemValue, fmt, variable);
|
||||
}
|
||||
|
||||
value = variable.current.value;
|
||||
let value = variable.current.value;
|
||||
if (this.isAllValue(value)) {
|
||||
value = this.getAllValue(variable);
|
||||
// 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]]
|
||||
* \${(\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
|
||||
export const variableRegexExec = (variableString: string) => {
|
||||
|
@ -68,8 +68,8 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
||||
const { defaults } = fieldOptions;
|
||||
|
||||
const suggestions = fieldOptions.values
|
||||
? getDataLinksVariableSuggestions()
|
||||
: getCalculationValueDataLinksVariableSuggestions();
|
||||
? getDataLinksVariableSuggestions(this.props.data.series)
|
||||
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
|
||||
const labelWidth = 6;
|
||||
|
||||
return (
|
||||
|
@ -72,9 +72,10 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
||||
const { options } = this.props;
|
||||
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
|
||||
const { defaults } = fieldOptions;
|
||||
|
||||
const suggestions = fieldOptions.values
|
||||
? getDataLinksVariableSuggestions()
|
||||
: getCalculationValueDataLinksVariableSuggestions();
|
||||
? getDataLinksVariableSuggestions(this.props.data.series)
|
||||
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -12,7 +12,7 @@ export interface FlotDataPoint {
|
||||
export class GraphContextMenuCtrl {
|
||||
private source?: FlotDataPoint | null;
|
||||
private scope?: any;
|
||||
menuItems: ContextMenuItem[];
|
||||
menuItemsSupplier?: () => ContextMenuItem[];
|
||||
scrollContextElement: HTMLElement | null;
|
||||
position: {
|
||||
x: number;
|
||||
@ -23,7 +23,6 @@ export class GraphContextMenuCtrl {
|
||||
|
||||
constructor($scope: any) {
|
||||
this.isVisible = false;
|
||||
this.menuItems = [];
|
||||
this.scope = $scope;
|
||||
}
|
||||
|
||||
@ -70,11 +69,7 @@ export class GraphContextMenuCtrl {
|
||||
return this.source;
|
||||
};
|
||||
|
||||
setMenuItems = (items: ContextMenuItem[]) => {
|
||||
this.menuItems = items;
|
||||
};
|
||||
|
||||
getMenuItems = () => {
|
||||
return this.menuItems;
|
||||
setMenuItemsSupplier = (menuItemsSupplier: () => ContextMenuItem[]) => {
|
||||
this.menuItemsSupplier = menuItemsSupplier;
|
||||
};
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import GraphTooltip from './graph_tooltip';
|
||||
import { ThresholdManager } from './threshold_manager';
|
||||
import { TimeRegionManager } from './time_region_manager';
|
||||
import { EventManager } from 'app/features/annotations/all';
|
||||
import { LinkService, LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||
import { convertToHistogramData } from './histogram';
|
||||
import { alignYLevel } from './align_yaxes';
|
||||
import config from 'app/core/config';
|
||||
@ -25,12 +24,13 @@ import ReactDOM from 'react-dom';
|
||||
import { GraphLegendProps, Legend } from './Legend/Legend';
|
||||
|
||||
import { GraphCtrl } from './module';
|
||||
import { getValueFormat, ContextMenuItem, ContextMenuGroup, DataLinkBuiltInVars } from '@grafana/ui';
|
||||
import { provideTheme } from 'app/core/utils/ConfigProvider';
|
||||
import { DataLink, toUtc } from '@grafana/data';
|
||||
import { GraphContextMenuCtrl, FlotDataPoint } from './GraphContextMenuCtrl';
|
||||
import { getValueFormat, ContextMenuGroup, FieldDisplay, ContextMenuItem, getDisplayProcessor } from '@grafana/ui';
|
||||
import { provideTheme, getCurrentTheme } from 'app/core/utils/ConfigProvider';
|
||||
import { toUtc, LinkModelSupplier, DataFrameView } from '@grafana/data';
|
||||
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
|
||||
const LegendWithThemeProvider = provideTheme(Legend);
|
||||
|
||||
@ -50,7 +50,7 @@ class GraphElement {
|
||||
timeRegionManager: TimeRegionManager;
|
||||
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.contextMenu = scope.ctrl.contextMenuCtrl;
|
||||
this.dashboard = this.ctrl.dashboard;
|
||||
@ -175,53 +175,48 @@ class GraphElement {
|
||||
}
|
||||
}
|
||||
|
||||
getContextMenuItems = (flotPosition: { x: number; y: number }, item?: FlotDataPoint): ContextMenuGroup[] => {
|
||||
const dataLinks: DataLink[] = this.panel.options.dataLinks || [];
|
||||
getContextMenuItemsSupplier = (
|
||||
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[] = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: 'Add annotation',
|
||||
icon: 'gicon gicon-annotation',
|
||||
onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
if (!linksSupplier) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return item
|
||||
? [
|
||||
...items,
|
||||
{
|
||||
items: [
|
||||
...dataLinks.map<ContextMenuItem>(link => {
|
||||
const linkUiModel = this.linkSrv.getDataLinkUIModel(
|
||||
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 {
|
||||
label: linkUiModel.title,
|
||||
url: linkUiModel.href,
|
||||
target: linkUiModel.target,
|
||||
icon: `fa ${linkUiModel.target === '_self' ? 'fa-link' : 'fa-external-link'}`,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
]
|
||||
: items;
|
||||
const dataLinks = [
|
||||
{
|
||||
items: linksSupplier.getLinks(this.panel.scopedVars).map<ContextMenuItem>(link => {
|
||||
return {
|
||||
label: link.title,
|
||||
url: link.href,
|
||||
target: link.target,
|
||||
icon: `fa ${link.target === '_self' ? 'fa-link' : 'fa-external-link'}`,
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return [...items, ...dataLinks];
|
||||
};
|
||||
};
|
||||
|
||||
onPlotClick(event: JQueryEventObject, pos: any, item: any) {
|
||||
const scrollContextElement = this.elem.closest('.view') ? this.elem.closest('.view').get()[0] : null;
|
||||
const contextMenuSourceItem = item;
|
||||
let contextMenuItems: ContextMenuItem[];
|
||||
|
||||
if (this.panel.xaxis.mode !== 'time') {
|
||||
// Skip if panel in histogram or series mode
|
||||
@ -239,12 +234,40 @@ class GraphElement {
|
||||
return;
|
||||
} else {
|
||||
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(() => {
|
||||
// Setting nearest CustomScrollbar element as a scroll context for graph context menu
|
||||
this.contextMenu.setScrollContextElement(scrollContextElement);
|
||||
this.contextMenu.setSource(contextMenuSourceItem);
|
||||
this.contextMenu.setMenuItems(contextMenuItems);
|
||||
this.contextMenu.setMenuItemsSupplier(this.getContextMenuItemsSupplier(pos, linksSupplier) as any);
|
||||
this.contextMenu.toggleMenu(pos);
|
||||
});
|
||||
}
|
||||
@ -363,7 +386,6 @@ class GraphElement {
|
||||
this.thresholdManager.addFlotOptions(options, this.panel);
|
||||
this.timeRegionManager.addFlotOptions(options, this.panel);
|
||||
this.eventManager.addFlotEvents(this.annotations, options);
|
||||
|
||||
this.sortedSeries = this.sortSeries(this.data, this.panel);
|
||||
this.callPlot(options, true);
|
||||
}
|
||||
@ -855,12 +877,12 @@ class GraphElement {
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
function graphDirective(timeSrv: TimeSrv, popoverSrv: any, contextSrv: ContextSrv, linkSrv: LinkSrv) {
|
||||
function graphDirective(timeSrv: TimeSrv, popoverSrv: any, contextSrv: ContextSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '',
|
||||
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;
|
||||
processor: DataProcessor;
|
||||
contextMenuCtrl: GraphContextMenuCtrl;
|
||||
linkVariableSuggestions: VariableSuggestion[] = getDataLinksVariableSuggestions();
|
||||
linkVariableSuggestions: VariableSuggestion[] = [];
|
||||
|
||||
panelDefaults: any = {
|
||||
// datasource name, null = default datasource
|
||||
@ -216,6 +216,8 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
range: this.range,
|
||||
});
|
||||
|
||||
this.linkVariableSuggestions = getDataLinksVariableSuggestions(data);
|
||||
|
||||
this.dataWarning = null;
|
||||
const datapointsCount = this.seriesList.reduce((prev, series) => {
|
||||
return prev + series.datapoints.length;
|
||||
@ -337,6 +339,10 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
formatDate = (date: DateTimeInput, format?: string) => {
|
||||
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 };
|
||||
|
@ -121,7 +121,7 @@ describe('grafanaGraph', () => {
|
||||
$.plot = ctrl.plot = jest.fn();
|
||||
scope.ctrl = ctrl;
|
||||
|
||||
link = graphDirective({} as any, {}, {} as any, {} as any).link(scope, {
|
||||
link = graphDirective({} as any, {}, {} as any).link(scope, {
|
||||
width: () => 500,
|
||||
mouseleave: () => {},
|
||||
bind: () => {},
|
||||
|
@ -8,7 +8,7 @@ const template = `
|
||||
</div>
|
||||
<div ng-if="ctrl.contextMenuCtrl.isVisible">
|
||||
<graph-context-menu
|
||||
items="ctrl.contextMenuCtrl.menuItems"
|
||||
items="ctrl.contextMenuCtrl.menuItemsSupplier()"
|
||||
onClose="ctrl.onContextMenuClose"
|
||||
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
|
||||
formatSourceDate="ctrl.formatDate"
|
||||
|
@ -21,7 +21,7 @@ export const getGraphSeriesModel = (
|
||||
const graphs: GraphSeriesXY[] = [];
|
||||
|
||||
const displayProcessor = getDisplayProcessor({
|
||||
field: {
|
||||
config: {
|
||||
decimals: legendOptions.decimals,
|
||||
},
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onOptionsChange, options } = this.props;
|
||||
const { onOptionsChange, options, data } = this.props;
|
||||
const { fieldOptions } = options;
|
||||
const { defaults } = fieldOptions;
|
||||
|
||||
@ -51,7 +51,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
|
||||
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
|
||||
<PieChartOptionsBox data={data} onOptionsChange={onOptionsChange} options={options} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -238,7 +238,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
const processor = getDisplayProcessor({
|
||||
field: {
|
||||
config: {
|
||||
...fieldInfo.field.config,
|
||||
unit: panel.format,
|
||||
decimals: panel.decimals,
|
||||
|
@ -70,8 +70,8 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
||||
const { fieldOptions } = options;
|
||||
const { defaults } = fieldOptions;
|
||||
const suggestions = fieldOptions.values
|
||||
? getDataLinksVariableSuggestions()
|
||||
: getCalculationValueDataLinksVariableSuggestions();
|
||||
? getDataLinksVariableSuggestions(this.props.data.series)
|
||||
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
Loading…
Reference in New Issue
Block a user