import { chain } from 'lodash'; import { DataFrame, DataLink, DataLinkBuiltInVars, deprecationWarning, Field, FieldType, getFieldDisplayName, InterpolateFunction, KeyValue, LinkModel, locationUtil, ScopedVars, textUtil, urlUtil, VariableOrigin, VariableSuggestion, VariableSuggestionsScope, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { getConfig } from 'app/core/config'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl'; const timeRangeVars = [ { value: `${DataLinkBuiltInVars.keepTime}`, label: 'Time range', documentation: 'Adds current time range', origin: VariableOrigin.BuiltIn, }, { value: `${DataLinkBuiltInVars.timeRangeFrom}`, label: 'Time range: from', documentation: "Adds current time range's from value", origin: VariableOrigin.BuiltIn, }, { value: `${DataLinkBuiltInVars.timeRangeTo}`, label: 'Time range: to', documentation: "Adds current time range's to value", origin: VariableOrigin.BuiltIn, }, ]; const seriesVars = [ { value: `${DataLinkBuiltInVars.seriesName}`, label: 'Name', documentation: 'Name of the series', origin: VariableOrigin.Series, }, ]; const valueVars = [ { value: `${DataLinkBuiltInVars.valueNumeric}`, label: 'Numeric', documentation: 'Numeric representation of selected value', origin: VariableOrigin.Value, }, { value: `${DataLinkBuiltInVars.valueText}`, label: 'Text', documentation: 'Text representation of selected value', origin: VariableOrigin.Value, }, { value: `${DataLinkBuiltInVars.valueRaw}`, label: 'Raw', documentation: 'Raw value', origin: VariableOrigin.Value, }, ]; const buildLabelPath = (label: string) => { return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`; }; export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [ ...getTemplateSrv() .getVariables() .map((variable) => ({ value: variable.name as string, label: variable.name, origin: VariableOrigin.Template, })), { value: `${DataLinkBuiltInVars.includeVars}`, label: 'All variables', documentation: 'Adds current variables', origin: VariableOrigin.Template, }, ...timeRangeVars, ]; const getFieldVars = (dataFrames: DataFrame[]) => { const all = []; for (const df of dataFrames) { for (const f of df.fields) { if (f.labels) { for (const k of Object.keys(f.labels)) { all.push(k); } } } } const labels = chain(all).flatten().uniq().value(); return [ { value: `${DataLinkBuiltInVars.fieldName}`, label: 'Name', documentation: 'Field name of the clicked datapoint (in ms epoch)', origin: VariableOrigin.Field, }, ...labels.map((label) => ({ value: `__field.labels${buildLabelPath(label)}`, label: `labels.${label}`, documentation: `${label} label value`, origin: VariableOrigin.Field, })), ]; }; export const getDataFrameVars = (dataFrames: DataFrame[]) => { let numeric: Field | undefined = undefined; let title: Field | undefined = undefined; const suggestions: VariableSuggestion[] = []; const keys: KeyValue = {}; if (dataFrames.length !== 1) { // It's not possible to access fields of other dataframes. So if there are multiple dataframes we need to skip these suggestions. // Also return early if there are no dataFrames. return []; } const frame = dataFrames[0]; for (const field of frame.fields) { const displayName = getFieldDisplayName(field, frame, dataFrames); if (keys[displayName]) { continue; } suggestions.push({ value: `__data.fields${buildLabelPath(displayName)}`, label: `${displayName}`, documentation: `Formatted value for ${displayName} on the same row`, origin: VariableOrigin.Fields, }); keys[displayName] = true; if (!numeric && field.type === FieldType.number) { numeric = { ...field, name: displayName }; } if (!title && field.config.displayName && field.config.displayName !== field.name) { title = { ...field, name: displayName }; } } if (suggestions.length) { suggestions.push({ value: `__data.fields[0]`, label: `Select by index`, documentation: `Enter the field order`, origin: VariableOrigin.Fields, }); } if (numeric) { suggestions.push({ value: `__data.fields${buildLabelPath(numeric.name)}.numeric`, label: `Show numeric value`, documentation: `the numeric field value`, origin: VariableOrigin.Fields, }); suggestions.push({ value: `__data.fields${buildLabelPath(numeric.name)}.text`, label: `Show text value`, documentation: `the text value`, origin: VariableOrigin.Fields, }); } if (title) { suggestions.push({ value: `__data.fields${buildLabelPath(title.name)}`, label: `Select by title`, documentation: `Use the title to pick the field`, origin: VariableOrigin.Fields, }); } return suggestions; }; export const getDataLinksVariableSuggestions = ( dataFrames: DataFrame[], scope?: VariableSuggestionsScope ): VariableSuggestion[] => { const valueTimeVar = { value: `${DataLinkBuiltInVars.valueTime}`, label: 'Time', documentation: 'Time value of the clicked datapoint (in ms epoch)', origin: VariableOrigin.Value, }; const includeValueVars = scope === VariableSuggestionsScope.Values; return includeValueVars ? [ ...seriesVars, ...getFieldVars(dataFrames), ...valueVars, valueTimeVar, ...getDataFrameVars(dataFrames), ...getPanelLinksVariableSuggestions(), ] : [ ...seriesVars, ...getFieldVars(dataFrames), ...getDataFrameVars(dataFrames), ...getPanelLinksVariableSuggestions(), ]; }; export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => { const fieldVars = getFieldVars(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: (link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: T) => LinkModel; getAnchorInfo: (link: any) => any; getLinkUrl: (link: any) => string; } export class LinkSrv implements LinkService { getLinkUrl(link: any) { let url = locationUtil.assureBaseUrl(getTemplateSrv().replace(link.url || '')); let params: { [key: string]: any } = {}; if (link.keepTime) { const range = getTimeSrv().timeRangeForUrl(); params['from'] = range.from; params['to'] = range.to; } if (link.includeVars) { params = { ...params, ...getVariablesUrlParams(), }; } url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params)); return getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url); } getAnchorInfo(link: any) { const templateSrv = getTemplateSrv(); const info: any = {}; info.href = this.getLinkUrl(link); info.title = templateSrv.replace(link.title || ''); info.tooltip = templateSrv.replace(link.tooltip || ''); return info; } /** * Returns LinkModel which is basically a DataLink with all values interpolated through the templateSrv. */ getDataLinkUIModel = ( link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: T ): LinkModel => { let href = link.url; if (link.onBuildUrl) { href = link.onBuildUrl({ origin, replaceVariables, }); } const info: LinkModel = { href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')), title: link.title ?? '', target: link.targetBlank ? '_blank' : undefined, origin, }; if (replaceVariables) { info.href = replaceVariables(info.href); info.title = replaceVariables(link.title); } if (link.onClick) { info.onClick = (e) => { link.onClick!({ origin, replaceVariables, e, }); }; } info.href = getConfig().disableSanitizeHtml ? info.href : textUtil.sanitizeUrl(info.href); return info; }; /** * getPanelLinkAnchorInfo method is left for plugins compatibility reasons * * @deprecated Drilldown links should be generated using getDataLinkUIModel method */ getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) { deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel'); const replace: InterpolateFunction = (value, vars, fmt) => getTemplateSrv().replace(value, { ...scopedVars, ...vars }, fmt); return this.getDataLinkUIModel(link, replace, {}); } } let singleton: LinkService | undefined; export function setLinkSrv(srv: LinkService) { singleton = srv; } export function getLinkSrv(): LinkService { if (!singleton) { singleton = new LinkSrv(); } return singleton; }