diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 55b1c13af06..30f7ca60b03 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -1,11 +1,32 @@ +import { ScopedVars } from './ScopedVars'; + +/** + * Callback info for DataLink click events + */ +export interface DataLinkClickEvent { + origin: T; + scopedVars: ScopedVars; + e?: any; // mouse|react event +} + /** * Link configuration. The values may contain variables that need to be * processed before running */ export interface DataLink { - url: string; title: string; targetBlank?: boolean; + + // 3: The URL if others did not set it first + url: string; + + // 2: If exists, use this to construct the URL + // Not saved in JSON/DTO + onBuildUrl?: (event: DataLinkClickEvent) => string; + + // 1: If exists, handle click directly + // Not saved in JSON/DTO + onClick?: (event: DataLinkClickEvent) => void; } export type LinkTarget = '_blank' | '_self'; @@ -18,6 +39,9 @@ export interface LinkModel { title: string; target: LinkTarget; origin: T; + + // When a click callback exists, this is passed the raw mouse|react event + onClick?: (e: any) => void; } /** diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index c2f35bfd064..7d0b304f4b0 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -189,7 +189,7 @@ export const ContextMenu: React.FC = React.memo(({ x, y, onClo renderItem={(item, index) => { return ( <> - + ); }} @@ -215,7 +215,7 @@ const ContextMenuItem: React.FC = React.memo( return (
{ @@ -233,10 +233,10 @@ const ContextMenuItem: React.FC = React.memo( interface ContextMenuGroupProps { group: ContextMenuGroup; - onItemClick?: () => void; + onClick?: () => void; // Used with 'onClose' } -const ContextMenuGroup: React.FC = ({ group, onItemClick }) => { +const ContextMenuGroup: React.FC = ({ group, onClick }) => { const theme = useContext(ThemeContext); const styles = getContextMenuStyles(theme); @@ -261,8 +261,9 @@ const ContextMenuGroup: React.FC = ({ group, onItemClick item.onClick(e); } - if (onItemClick) { - onItemClick(); + // Typically closes the context menu + if (onClick) { + onClick(); } }} /> diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts index 74f22ed4885..26b4f3917ad 100644 --- a/packages/grafana-ui/src/utils/dataLinks.ts +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -27,6 +27,7 @@ export const linkModelToContextMenuItems: (links: LinkModelSupplier) => Con url: link.href, target: link.target, icon: `fa ${link.target === '_self' ? 'fa-link' : 'fa-external-link'}`, + onClick: link.onClick, }; }); }; diff --git a/public/app/features/panel/panellinks/link_srv.ts b/public/app/features/panel/panellinks/link_srv.ts index de53df74825..3be12a75621 100644 --- a/public/app/features/panel/panellinks/link_srv.ts +++ b/public/app/features/panel/panellinks/link_srv.ts @@ -156,11 +156,31 @@ export class LinkSrv implements LinkService { const params: KeyValue = {}; const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl()); + let href = link.url; + if (link.onBuildUrl) { + href = link.onBuildUrl({ + origin, + scopedVars, + }); + } + + let onClick: (e: any) => void = undefined; + if (link.onClick) { + onClick = (e: any) => { + link.onClick({ + origin, + scopedVars, + e, + }); + }; + } + const info: LinkModel = { - href: link.url.replace(/\s|\n/g, ''), + href: href.replace(/\s|\n/g, ''), title: this.templateSrv.replace(link.title || '', scopedVars), target: link.targetBlank ? '_blank' : '_self', origin, + onClick, }; this.templateSrv.fillVariableValuesForUrl(params, scopedVars); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index dd3ea38883a..a4dc1f71d00 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -214,6 +214,7 @@ class GraphElement { url: link.href, target: link.target, icon: `fa ${link.target === '_self' ? 'fa-link' : 'fa-external-link'}`, + onClick: link.onClick, }; }), }, @@ -248,19 +249,23 @@ class GraphElement { 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]; + let links = this.panel.options.dataLinks || []; + if (field.config.links && field.config.links.length) { + // Append the configured links to the panel datalinks + links = [...links, ...field.config.links]; + } + const fieldConfig = { + decimals: yAxisConfig.decimals, + links, + }; const fieldDisplay = getDisplayProcessor({ config: fieldConfig, theme: getCurrentTheme(), })(field.values.get(item.dataIndex)); - - linksSupplier = this.panel.options.dataLinks + linksSupplier = links.length ? getFieldLinksSupplier({ display: fieldDisplay, name: field.name,