Jelajahi Sumber

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
Torkel Ödegaard 6 tahun lalu
induk
melakukan
fd21e0ba14
39 mengubah file dengan 1174 tambahan dan 271 penghapusan
  1. 510 0
      devenv/dev-dashboards/feature-templating/testdata-datalinks.json
  2. 34 8
      docs/sources/features/panels/graph.md
  3. 4 3
      packages/grafana-data/src/utils/dataFrameHelper.ts
  4. 37 34
      packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx
  5. 6 6
      packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx
  6. 57 33
      packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx
  7. 2 1
      packages/grafana-ui/src/components/DataLinks/DataLinksEditor.tsx
  8. 4 1
      packages/grafana-ui/src/components/FormField/FormField.tsx
  9. 2 2
      packages/grafana-ui/src/components/List/AbstractList.tsx
  10. 2 2
      packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx
  11. 1 0
      packages/grafana-ui/src/types/panel.ts
  12. 10 2
      packages/grafana-ui/src/utils/dataLinks.ts
  13. 13 13
      packages/grafana-ui/src/utils/displayProcessor.test.ts
  14. 2 2
      packages/grafana-ui/src/utils/displayProcessor.ts
  15. 12 14
      packages/grafana-ui/src/utils/fieldDisplay.ts
  16. 2 2
      public/app/core/logs_model.ts
  17. 1 1
      public/app/core/services/keybindingSrv.ts
  18. 5 5
      public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap
  19. 4 4
      public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap
  20. 22 2
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  21. 66 1
      public/app/features/dashboard/state/DashboardMigrator.test.ts
  22. 51 1
      public/app/features/dashboard/state/DashboardMigrator.ts
  23. 65 17
      public/app/features/panel/panellinks/linkSuppliers.ts
  24. 96 26
      public/app/features/panel/panellinks/link_srv.ts
  25. 9 7
      public/app/features/panel/panellinks/specs/link_srv.test.ts
  26. 21 0
      public/app/features/templating/specs/template_srv.test.ts
  27. 37 9
      public/app/features/templating/template_srv.ts
  28. 1 1
      public/app/features/templating/variable.ts
  29. 2 2
      public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
  30. 3 2
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  31. 3 8
      public/app/plugins/panel/graph/GraphContextMenuCtrl.ts
  32. 75 53
      public/app/plugins/panel/graph/graph.ts
  33. 7 1
      public/app/plugins/panel/graph/module.ts
  34. 1 1
      public/app/plugins/panel/graph/specs/graph.test.ts
  35. 1 1
      public/app/plugins/panel/graph/template.ts
  36. 1 1
      public/app/plugins/panel/graph2/getGraphSeriesModel.ts
  37. 2 2
      public/app/plugins/panel/piechart/PieChartPanelEditor.tsx
  38. 1 1
      public/app/plugins/panel/singlestat/module.ts
  39. 2 2
      public/app/plugins/panel/singlestat2/SingleStatEditor.tsx

+ 510 - 0
devenv/dev-dashboards/feature-templating/testdata-datalinks.json

@@ -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
+}

+ 34 - 8
docs/sources/features/panels/graph.md

@@ -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
 
-#### Template variables in data links
 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.

+ 4 - 3
packages/grafana-data/src/utils/dataFrameHelper.ts

@@ -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 };
       }
     }
   }

+ 37 - 34
packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx

@@ -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>
     );
   }

+ 6 - 6
packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx

@@ -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 }) => {

+ 57 - 33
packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx

@@ -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>
             );
           }}

+ 2 - 1
packages/grafana-ui/src/components/DataLinks/DataLinksEditor.tsx

@@ -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}

+ 4 - 1
packages/grafana-ui/src/components/FormField/FormField.tsx

@@ -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>
   );
 };

+ 2 - 2
packages/grafana-ui/src/components/List/AbstractList.tsx

@@ -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}>

+ 2 - 2
packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx

@@ -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>

+ 1 - 0
packages/grafana-ui/src/types/panel.ts

@@ -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> {

+ 10 - 2
packages/grafana-ui/src/utils/dataLinks.ts

@@ -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',
 };
 
 /**

+ 13 - 13
packages/grafana-ui/src/utils/displayProcessor.test.ts

@@ -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');
   });
 });

+ 2 - 2
packages/grafana-ui/src/utils/displayProcessor.ts

@@ -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) => {

+ 12 - 14
packages/grafana-ui/src/utils/fieldDisplay.ts

@@ -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,
             });
           }
         }

+ 2 - 2
public/app/core/logs_model.ts

@@ -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();

+ 1 - 1
public/app/core/services/keybindingSrv.ts

@@ -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);
     }
   }
 

+ 5 - 5
public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap

@@ -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 [],

+ 4 - 4
public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap

@@ -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 [],

+ 22 - 2
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -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) {

+ 66 - 1
public/app/features/dashboard/state/DashboardMigrator.test.ts

@@ -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[]) {

+ 51 - 1
public/app/features/dashboard/state/DashboardMigrator.ts

@@ -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;
+  });
+}

+ 65 - 17
public/app/features/panel/panellinks/linkSuppliers.ts

@@ -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);

+ 96 - 26
public/app/features/panel/panellinks/link_srv.ts

@@ -4,48 +4,119 @@ 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';
 
-export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
-  ...templateSrv.variables.map(variable => ({
-    value: variable.name as string,
-    origin: VariableOrigin.Template,
-  })),
+const timeRangeVars = [
   {
-    value: `${DataLinkBuiltInVars.includeVars}`,
-    documentation: 'Adds current variables',
+    value: `${DataLinkBuiltInVars.keepTime}`,
+    label: 'Time range',
+    documentation: 'Adds current time range',
     origin: VariableOrigin.BuiltIn,
   },
   {
-    value: `${DataLinkBuiltInVars.keepTime}`,
-    documentation: 'Adds current time range',
+    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,
   },
 ];
 
-export const getDataLinksVariableSuggestions = (): VariableSuggestion[] => [
-  ...getPanelLinksVariableSuggestions(),
+const fieldVars = [
   {
-    value: `${DataLinkBuiltInVars.seriesName}`,
-    documentation: 'Adds series name',
-    origin: VariableOrigin.BuiltIn,
+    value: `${DataLinkBuiltInVars.fieldName}`,
+    label: 'Name',
+    documentation: 'Field name of the clicked datapoint (in ms epoch)',
+    origin: VariableOrigin.Field,
   },
+];
+
+const valueVars = [
   {
-    value: `${DataLinkBuiltInVars.valueTime}`,
-    documentation: 'Time value of the clicked datapoint (in ms epoch)',
-    origin: VariableOrigin.BuiltIn,
+    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,
   },
 ];
 
-export const getCalculationValueDataLinksVariableSuggestions = (): VariableSuggestion[] => [
-  ...getPanelLinksVariableSuggestions(),
+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.seriesName}`,
-    documentation: 'Adds series name',
-    origin: VariableOrigin.BuiltIn,
+    value: `${DataLinkBuiltInVars.includeVars}`,
+    label: 'All variables',
+    documentation: 'Adds current variables',
+    origin: VariableOrigin.Template,
   },
+  ...timeRangeVars,
 ];
 
+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.Value,
+  };
+
+  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,

+ 9 - 7
public/app/features/panel/panellinks/specs/link_srv.test.ts

@@ -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',
             },
           },
           {}

+ 21 - 0
public/app/features/templating/specs/template_srv.test.ts

@@ -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' },

+ 37 - 9
public/app/features/templating/template_srv.ts

@@ -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

+ 1 - 1
public/app/features/templating/variable.ts

@@ -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) => {

+ 2 - 2
public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx

@@ -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 (

+ 3 - 2
public/app/plugins/panel/gauge/GaugePanelEditor.tsx

@@ -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 (
       <>

+ 3 - 8
public/app/plugins/panel/graph/GraphContextMenuCtrl.ts

@@ -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;
   };
 }

+ 75 - 53
public/app/plugins/panel/graph/graph.ts

@@ -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 || [];
-
-    const items: ContextMenuGroup[] = [
-      {
-        items: [
-          {
-            label: 'Add annotation',
-            icon: 'gicon gicon-annotation',
-            onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),
-          },
-        ],
-      },
-    ];
-
-    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;
+  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 }),
+            },
+          ],
+        },
+      ];
+
+      if (!linksSupplier) {
+        return 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);
     },
   };
 }

+ 7 - 1
public/app/plugins/panel/graph/module.ts

@@ -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 };

+ 1 - 1
public/app/plugins/panel/graph/specs/graph.test.ts

@@ -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: () => {},

+ 1 - 1
public/app/plugins/panel/graph/template.ts

@@ -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"

+ 1 - 1
public/app/plugins/panel/graph2/getGraphSeriesModel.ts

@@ -21,7 +21,7 @@ export const getGraphSeriesModel = (
   const graphs: GraphSeriesXY[] = [];
 
   const displayProcessor = getDisplayProcessor({
-    field: {
+    config: {
       decimals: legendOptions.decimals,
     },
   });

+ 2 - 2
public/app/plugins/panel/piechart/PieChartPanelEditor.tsx

@@ -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} />

+ 1 - 1
public/app/plugins/panel/singlestat/module.ts

@@ -238,7 +238,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     }
 
     const processor = getDisplayProcessor({
-      field: {
+      config: {
         ...fieldInfo.field.config,
         unit: panel.format,
         decimals: panel.decimals,

+ 2 - 2
public/app/plugins/panel/singlestat2/SingleStatEditor.tsx

@@ -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 (
       <>