From ff6b8c5adc8e96f6a34385d113c93f2a4948914a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 27 Aug 2019 23:50:43 -0700 Subject: [PATCH] DataLinks: enable data links in Gauge, BarGauge and SingleStat2 panel (#18605) * datalink on field * add dataFrame to view * Use scoped variables to pass series name and value time to data links interpolation * Use scoped variables to pass series name and value time to data links interpolation * Enable value specific variable suggestions when Gauge is displaying values * Fix prettier * Add basic context menu with data links to GaugePanel * Fix incorrect import in grafana/ui * Add custom cursor indicating datalinks available via context menu (in Gauge only now) * Add data links to SingleStat2 * Minor refactor * Retrieve data links in a lazy way * Update test to respect links retrieval being lazy * delay link creation * cleanup * Add origin to LinkModel and introduce field & panel links suppliers * Add value time and series name field link supplier * Remove links prop from visualization and implement common UI for data links context menu * Update snapshot * Rename className prop to clickTargetClassName * Simplify condition * Updated drilldown dashboard and minor changes * Use class name an onClick handler on the top level dom element in visualization * Enable series name interpolation when presented value is a calculation --- .../testdata-nested-variables.json | 128 ++++++++++++++++-- packages/grafana-data/src/types/dataFrame.ts | 4 + packages/grafana-data/src/types/dataLink.ts | 25 ++++ .../grafana-data/src/utils/dataFrameView.ts | 4 + .../src/components/BarGauge/BarGauge.tsx | 18 ++- .../__snapshots__/BarGauge.test.tsx.snap | 56 ++++---- .../src/components/BigValue/BigValue.tsx | 19 ++- .../components/ContextMenu/ContextMenu.tsx | 3 +- .../ContextMenu/WithContextMenu.tsx | 35 +++++ .../components/DataLinks/DataLinkEditor.tsx | 1 + .../DataLinks/DataLinksContextMenu.tsx | 33 +++++ .../components/DataLinks/DataLinksEditor.tsx | 2 +- .../src/components/FormField/_FormField.scss | 2 +- .../grafana-ui/src/components/Gauge/Gauge.tsx | 37 +++-- .../ValueMappingsEditor.test.tsx | 2 +- .../ValueMappingsEditor.tsx | 28 ++-- .../ValueMappingsEditor.test.tsx.snap | 66 +++++---- packages/grafana-ui/src/components/index.ts | 1 + packages/grafana-ui/src/utils/dataLinks.ts | 24 ++++ packages/grafana-ui/src/utils/fieldDisplay.ts | 63 +++++---- packages/grafana-ui/src/utils/index.ts | 1 + .../dashgrid/PanelHeader/PanelHeader.tsx | 3 +- .../PanelHeader/PanelHeaderCorner.test.tsx | 9 +- .../PanelHeader/PanelHeaderCorner.tsx | 17 +-- .../dashboard/state/DashboardMigrator.test.ts | 2 +- .../dashboard/state/DashboardMigrator.ts | 2 +- public/app/features/panel/panel_ctrl.ts | 16 +-- .../panel/panellinks/linkSuppliers.ts | 66 +++++++++ .../app/features/panel/panellinks/link_srv.ts | 62 ++------- .../panel/panellinks/specs/link_srv.test.ts | 27 +++- .../plugins/panel/bargauge/BarGaugePanel.tsx | 35 +++-- .../panel/bargauge/BarGaugePanelEditor.tsx | 25 +++- public/app/plugins/panel/gauge/GaugePanel.tsx | 33 +++-- .../plugins/panel/gauge/GaugePanelEditor.tsx | 26 +++- public/app/plugins/panel/graph/graph.ts | 15 +- public/app/plugins/panel/singlestat/module.ts | 7 +- .../panel/singlestat2/SingleStatEditor.tsx | 26 +++- .../panel/singlestat2/SingleStatPanel.tsx | 28 +++- 38 files changed, 708 insertions(+), 243 deletions(-) create mode 100644 packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx create mode 100644 packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx create mode 100644 packages/grafana-ui/src/utils/dataLinks.ts create mode 100644 public/app/features/panel/panellinks/linkSuppliers.ts diff --git a/devenv/dev-dashboards/feature-templating/testdata-nested-variables.json b/devenv/dev-dashboards/feature-templating/testdata-nested-variables.json index 90df8ad99f6..dd76b2c61d4 100644 --- a/devenv/dev-dashboards/feature-templating/testdata-nested-variables.json +++ b/devenv/dev-dashboards/feature-templating/testdata-nested-variables.json @@ -15,14 +15,15 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1565097360786, + "id": 13844, + "iteration": 1566896059256, "links": [], "panels": [ { "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod", "gridPos": { - "h": 6, - "w": 14, + "h": 9, + "w": 12, "x": 0, "y": 0 }, @@ -55,9 +56,9 @@ "thresholdMarkers": true }, "gridPos": { - "h": 6, - "w": 10, - "x": 14, + "h": 9, + "w": 4, + "x": 12, "y": 0 }, "id": 6, @@ -116,6 +117,117 @@ ], "valueName": "avg" }, + { + "cacheTimeout": null, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 8, + "links": [], + "options": { + "fieldOptions": { + "calcs": ["mean"], + "defaults": { + "links": [ + { + "targetBlank": true, + "title": "Go to drilldown", + "url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1&${__all_variables}&${__url_time_range}" + } + ], + "mappings": [], + "max": 100, + "min": 0, + "nullValueMode": "connected", + "thresholds": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ], + "unit": "none" + }, + "override": {}, + "values": false + }, + "orientation": "horizontal", + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "6.4.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "React gauge datalink", + "type": "gauge" + }, + { + "cacheTimeout": null, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 9, + "links": [], + "options": { + "displayMode": "basic", + "fieldOptions": { + "calcs": ["mean"], + "defaults": { + "links": [ + { + "targetBlank": true, + "title": "Go to drilldown", + "url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1&${__all_variables}&${__url_time_range}" + } + ], + "mappings": [], + "max": 100, + "min": 0, + "nullValueMode": "connected", + "thresholds": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ], + "unit": "none" + }, + "override": {}, + "values": false + }, + "orientation": "vertical" + }, + "pluginVersion": "6.4.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "React gauge datalink", + "type": "bargauge" + }, { "aliasColors": {}, "bars": false, @@ -128,7 +240,7 @@ "h": 13, "w": 24, "x": 0, - "y": 6 + "y": 9 }, "id": 2, "legend": { @@ -296,5 +408,5 @@ "timezone": "", "title": "Templating - Nested Template Variables", "uid": "-Y-tnEDWk", - "version": 11 + "version": 2 } diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index b18f6ef0e2d..c560260b4cb 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -3,6 +3,7 @@ import { ValueMapping } from './valueMapping'; import { QueryResultBase, Labels, NullValueMode } from './data'; import { FieldCalcs } from '../utils/index'; import { DisplayProcessor } from './displayValue'; +import { DataLink } from './dataLink'; export enum FieldType { time = 'time', // or date @@ -36,6 +37,9 @@ export interface FieldConfig { // Used when reducing field values nullValueMode?: NullValueMode; + // The behavior when clicking on a result + links?: DataLink[]; + // Alternative to empty string noValue?: string; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 0551eb28742..55b1c13af06 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -1,5 +1,30 @@ +/** + * Link configuration. The values may contain variables that need to be + * processed before running + */ export interface DataLink { url: string; title: string; targetBlank?: boolean; } + +export type LinkTarget = '_blank' | '_self'; + +/** + * Processed Link Model. The values are ready to use + */ +export interface LinkModel { + href: string; + title: string; + target: LinkTarget; + origin: T; +} + +/** + * Provides a way to produce links on demand + * + * TODO: ScopedVars in in GrafanaUI package! + */ +export interface LinkModelSupplier { + getLinks(scopedVars?: any): Array>; +} diff --git a/packages/grafana-data/src/utils/dataFrameView.ts b/packages/grafana-data/src/utils/dataFrameView.ts index d80b376a0f7..1838779478c 100644 --- a/packages/grafana-data/src/utils/dataFrameView.ts +++ b/packages/grafana-data/src/utils/dataFrameView.ts @@ -44,6 +44,10 @@ export class DataFrameView implements Vector { this.obj = obj; } + get dataFrame() { + return this.data; + } + get length() { return this.data.length; } diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index e282d0afb02..a4fc9404467 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -26,6 +26,8 @@ export interface Props extends Themeable { orientation: VizOrientation; itemSpacing?: number; displayMode: 'basic' | 'lcd' | 'gradient'; + onClick?: React.MouseEventHandler; + className?: string; } export class BarGauge extends PureComponent { @@ -43,16 +45,20 @@ export class BarGauge extends PureComponent { }; render() { + const { onClick, className } = this.props; const { title } = this.props.value; - - if (!title) { - return this.renderBarAndValue(); - } - const styles = getTitleStyles(this.props); + if (!title) { + return ( +
+ {this.renderBarAndValue()} +
+ ); + } + return ( -
+
{title}
{this.renderBarAndValue()}
diff --git a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap index 1d341a9b0d4..320b53aa161 100644 --- a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap +++ b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap @@ -4,41 +4,51 @@ exports[`BarGauge Render with basic options should render 1`] = `
- 25 -
-
+ > + 25 +
+
+
`; diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.tsx index 3799fafb0d8..e922ad131f0 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.tsx @@ -1,7 +1,7 @@ // Library import React, { PureComponent, ReactNode, CSSProperties } from 'react'; import $ from 'jquery'; -import { css } from 'emotion'; +import { css, cx } from 'emotion'; import { DisplayValue } from '@grafana/data'; // Utils @@ -27,6 +27,8 @@ export interface Props extends Themeable { suffix?: DisplayValue; sparkline?: BigValueSparkline; backgroundColor?: string; + onClick?: React.MouseEventHandler; + className?: string; } /* @@ -119,15 +121,19 @@ export class BigValue extends PureComponent { } render() { - const { height, width, value, prefix, suffix, sparkline, backgroundColor } = this.props; + const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props; return (
{value.title && (
{ {value.title}
)} + ) => void; diff --git a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx new file mode 100644 index 00000000000..dd5b2fd0bbf --- /dev/null +++ b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import { ContextMenu, ContextMenuGroup } from '../ContextMenu/ContextMenu'; + +interface WithContextMenuProps { + children: (props: { openMenu: React.MouseEventHandler }) => JSX.Element; + getContextMenuItems: () => ContextMenuGroup[]; +} + +export const WithContextMenu: React.FC = ({ children, getContextMenuItems }) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [menuPosition, setMenuPositon] = useState({ x: 0, y: 0 }); + + return ( + <> + {children({ + openMenu: e => { + setIsMenuOpen(true); + setMenuPositon({ + x: e.pageX, + y: e.pageY, + }); + }, + })} + + {isMenuOpen && ( + setIsMenuOpen(false)} + x={menuPosition.x} + y={menuPosition.y} + items={getContextMenuItems()} + /> + )} + + ); +}; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index 8a4c5df9122..4aafe4ebce3 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -59,6 +59,7 @@ export const DataLinkEditor: React.FC = React.memo( onBlur={onTitleBlur} inputWidth={15} labelWidth={5} + placeholder="Show details" /> ; targetClassName?: string }) => JSX.Element; + links?: LinkModelSupplier; +} + +export const DataLinksContextMenu: React.FC = ({ children, links }) => { + if (!links) { + return children({}); + } + + const getDataLinksContextMenuItems = () => { + return [{ items: linkModelToContextMenuItems(links), label: 'Data links' }]; + }; + + // Use this class name (exposed via render prop) to add context menu indicator to the click target of the visualization + const targetClassName = css` + cursor: context-menu; + `; + + return ( + + {({ openMenu }) => { + return children({ openMenu, targetClassName }); + }} + + ); +}; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksEditor.tsx index 881fad58d4e..725baed7a6c 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksEditor.tsx @@ -68,7 +68,7 @@ export const DataLinksEditor: FC = React.memo(({ value, on {(!value || (value && value.length < (maxLinks || Infinity))) && ( )} diff --git a/packages/grafana-ui/src/components/FormField/_FormField.scss b/packages/grafana-ui/src/components/FormField/_FormField.scss index 0c69e67f82a..4d6d18a6ccd 100644 --- a/packages/grafana-ui/src/components/FormField/_FormField.scss +++ b/packages/grafana-ui/src/components/FormField/_FormField.scss @@ -2,7 +2,7 @@ margin-bottom: $space-xxs; display: flex; flex-direction: row; - align-items: center; + align-items: flex-start; text-align: left; position: relative; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 868caa43a6d..420c1907693 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -15,6 +15,8 @@ export interface Props extends Themeable { showThresholdLabels: boolean; width: number; value: DisplayValue; + onClick?: React.MouseEventHandler; + className?: string; } const FONT_SCALE = 1; @@ -133,24 +135,16 @@ export class Gauge extends PureComponent { } } - render() { - const { width, value, height } = this.props; + renderVisualization = () => { + const { width, value, height, onClick } = this.props; const autoProps = calculateGaugeAutoProps(width, height, value.title); return ( -
+ <>
(this.canvasElement = element)} + onClick={onClick} /> {autoProps.showLabel && (
{ position: 'relative', width: '100%', top: '-4px', + cursor: 'default', }} > {value.title}
)} + + ); + }; + + render() { + return ( +
+ {this.renderVisualization()}
); } diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx index 33c05156c10..9e5ea12fc5d 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx @@ -59,7 +59,7 @@ describe('Next id to add', () => { it('should be 4', () => { const { instance } = setup(); - instance.addMapping(); + instance.onAddMapping(); expect(instance.state.nextIdToAdd).toEqual(4); }); diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx index d3e87a806dc..5f187ee2a44 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import MappingRow from './MappingRow'; import { MappingType, ValueMapping } from '@grafana/data'; +import { Button } from '../index'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; export interface Props { @@ -30,7 +31,7 @@ export class ValueMappingsEditor extends PureComponent { return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1; } - addMapping = () => + onAddMapping = () => this.setState(prevState => ({ valueMappings: [ ...prevState.valueMappings, @@ -81,16 +82,21 @@ export class ValueMappingsEditor extends PureComponent { const { valueMappings } = this.state; return ( - - {valueMappings.length > 0 && - valueMappings.map((valueMapping, index) => ( - this.onRemoveMapping(valueMapping.id)} - /> - ))} + +
+ {valueMappings.length > 0 && + valueMappings.map((valueMapping, index) => ( + this.onRemoveMapping(valueMapping.id)} + /> + ))} + +
); } diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap index b0dd7d81840..11257b7a47e 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap @@ -2,37 +2,45 @@ exports[`Render should render component 1`] = ` - + - + + /> + +
`; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index b8075e10cda..f915b6ce681 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -76,4 +76,5 @@ export { CallToActionCard } from './CallToActionCard/CallToActionCard'; export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu'; export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggestions'; export { DataLinksEditor } from './DataLinks/DataLinksEditor'; +export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; export { SeriesIcon } from './Legend/SeriesIcon'; diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts new file mode 100644 index 00000000000..38f212a745a --- /dev/null +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -0,0 +1,24 @@ +import { ContextMenuItem } from '../components/ContextMenu/ContextMenu'; +import { LinkModelSupplier } from '@grafana/data'; + +export const DataLinkBuiltInVars = { + keepTime: '__url_time_range', + includeVars: '__all_variables', + seriesName: '__series_name', + valueTime: '__value_time', +}; + +/** + * Delays creating links until we need to open the ContextMenu + */ +export const linkModelToContextMenuItems: (links: LinkModelSupplier) => ContextMenuItem[] = links => { + return links.getLinks().map(link => { + return { + label: link.title, + // TODO: rename to href + url: link.href, + target: link.target, + icon: `fa ${link.target === '_self' ? 'fa-link' : 'fa-external-link'}`, + }; + }); +}; diff --git a/packages/grafana-ui/src/utils/fieldDisplay.ts b/packages/grafana-ui/src/utils/fieldDisplay.ts index 86c149745a4..66e06f01e0c 100644 --- a/packages/grafana-ui/src/utils/fieldDisplay.ts +++ b/packages/grafana-ui/src/utils/fieldDisplay.ts @@ -6,6 +6,7 @@ import { FieldConfig, DisplayValue, GraphSeriesValue, + DataFrameView, } from '@grafana/data'; import toNumber from 'lodash/toNumber'; @@ -14,6 +15,7 @@ import toString from 'lodash/toString'; import { GrafanaTheme, InterpolateFunction, ScopedVars } 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 @@ -23,7 +25,7 @@ export interface FieldDisplayOptions { defaults: FieldConfig; // Use these values unless otherwise stated 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_CALC = '__calc'; @@ -59,10 +61,15 @@ function getTitleTemplate(title: string | undefined, stats: string[], data?: Dat } export interface FieldDisplay { - name: string; // NOT title! + name: string; // The field name (title is in display) field: FieldConfig; display: DisplayValue; sparkline?: GraphSeriesValue[][]; + + // 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) } export interface GetFieldDisplayValuesOptions { @@ -75,8 +82,19 @@ export interface GetFieldDisplayValuesOptions { export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25; +const getTimeColumnIdx = (series: DataFrame) => { + let timeColumn = -1; + for (let i = 0; i < series.fields.length; i++) { + if (series.fields[i].type === FieldType.time) { + timeColumn = i; + break; + } + } + return timeColumn; +}; + export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => { - const { data, replaceVariables, fieldOptions, sparkline } = options; + const { data, replaceVariables, fieldOptions } = options; const { defaults, override } = fieldOptions; const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last]; @@ -96,17 +114,11 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi name: series.refId ? series.refId : `Series[${s}]`, }; } - scopedVars[VAR_SERIES_NAME] = { text: 'Series', value: series.name }; - let timeColumn = -1; - if (sparkline) { - for (let i = 0; i < series.fields.length; i++) { - if (series.fields[i].type === FieldType.time) { - timeColumn = i; - break; - } - } - } + scopedVars[DataLinkBuiltInVars.seriesName] = { text: 'Series', value: series.name }; + + const timeColumn = getTimeColumnIdx(series); + const view = new DataFrameView(series); for (let i = 0; i < series.fields.length && !hitLimit; i++) { const field = series.fields[i]; @@ -131,7 +143,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi const title = config.title ? config.title : defaultTitle; - // Show all number fields + // Show all rows if (fieldOptions.values) { const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0; @@ -154,6 +166,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi name, field: config, display: displayValue, + view, + column: i, + row: j, }); if (values.length >= limit) { @@ -166,15 +181,15 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi field, reducers: calcs, // The stats to calculate }); + let sparkline: GraphSeriesValue[][] | undefined = undefined; - // Single sparkline for a field - const points = - timeColumn < 0 - ? undefined - : getFlotPairs({ - xField: series.fields[timeColumn], - yField: series.fields[i], - }); + // Single sparkline for every reducer + if (options.sparkline && timeColumn >= 0) { + sparkline = getFlotPairs({ + xField: series.fields[timeColumn], + yField: series.fields[i], + }); + } for (const calc of calcs) { scopedVars[VAR_CALC] = { value: calc, text: calc }; @@ -184,7 +199,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi name, field: config, display: displayValue, - sparkline: points, + sparkline, + view, + column: i, }); } } diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 991b8558857..8f5457e4adf 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -6,6 +6,7 @@ export * from './fieldDisplay'; export * from './validate'; export { getFlotPairs } from './flotPairs'; export * from './slate'; +export * from './dataLinks'; export { default as ansicolor } from './ansicolor'; // Export with a namespace diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 758ef85f438..852476c52de 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -11,6 +11,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { ClickOutsideWrapper } from '@grafana/ui'; import { DataLink } from '@grafana/data'; +import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; export interface Props { panel: PanelModel; @@ -88,7 +89,7 @@ export class PanelHeader extends Component { title={panel.title} description={panel.description} scopedVars={panel.scopedVars} - links={panel.links} + links={getPanelLinksSupplier(panel)} error={error} />
{ it('should render component', () => { const panel = new PanelModel({}); - const links: any[] = [ - { - url: 'asd', - title: 'asd', - }, - ]; - - const wrapper = shallow(); + const wrapper = shallow(); const instance = wrapper.instance() as PanelHeaderCorner; expect(instance.getInfoContent()).toBeDefined(); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index 0a34242450e..ee79bc4f91c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -1,12 +1,10 @@ import React, { Component } from 'react'; -import { renderMarkdown } from '@grafana/data'; +import { renderMarkdown, LinkModelSupplier } from '@grafana/data'; import { Tooltip, ScopedVars, PopoverContent } from '@grafana/ui'; -import { DataLink } from '@grafana/data'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import templateSrv from 'app/features/templating/template_srv'; -import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; enum InfoMode { @@ -20,7 +18,7 @@ interface Props { title?: string; description?: string; scopedVars?: ScopedVars; - links?: DataLink[]; + links?: LinkModelSupplier; error?: string; } @@ -45,22 +43,21 @@ export class PanelHeaderCorner extends Component { getInfoContent = (): JSX.Element => { const { panel } = this.props; const markdown = panel.description || ''; - const linkSrv = new LinkSrv(templateSrv, this.timeSrv); const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars); const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown); + const links = this.props.links && this.props.links.getLinks(panel); return (
- {panel.links && panel.links.length > 0 && ( + {links && links.length > 0 && (
    - {panel.links.map((link, idx) => { - const info = linkSrv.getDataLinkUIModel(link, panel.scopedVars); + {links.map((link, idx) => { return (
  • - - {info.title} + + {link.title}
  • ); diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index 7bb0082127a..fedc4e17fd2 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -3,7 +3,7 @@ import { DashboardModel } from '../state/DashboardModel'; import { PanelModel } from '../state/PanelModel'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { expect } from 'test/lib/common'; -import { DataLinkBuiltInVars } from 'app/features/panel/panellinks/link_srv'; +import { DataLinkBuiltInVars } from '@grafana/ui'; jest.mock('app/core/services/context_srv', () => ({})); diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index e9ec4228d60..e7811c6dcff 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -20,7 +20,7 @@ import { MIN_PANEL_HEIGHT, DEFAULT_PANEL_SPAN, } from 'app/core/constants'; -import { DataLinkBuiltInVars } from 'app/features/panel/panellinks/link_srv'; +import { DataLinkBuiltInVars } from '@grafana/ui'; export class DashboardMigrator { dashboard: DashboardModel; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 185d517025e..b32aaae051d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -18,8 +18,8 @@ import { import { GRID_COLUMN_COUNT } from 'app/core/constants'; import { auto } from 'angular'; import { TemplateSrv } from '../templating/template_srv'; -import { LinkSrv } from './panellinks/link_srv'; import { PanelPluginMeta } from '@grafana/ui/src/types/panel'; +import { getPanelLinksSupplier } from './panellinks/linkSuppliers'; export class PanelCtrl { panel: any; @@ -255,31 +255,31 @@ export class PanelCtrl { markdown = this.error || this.panel.description || ''; } - const linkSrv: LinkSrv = this.$injector.get('linkSrv'); const templateSrv: TemplateSrv = this.$injector.get('templateSrv'); const interpolatedMarkdown = templateSrv.replace(markdown, this.panel.scopedVars); let html = '
    '; const md = renderMarkdown(interpolatedMarkdown); html += config.disableSanitizeHtml ? md : sanitize(md); + const links = this.panel.links && getPanelLinksSupplier(this.panel).getLinks(); - if (this.panel.links && this.panel.links.length > 0) { + if (links && links.length > 0) { html += ''; } html += '
    '; + return html; } diff --git a/public/app/features/panel/panellinks/linkSuppliers.ts b/public/app/features/panel/panellinks/linkSuppliers.ts new file mode 100644 index 00000000000..376d631a192 --- /dev/null +++ b/public/app/features/panel/panellinks/linkSuppliers.ts @@ -0,0 +1,66 @@ +import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { FieldDisplay, ScopedVars, DataLinkBuiltInVars } from '@grafana/ui'; +import { LinkModelSupplier, DataFrameHelper, FieldType } from '@grafana/data'; +import { getLinkSrv } from './link_srv'; + +/** + * Link suppliers creates link models based on a link origin + */ + +export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier | undefined => { + const links = value.field.links; + if (!links || links.length === 0) { + return undefined; + } + return { + getLinks: (_scopedVars?: any) => { + const scopedVars: ScopedVars = {}; + // TODO, add values to scopedVars and/or pass objects to event listeners + if (value.view) { + scopedVars[DataLinkBuiltInVars.seriesName] = { + text: 'Series', + value: value.view.dataFrame.name, + }; + const field = value.column ? value.view.dataFrame.fields[value.column] : undefined; + if (field) { + console.log('Full Field Info:', field); + } + if (value.row) { + const row = value.view.get(value.row); + console.log('ROW:', row); + const dataFrame = new DataFrameHelper(value.view.dataFrame); + + const timeField = dataFrame.getFirstFieldOfType(FieldType.time); + if (timeField) { + scopedVars[DataLinkBuiltInVars.valueTime] = { + text: 'Value time', + value: timeField.values.get(value.row), + }; + } + } + } else { + console.log('VALUE', value); + } + + return links.map(link => { + return getLinkSrv().getDataLinkUIModel(link, scopedVars, value); + }); + }, + }; +}; + +export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier => { + const links = value.links; + + if (!links || links.length === 0) { + return undefined; + } + + return { + getLinks: () => { + return links.map(link => { + return getLinkSrv().getDataLinkUIModel(link, value.scopedVars, value); + }); + }, + }; +}; diff --git a/public/app/features/panel/panellinks/link_srv.ts b/public/app/features/panel/panellinks/link_srv.ts index 38e6a19e422..d34214efec5 100644 --- a/public/app/features/panel/panellinks/link_srv.ts +++ b/public/app/features/panel/panellinks/link_srv.ts @@ -3,15 +3,8 @@ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; 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, ScopedVars, VariableOrigin } from '@grafana/ui'; -import { TimeSeriesValue, DateTime, dateTime, DataLink, KeyValue, deprecationWarning } from '@grafana/data'; - -export const DataLinkBuiltInVars = { - keepTime: '__url_time_range', - includeVars: '__all_variables', - seriesName: '__series_name', - valueTime: '__value_time', -}; +import { VariableSuggestion, ScopedVars, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui'; +import { DataLink, KeyValue, deprecationWarning, LinkModel } from '@grafana/data'; export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [ ...templateSrv.variables.map(variable => ({ @@ -44,22 +37,17 @@ export const getDataLinksVariableSuggestions = (): VariableSuggestion[] => [ }, ]; -type LinkTarget = '_blank' | '_self'; +export const getCalculationValueDataLinksVariableSuggestions = (): VariableSuggestion[] => [ + ...getPanelLinksVariableSuggestions(), + { + value: `${DataLinkBuiltInVars.seriesName}`, + documentation: 'Adds series name', + origin: VariableOrigin.BuiltIn, + }, +]; -export interface LinkModel { - href: string; - title: string; - target: LinkTarget; -} - -interface LinkDataPoint { - datapoint: TimeSeriesValue[]; - seriesName: string; - [key: number]: any; -} export interface LinkService { - getDataLinkUIModel: (link: DataLink, scopedVars: ScopedVars, dataPoint?: LinkDataPoint) => LinkModel; - getDataPointVars: (seriesName: string, dataPointTs: DateTime) => ScopedVars; + getDataLinkUIModel: (link: DataLink, scopedVars: ScopedVars, origin: T) => LinkModel; } export class LinkSrv implements LinkService { @@ -90,33 +78,20 @@ export class LinkSrv implements LinkService { return info; } - getDataPointVars = (seriesName: string, valueTime: DateTime) => { - return { - [DataLinkBuiltInVars.valueTime]: { - text: valueTime.valueOf(), - value: valueTime.valueOf(), - }, - [DataLinkBuiltInVars.seriesName]: { - text: seriesName, - value: seriesName, - }, - }; - }; - - getDataLinkUIModel = (link: DataLink, scopedVars: ScopedVars, dataPoint?: LinkDataPoint) => { + getDataLinkUIModel = (link: DataLink, scopedVars: ScopedVars, origin: T) => { const params: KeyValue = {}; const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl()); - const info: LinkModel = { + const info: LinkModel = { href: link.url, 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, { ...scopedVars, [DataLinkBuiltInVars.keepTime]: { @@ -129,13 +104,6 @@ export class LinkSrv implements LinkService { }, }); - if (dataPoint) { - info.href = this.templateSrv.replace( - info.href, - this.getDataPointVars(dataPoint.seriesName, dateTime(dataPoint.datapoint[0])) - ); - } - return info; }; @@ -146,7 +114,7 @@ export class LinkSrv implements LinkService { */ getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) { deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel'); - return this.getDataLinkUIModel(link, scopedVars); + return this.getDataLinkUIModel(link, scopedVars, {}); } } diff --git a/public/app/features/panel/panellinks/specs/link_srv.test.ts b/public/app/features/panel/panellinks/specs/link_srv.test.ts index 4cdf65dffd6..3320d3aa128 100644 --- a/public/app/features/panel/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panel/panellinks/specs/link_srv.test.ts @@ -1,4 +1,5 @@ -import { LinkSrv, DataLinkBuiltInVars } from '../link_srv'; +import { LinkSrv } from '../link_srv'; +import { DataLinkBuiltInVars } from '@grafana/ui'; import _ from 'lodash'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -80,6 +81,7 @@ describe('linkSrv', () => { title: 'Any title', url: `/d/1?$${DataLinkBuiltInVars.keepTime}`, }, + {}, {} ).href ).toEqual('/d/1?from=now-1h&to=now'); @@ -92,32 +94,43 @@ describe('linkSrv', () => { title: 'Any title', url: `/d/1?$${DataLinkBuiltInVars.includeVars}`, }, + {}, {} ).href ).toEqual('/d/1?var-test1=val1&var-test2=val2'); }); - it('should interpolate series name from datapoint', () => { + it('should interpolate series name', () => { expect( linkSrv.getDataLinkUIModel( { title: 'Any title', url: `/d/1?var-test=$${DataLinkBuiltInVars.seriesName}`, }, - {}, - dataPointMock + { + [DataLinkBuiltInVars.seriesName]: { + value: 'A-series', + text: 'A-series', + }, + }, + {} ).href ).toEqual('/d/1?var-test=A-series'); }); - it('should interpolate time range based on datapoint timestamp', () => { + it('should interpolate value time', () => { expect( linkSrv.getDataLinkUIModel( { title: 'Any title', url: `/d/1?time=$${DataLinkBuiltInVars.valueTime}`, }, - {}, - dataPointMock + { + [DataLinkBuiltInVars.valueTime]: { + value: dataPointMock.datapoint[0], + text: dataPointMock.datapoint[0], + }, + }, + {} ).href ).toEqual('/d/1?time=1000000001'); }); diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 87a8e735a89..86b3dce9550 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -5,11 +5,12 @@ import React, { PureComponent } from 'react'; import { config } from 'app/core/config'; // Components -import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay } from '@grafana/ui'; +import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay, DataLinksContextMenu } from '@grafana/ui'; // Types import { BarGaugeOptions } from './types'; import { PanelProps } from '@grafana/ui'; +import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; export class BarGaugePanel extends PureComponent> { renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => { @@ -17,18 +18,26 @@ export class BarGaugePanel extends PureComponent> { const { field, display } = value; return ( - + + {({ openMenu, targetClassName }) => { + return ( + + ); + }} + ); }; diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx index 134a48a021c..96a72e253b8 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx @@ -12,11 +12,16 @@ import { FormLabel, PanelEditorProps, Select, + DataLinksEditor, } from '@grafana/ui'; -import { FieldConfig } from '@grafana/data'; +import { FieldConfig, DataLink } from '@grafana/data'; import { Threshold, ValueMapping } from '@grafana/data'; import { BarGaugeOptions, orientationOptions, displayModes } from './types'; +import { + getDataLinksVariableSuggestions, + getCalculationValueDataLinksVariableSuggestions, +} from 'app/features/panel/panellinks/link_srv'; export class BarGaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => { @@ -51,11 +56,20 @@ export class BarGaugePanelEditor extends PureComponent this.props.onOptionsChange({ ...this.props.options, orientation: value }); onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value }); + onDataLinksChanged = (links: DataLink[]) => { + this.onDefaultsChange({ + ...this.props.options.fieldOptions.defaults, + links, + }); + }; render() { const { options } = this.props; const { fieldOptions } = options; const { defaults } = fieldOptions; + const suggestions = fieldOptions.values + ? getDataLinksVariableSuggestions() + : getCalculationValueDataLinksVariableSuggestions(); const labelWidth = 6; return ( @@ -92,6 +106,15 @@ export class BarGaugePanelEditor extends PureComponent + + + + ); } diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 1e1c02c3755..5bdb2de6d71 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -5,11 +5,12 @@ import React, { PureComponent } from 'react'; import { config } from 'app/core/config'; // Components -import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation } from '@grafana/ui'; +import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation, DataLinksContextMenu } from '@grafana/ui'; // Types import { GaugeOptions } from './types'; import { PanelProps, VizRepeater } from '@grafana/ui'; +import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; export class GaugePanel extends PureComponent> { renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => { @@ -17,17 +18,25 @@ export class GaugePanel extends PureComponent> { const { field, display } = value; return ( - + + {({ openMenu, targetClassName }) => { + return ( + + ); + }} + ); }; diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx index 1ed9bd4a506..f5514087b9a 100644 --- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -10,10 +10,15 @@ import { FieldPropertiesEditor, Switch, PanelOptionsGroup, + DataLinksEditor, } from '@grafana/ui'; -import { Threshold, ValueMapping, FieldConfig } from '@grafana/data'; +import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data'; import { GaugeOptions } from './types'; +import { + getCalculationValueDataLinksVariableSuggestions, + getDataLinksVariableSuggestions, +} from 'app/features/panel/panellinks/link_srv'; export class GaugePanelEditor extends PureComponent> { labelWidth = 6; @@ -56,10 +61,20 @@ export class GaugePanelEditor extends PureComponent { + this.onDefaultsChange({ + ...this.props.options.fieldOptions.defaults, + links, + }); + }; + render() { const { options } = this.props; const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options; const { defaults } = fieldOptions; + const suggestions = fieldOptions.values + ? getDataLinksVariableSuggestions() + : getCalculationValueDataLinksVariableSuggestions(); return ( <> @@ -92,6 +107,15 @@ export class GaugePanelEditor extends PureComponent + + + + ); } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index fe8e898bec4..ed5b965b8f2 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -25,7 +25,7 @@ import ReactDOM from 'react-dom'; import { GraphLegendProps, Legend } from './Legend/Legend'; import { GraphCtrl } from './module'; -import { getValueFormat, ContextMenuItem, ContextMenuGroup } from '@grafana/ui'; +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'; @@ -196,10 +196,15 @@ class GraphElement { { items: [ ...dataLinks.map(link => { - const linkUiModel = this.linkSrv.getDataLinkUIModel(link, this.panel.scopedVars, { - seriesName: item.series.alias, - datapoint: item.datapoint, - }); + 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, diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index fa565cdb889..2ba1c725014 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -24,9 +24,10 @@ import { DisplayValue, fieldReducers, KeyValue, + LinkModel, } from '@grafana/data'; import { auto } from 'angular'; -import { LinkSrv, LinkModel } from 'app/features/panel/panellinks/link_srv'; +import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner'; import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState'; @@ -328,7 +329,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { const $sanitize = this.$sanitize; const panel = ctrl.panel; const templateSrv = this.templateSrv; - let linkInfo: LinkModel | null = null; + let linkInfo: LinkModel | null = null; const $panelContainer = elem.find('.panel-container'); elem = elem.find('.singlestat-panel'); @@ -592,7 +593,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { elem.toggleClass('pointer', panel.links.length > 0); if (panel.links.length > 0) { - linkInfo = linkSrv.getDataLinkUIModel(panel.links[0], data.scopedVars); + linkInfo = linkSrv.getDataLinkUIModel(panel.links[0], data.scopedVars, {}); } else { linkInfo = null; } diff --git a/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx index c6ae577184b..68b0b1c2c03 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx @@ -9,13 +9,18 @@ import { FieldDisplayEditor, FieldPropertiesEditor, PanelOptionsGroup, + DataLinksEditor, } from '@grafana/ui'; -import { Threshold, ValueMapping, FieldConfig } from '@grafana/data'; +import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data'; import { SingleStatOptions, SparklineOptions } from './types'; import { ColoringEditor } from './ColoringEditor'; import { FontSizeEditor } from './FontSizeEditor'; import { SparklineEditor } from './SparklineEditor'; +import { + getDataLinksVariableSuggestions, + getCalculationValueDataLinksVariableSuggestions, +} from 'app/features/panel/panellinks/link_srv'; export class SingleStatEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => { @@ -53,10 +58,20 @@ export class SingleStatEditor extends PureComponent { + this.onDefaultsChange({ + ...this.props.options.fieldOptions.defaults, + links, + }); + }; + render() { const { options } = this.props; const { fieldOptions } = options; const { defaults } = fieldOptions; + const suggestions = fieldOptions.values + ? getDataLinksVariableSuggestions() + : getCalculationValueDataLinksVariableSuggestions(); return ( <> @@ -77,6 +92,15 @@ export class SingleStatEditor extends PureComponent + + + + ); } diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx index cc11a535ca1..8dafa1315f8 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx @@ -6,8 +6,16 @@ import { config } from 'app/core/config'; // Types import { SingleStatOptions } from './types'; -import { PanelProps, getFieldDisplayValues, VizRepeater, FieldDisplay, BigValue } from '@grafana/ui'; +import { + PanelProps, + getFieldDisplayValues, + VizRepeater, + FieldDisplay, + BigValue, + DataLinksContextMenu, +} from '@grafana/ui'; import { BigValueSparkline } from '@grafana/ui/src/components/BigValue/BigValue'; +import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; export class SingleStatPanel extends PureComponent> { renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => { @@ -23,7 +31,23 @@ export class SingleStatPanel extends PureComponent }; } - return ; + return ( + + {({ openMenu, targetClassName }) => { + return ( + + ); + }} + + ); }; getValues = (): FieldDisplay[] => {