diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index d6aee5a2442..ff9685f1ee4 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -27,6 +27,7 @@ import { fieldIndexComparer } from '../field/fieldComparers'; function convertTableToDataFrame(table: TableData): DataFrame { const fields = table.columns.map(c => { + // TODO: should be Column but type does not exists there so not sure whats up here. const { text, type, ...disp } = c as any; return { name: text, // rename 'text' to the 'name' field diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 4d5fd1afbcc..ab75cbcac65 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -118,6 +118,7 @@ describe('applyFieldOverrides', () => { overrides: [], }, replaceVariables: (value: any) => value, + getDataSourceSettingsByUid: undefined as any, theme: {} as GrafanaTheme, fieldConfigRegistry: new FieldConfigOptionsRegistry(), }); @@ -187,6 +188,7 @@ describe('applyFieldOverrides', () => { overrides: [], }, fieldConfigRegistry: customFieldRegistry, + getDataSourceSettingsByUid: undefined as any, replaceVariables: v => v, theme: {} as GrafanaTheme, })[0]; @@ -204,6 +206,7 @@ describe('applyFieldOverrides', () => { data: [f0], // the frame fieldConfig: src as FieldConfigSource, // defaults + overrides replaceVariables: (undefined as any) as InterpolateFunction, + getDataSourceSettingsByUid: undefined as any, theme: (undefined as any) as GrafanaTheme, fieldConfigRegistry: customFieldRegistry, })[0]; @@ -231,6 +234,7 @@ describe('applyFieldOverrides', () => { data: [f0], // the frame fieldConfig: src as FieldConfigSource, // defaults + overrides replaceVariables: (undefined as any) as InterpolateFunction, + getDataSourceSettingsByUid: undefined as any, theme: (undefined as any) as GrafanaTheme, autoMinMax: true, })[0]; @@ -478,11 +482,72 @@ describe('getLinksSupplier', () => { }); const replaceSpy = jest.fn(); - const supplier = getLinksSupplier(f0, f0.fields[0], {}, replaceSpy, { theme: {} as GrafanaTheme }); + const supplier = getLinksSupplier( + f0, + f0.fields[0], + {}, + replaceSpy, + // this is used only for internal links so isn't needed here + () => ({} as any), + { + theme: {} as GrafanaTheme, + } + ); supplier({}); expect(replaceSpy).toBeCalledTimes(2); expect(replaceSpy.mock.calls[0][0]).toEqual('url to be interpolated'); expect(replaceSpy.mock.calls[1][0]).toEqual('title to be interpolated'); }); + + it('handles internal links', () => { + locationUtil.initialize({ + getConfig: () => ({ appSubUrl: '' } as any), + buildParamsFromVariables: (() => {}) as any, + getTimeRangeForUrl: (() => {}) as any, + }); + + const f0 = new MutableDataFrame({ + name: 'A', + fields: [ + { + name: 'message', + type: FieldType.string, + values: [10, 20], + config: { + links: [ + { + url: '', + title: '', + internal: { + datasourceUid: '0', + query: '12345', + }, + }, + ], + }, + }, + ], + }); + + const supplier = getLinksSupplier( + f0, + f0.fields[0], + {}, + // We do not need to interpolate anything for this test + (value, vars, format) => value, + uid => ({ name: 'testDS' } as any), + { theme: {} as GrafanaTheme } + ); + const links = supplier({ valueRowIndex: 0 }); + expect(links.length).toBe(1); + expect(links[0]).toEqual( + expect.objectContaining({ + title: 'testDS', + href: + '/explore?left={"datasource":"testDS","queries":["12345"],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}', + onClick: undefined, + }) + ); + }); }); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index c717d5dabb0..2bf3e09ef21 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -16,6 +16,8 @@ import { ValueLinkConfig, GrafanaTheme, TimeZone, + DataLink, + DataSourceInstanceSettings, } from '../types'; import { fieldMatchers, ReducerID, reduceField } from '../transformations'; import { FieldMatcher } from '../types/transformations'; @@ -33,6 +35,7 @@ import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; import { formatLabels } from '../utils/labels'; import { getFrameDisplayName, getFieldDisplayName } from './fieldState'; import { getTimeField } from '../dataframe/processDataFrame'; +import { mapInternalLinkToExplore } from '../utils/dataLinks'; interface OverrideProps { match: FieldMatcher; @@ -129,6 +132,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra data: options.data!, dataFrameIndex: index, replaceVariables: options.replaceVariables, + getDataSourceSettingsByUid: options.getDataSourceSettingsByUid, fieldConfigRegistry: fieldConfigRegistry, }; @@ -206,10 +210,17 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra }); // Attach data links supplier - f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, { - theme: options.theme, - timeZone: options.timeZone, - }); + f.getLinks = getLinksSupplier( + frame, + f, + fieldScopedVars, + context.replaceVariables, + context.getDataSourceSettingsByUid, + { + theme: options.theme, + timeZone: options.timeZone, + } + ); return f; }); @@ -348,6 +359,7 @@ export const getLinksSupplier = ( field: Field, fieldScopedVars: ScopedVars, replaceVariables: InterpolateFunction, + getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined, options: { theme: GrafanaTheme; timeZone?: TimeZone; @@ -359,20 +371,11 @@ export const getLinksSupplier = ( const timeRangeUrl = locationUtil.getTimeRangeUrlParams(); const { timeField } = getTimeField(frame); - return field.config.links.map(link => { - let href = link.url; + return field.config.links.map((link: DataLink) => { + const variablesQuery = locationUtil.getVariablesUrlParams(); let dataFrameVars = {}; let valueVars = {}; - const info: LinkModel = { - href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')), - title: link.title || '', - target: link.targetBlank ? '_blank' : undefined, - origin: field, - }; - - const variablesQuery = locationUtil.getVariablesUrlParams(); - // We are not displaying reduction result if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) { const fieldsProxy = getFieldDisplayValuesProxy(frame, config.valueRowIndex, options); @@ -419,10 +422,25 @@ export const getLinksSupplier = ( }, }; - info.href = replaceVariables(info.href, variables); - info.title = replaceVariables(info.title, variables); - info.href = locationUtil.processUrl(info.href); + if (link.internal) { + // For internal links at the moment only destination is Explore. + return mapInternalLinkToExplore(link, variables, {} as any, field, { + replaceVariables, + getDataSourceSettingsByUid, + }); + } else { + let href = locationUtil.assureBaseUrl(link.url.replace(/\n/g, '')); + href = replaceVariables(href, variables); + href = locationUtil.processUrl(href); - return info; + const info: LinkModel = { + href, + title: replaceVariables(link.title || '', variables), + target: link.targetBlank ? '_blank' : undefined, + origin: field, + }; + + return info; + } }); }; diff --git a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx index 680d505433c..f4e746eee82 100644 --- a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx +++ b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx @@ -28,6 +28,7 @@ describe('getFieldDisplayValuesProxy', () => { overrides: [], }, replaceVariables: (val: string) => val, + getDataSourceSettingsByUid: (val: string) => ({} as any), timeZone: 'utc', theme: {} as GrafanaTheme, autoMinMax: true, diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index bba9fea9dca..09484215977 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -1,4 +1,5 @@ import { ScopedVars } from './ScopedVars'; +import { DataQuery } from './datasource'; /** * Callback info for DataLink click events @@ -10,10 +11,13 @@ export interface DataLinkClickEvent { } /** - * Link configuration. The values may contain variables that need to be - * processed before running + * Link configuration. The values may contain variables that need to be + * processed before showing the link to user. + * + * TODO: is not strictly true for internal links as we do not need refId for example but all + * data source defined queries extend this so this is more for documentation. */ -export interface DataLink { +export interface DataLink { title: string; targetBlank?: boolean; @@ -28,16 +32,19 @@ export interface DataLink { // Not saved in JSON/DTO onClick?: (event: DataLinkClickEvent) => void; - // At the moment this is used for derived fields for metadata about internal linking. - meta?: { - datasourceUid?: string; + // If dataLink represents internal link this has to be filled. Internal link is defined as a query in a particular + // datas ource that we want to show to the user. Usually this results in a link to explore but can also lead to + // more custom onClick behaviour if needed. + internal?: { + query: T; + datasourceUid: string; }; } export type LinkTarget = '_blank' | '_self' | undefined; /** - * Processed Link Model. The values are ready to use + * Processed Link Model. The values are ready to use */ export interface LinkModel { href: string; diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts new file mode 100644 index 00000000000..04812ab435a --- /dev/null +++ b/packages/grafana-data/src/types/explore.ts @@ -0,0 +1,22 @@ +import { ExploreMode } from './datasource'; +import { RawTimeRange } from './time'; +import { LogsDedupStrategy } from './logs'; + +/** @internal */ +export interface ExploreUrlState { + datasource: string; + queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense + mode: ExploreMode; + range: RawTimeRange; + ui: ExploreUIState; + originPanelId?: number; + context?: string; +} + +/** @internal */ +export interface ExploreUIState { + showingTable: boolean; + showingGraph: boolean; + showingLogs: boolean; + dedupStrategy?: LogsDedupStrategy; +} diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index 44f60d3bb6c..02ab152d301 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -1,5 +1,13 @@ import { ComponentType } from 'react'; -import { MatcherConfig, FieldConfig, Field, DataFrame, GrafanaTheme, TimeZone } from '../types'; +import { + MatcherConfig, + FieldConfig, + Field, + DataFrame, + GrafanaTheme, + TimeZone, + DataSourceInstanceSettings, +} from '../types'; import { InterpolateFunction } from './panel'; import { StandardEditorProps, FieldConfigOptionsRegistry, StandardEditorContext } from '../field'; import { OptionsEditorItem } from './OptionsUIRegistryBuilder'; @@ -106,6 +114,7 @@ export interface ApplyFieldOverrideOptions { data?: DataFrame[]; fieldConfig: FieldConfigSource; replaceVariables: InterpolateFunction; + getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined; theme: GrafanaTheme; timeZone?: TimeZone; autoMinMax?: boolean; diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 030afa62e6e..35d02d1f91e 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -25,6 +25,7 @@ export * from './theme'; export * from './orgs'; export * from './flot'; export * from './trace'; +export * from './explore'; import * as AppEvents from './appEvents'; import { AppEvent } from './appEvents'; diff --git a/packages/grafana-data/src/utils/dataLinks.test.ts b/packages/grafana-data/src/utils/dataLinks.test.ts new file mode 100644 index 00000000000..08b27d21567 --- /dev/null +++ b/packages/grafana-data/src/utils/dataLinks.test.ts @@ -0,0 +1,39 @@ +import { mapInternalLinkToExplore } from './dataLinks'; +import { FieldType } from '../types'; +import { ArrayVector } from '../vector'; + +describe('mapInternalLinkToExplore', () => { + it('creates internal link', () => { + const link = mapInternalLinkToExplore( + { + url: '', + title: '', + internal: { + datasourceUid: 'uid', + query: { query: '12344' }, + }, + }, + {}, + {} as any, + { + name: 'test', + type: FieldType.number, + config: {}, + values: new ArrayVector([2]), + }, + { + replaceVariables: val => val, + getDataSourceSettingsByUid: uid => ({ name: 'testDS' } as any), + } + ); + + expect(link).toEqual( + expect.objectContaining({ + title: 'testDS', + href: + '/explore?left={"datasource":"testDS","queries":[{"query":"12344"}],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}', + onClick: undefined, + }) + ); + }); +}); diff --git a/packages/grafana-data/src/utils/dataLinks.ts b/packages/grafana-data/src/utils/dataLinks.ts index 70a9b72df10..5921b739ec1 100644 --- a/packages/grafana-data/src/utils/dataLinks.ts +++ b/packages/grafana-data/src/utils/dataLinks.ts @@ -1,3 +1,17 @@ +import { + DataLink, + DataQuery, + DataSourceInstanceSettings, + ExploreMode, + Field, + InterpolateFunction, + LinkModel, + ScopedVars, + TimeRange, +} from '../types'; +import { locationUtil } from './location'; +import { serializeStateToUrlParam } from './url'; + export const DataLinkBuiltInVars = { keepTime: '__url_time_range', timeRangeFrom: '__from', @@ -12,3 +26,96 @@ export const DataLinkBuiltInVars = { // name of the calculation represented by the value valueCalc: '__value.calc', }; + +type Options = { + onClickFn?: (options: { datasourceUid: string; query: any }) => void; + replaceVariables: InterpolateFunction; + getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined; +}; + +export function mapInternalLinkToExplore( + link: DataLink, + scopedVars: ScopedVars, + range: TimeRange, + field: Field, + options: Options +): LinkModel { + if (!link.internal) { + throw new Error('Trying to map external link as internal'); + } + const { onClickFn, replaceVariables, getDataSourceSettingsByUid } = options; + + const interpolatedQuery = interpolateQuery(link, scopedVars, replaceVariables); + return { + title: link.title + ? replaceVariables(link.title || '', scopedVars) + : getDataSourceSettingsByUid(link.internal.datasourceUid)?.name || 'Unknown datasource', + + // In this case this is meant to be internal link (opens split view by default) the href will also points + // to explore but this way you can open it in new tab. + href: generateInternalHref( + getDataSourceSettingsByUid(link.internal.datasourceUid)?.name || 'unknown', + interpolatedQuery, + range + ), + onClick: onClickFn + ? () => { + onClickFn?.({ + datasourceUid: link.internal!.datasourceUid, + query: interpolatedQuery, + }); + } + : undefined, + target: '_self', + origin: field, + }; +} + +/** + * Generates href for internal derived field link. + */ +function generateInternalHref(datasourceName: string, query: T, range: TimeRange): string { + return locationUtil.assureBaseUrl( + `/explore?left=${serializeStateToUrlParam({ + range: range.raw, + datasource: datasourceName, + queries: [query], + // This should get overwritten if datasource does not support that mode and we do not know what mode is + // preferred anyway. + mode: ExploreMode.Metrics, + ui: { + showingGraph: true, + showingTable: true, + showingLogs: true, + }, + })}` + ); +} + +function interpolateQuery( + link: DataLink, + scopedVars: ScopedVars, + replaceVariables: InterpolateFunction +): T { + let stringifiedQuery = ''; + try { + stringifiedQuery = JSON.stringify(link.internal?.query || ''); + } catch (err) { + // should not happen and not much to do about this, possibly something non stringifiable in the query + console.error(err); + } + + // Replace any variables inside the query. This may not be the safest as it can also replace keys etc so may not + // actually work with every datasource query right now. + stringifiedQuery = replaceVariables(stringifiedQuery, scopedVars); + + let replacedQuery = {} as T; + try { + replacedQuery = JSON.parse(stringifiedQuery); + } catch (err) { + // again should not happen and not much to do about this, probably some issue with how we replaced the variables. + console.error(stringifiedQuery, err); + } + + return replacedQuery; +} diff --git a/packages/grafana-data/src/utils/url.ts b/packages/grafana-data/src/utils/url.ts index 9e7999c829b..f7a492e9619 100644 --- a/packages/grafana-data/src/utils/url.ts +++ b/packages/grafana-data/src/utils/url.ts @@ -2,6 +2,8 @@ * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT */ +import { ExploreUrlState } from '../types/explore'; + /** * Type to represent the value of a single query variable. * @@ -129,3 +131,24 @@ export const urlUtil = { appendQueryToUrl, getUrlSearchParams, }; + +export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { + if (compact) { + return JSON.stringify([ + urlState.range.from, + urlState.range.to, + urlState.datasource, + ...urlState.queries, + { mode: urlState.mode }, + { + ui: [ + !!urlState.ui.showingGraph, + !!urlState.ui.showingLogs, + !!urlState.ui.showingTable, + urlState.ui.dedupStrategy, + ], + }, + ]); + } + return JSON.stringify(urlState); +} diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index 524acc084f3..26a7f00ba69 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -1,5 +1,5 @@ import React, { ChangeEvent, useContext } from 'react'; -import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data'; +import { VariableSuggestion, GrafanaTheme, DataLink } from '@grafana/data'; import { Switch } from '../Switch/Switch'; import { css } from 'emotion'; import { ThemeContext, stylesFactory } from '../../themes/index'; diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index ad9e0d3dc30..09ad685b71b 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -20,7 +20,23 @@ export const DefaultCell: FC = props => {
{link ? ( - + { + // Allow opening in new tab + if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link!.onClick) { + event.preventDefault(); + link!.onClick(event); + } + } + : undefined + } + target={link.target} + title={link.title} + className={tableStyles.tableCellLink} + > {value} diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index b6aae20c91d..92eba4fd293 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -90,6 +90,7 @@ function buildData(theme: GrafanaTheme, config: Record): Da }, theme, replaceVariables: (value: string) => value, + getDataSourceSettingsByUid: (value: string) => ({} as any), })[0]; } diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index be3cd3664fb..97f6a59b22d 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -8,12 +8,10 @@ import { hasNonEmptyQuery, parseUrlState, refreshIntervalToSortOrder, - serializeStateToUrlParam, sortLogsResult, SortOrder, updateHistory, } from './explore'; -import { ExploreUrlState } from 'app/types/explore'; import store from 'app/core/store'; import { DataQueryError, @@ -24,8 +22,10 @@ import { LogsDedupStrategy, LogsModel, MutableDataFrame, + ExploreUrlState, } from '@grafana/data'; import { RefreshPicker } from '@grafana/ui'; +import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: '', diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 88eb327c540..05eaca48541 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -3,12 +3,14 @@ import _ from 'lodash'; import { Unsubscribable } from 'rxjs'; // Services & Utils import { - DataQuery, CoreApp, + DataQuery, DataQueryError, DataQueryRequest, DataSourceApi, dateMath, + DefaultTimeZone, + ExploreMode, HistoryItem, IntervalValues, LogRowModel, @@ -19,16 +21,15 @@ import { TimeRange, TimeZone, toUtc, - ExploreMode, urlUtil, - DefaultTimeZone, + ExploreUrlState, } from '@grafana/data'; import store from 'app/core/store'; import kbn from 'app/core/utils/kbn'; import { getNextRefIdChar } from './query'; // Types import { RefreshPicker } from '@grafana/ui'; -import { ExploreUrlState, QueryOptions, QueryTransaction } from 'app/types/explore'; +import { QueryOptions, QueryTransaction } from 'app/types/explore'; import { config } from '../config'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DataSourceSrv } from '@grafana/runtime'; @@ -260,27 +261,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { return { datasource, queries, range, ui, mode, originPanelId }; } -export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { - if (compact) { - return JSON.stringify([ - urlState.range.from, - urlState.range.to, - urlState.datasource, - ...urlState.queries, - { mode: urlState.mode }, - { - ui: [ - !!urlState.ui.showingGraph, - !!urlState.ui.showingLogs, - !!urlState.ui.showingTable, - urlState.ui.dedupStrategy, - ], - }, - ]); - } - return JSON.stringify(urlState); -} - export function generateKey(index = 0): string { return `Q-${Date.now()}-${Math.random()}-${index}`; } diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index a0bbe47d800..dc201b59dd9 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -2,14 +2,23 @@ import _ from 'lodash'; // Services & Utils -import { DataQuery, DataSourceApi, ExploreMode, dateTimeFormat, AppEvents, urlUtil } from '@grafana/data'; +import { + DataQuery, + DataSourceApi, + ExploreMode, + dateTimeFormat, + AppEvents, + urlUtil, + ExploreUrlState, +} from '@grafana/data'; import appEvents from 'app/core/app_events'; import store from 'app/core/store'; -import { serializeStateToUrlParam, SortOrder } from './explore'; +import { SortOrder } from './explore'; import { getExploreDatasources } from '../../features/explore/state/selectors'; // Types -import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore'; +import { RichHistoryQuery } from 'app/types/explore'; +import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; const RICH_HISTORY_KEY = 'grafana.explore.richHistory'; diff --git a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx index e7bc72afbcc..e05a9c533af 100644 --- a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx @@ -26,6 +26,7 @@ import { GetDataOptions } from '../../state/PanelQueryRunner'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { PanelModel } from 'app/features/dashboard/state'; import { DetailText } from './DetailText'; +import { getDatasourceSrv } from '../../../plugins/datasource_srv'; interface Props { panel: PanelModel; @@ -139,6 +140,7 @@ export class InspectDataTab extends PureComponent { replaceVariables: (value: string) => { return value; }, + getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid, }); } diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index dfde0af220d..b29e320863e 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn'; // Types import { PanelModel } from './PanelModel'; import { DashboardModel } from './DashboardModel'; -import { DataLink, DataLinkBuiltInVars, urlUtil } from '@grafana/data'; +import { DataLinkBuiltInVars, DataLink, urlUtil } from '@grafana/data'; // Constants import { DEFAULT_PANEL_SPAN, diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 7fa4600b4aa..62c85753466 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -7,9 +7,11 @@ import { standardEditorsRegistry, standardFieldConfigEditorRegistry, PanelData, + DataSourceInstanceSettings, } from '@grafana/data'; import { ComponentClass } from 'react'; import { PanelQueryRunner } from './PanelQueryRunner'; +import { setDataSourceSrv } from '@grafana/runtime'; class TablePanelCtrl {} @@ -149,6 +151,11 @@ describe('PanelModel', () => { }); it('should apply field config defaults', () => { + setDataSourceSrv({ + getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined { + return undefined; + }, + } as any); // default unit is overriden by model expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg'); // default decimals are aplied diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index e0aa0bb019a..c44ee68fa8e 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -20,6 +20,7 @@ import { import { EDIT_PANEL_ID } from 'app/core/constants'; import config from 'app/core/config'; import { PanelQueryRunner } from './PanelQueryRunner'; +import { getDatasourceSrv } from '../../plugins/datasource_srv'; export const panelAdded = eventFactory('panel-added'); export const panelRemoved = eventFactory('panel-removed'); @@ -439,6 +440,7 @@ export class PanelModel implements DataConfigSource { return { fieldConfig: this.fieldConfig, replaceVariables: this.replaceVariables, + getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid.bind(getDatasourceSrv()), fieldConfigRegistry: this.plugin.fieldConfigRegistry, theme: config.theme, }; diff --git a/public/app/features/dashboard/state/PanelQueryRunner.test.ts b/public/app/features/dashboard/state/PanelQueryRunner.test.ts index b89782b8332..6b6b47bfb18 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.test.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.test.ts @@ -10,7 +10,7 @@ import { ScopedVars, } from '@grafana/data'; import { DashboardModel } from './index'; -import { setEchoSrv } from '@grafana/runtime'; +import { setDataSourceSrv, setEchoSrv } from '@grafana/runtime'; import { Echo } from '../../../core/services/echo/Echo'; jest.mock('app/core/services/backend_srv'); @@ -80,6 +80,11 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn }, ], }; + setDataSourceSrv({ + getDataSourceSettingsByUid() { + return {} as any; + }, + } as any); beforeEach(async () => { setEchoSrv(new Echo()); @@ -226,6 +231,7 @@ describe('PanelQueryRunner', () => { overrides: [], }, replaceVariables: v => v, + getDataSourceSettingsByUid: undefined as any, theme: {} as GrafanaTheme, }), getTransformations: () => undefined, @@ -292,6 +298,7 @@ describe('PanelQueryRunner', () => { overrides: [], }, replaceVariables: v => v, + getDataSourceSettingsByUid: undefined as any, theme: {} as GrafanaTheme, }), // @ts-ignore @@ -336,6 +343,7 @@ describe('PanelQueryRunner', () => { overrides: [], }, replaceVariables: v => v, + getDataSourceSettingsByUid: undefined as any, theme: {} as GrafanaTheme, }), // @ts-ignore diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index b945acdce31..a5536bbee8d 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -100,6 +100,7 @@ export class PanelQueryRunner { timeZone: this.timeZone, autoMinMax: true, data: processedData.series, + getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid.bind(getDatasourceSrv()), ...fieldConfig, }), }; diff --git a/public/app/features/dashboard/state/runRequest.ts b/public/app/features/dashboard/state/runRequest.ts index f7ce5891ceb..1e03fee25f3 100644 --- a/public/app/features/dashboard/state/runRequest.ts +++ b/public/app/features/dashboard/state/runRequest.ts @@ -156,7 +156,7 @@ export function callQueryMethod(datasource: DataSourceApi, request: DataQueryReq } /** - * All panels will be passed tables that have our best guess at colum type set + * All panels will be passed tables that have our best guess at column type set * * This is also used by PanelChrome for snapshot support */ diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index bda92fd476b..caa846c3c17 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -19,6 +19,8 @@ import { RawTimeRange, TimeRange, TimeZone, + ExploreUIState, + ExploreUrlState, } from '@grafana/data'; import store from 'app/core/store'; @@ -38,7 +40,7 @@ import { updateTimeRange, } from './state/actions'; -import { ExploreId, ExploreItemState, ExploreUIState, ExploreUpdateState, ExploreUrlState } from 'app/types/explore'; +import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types/explore'; import { StoreState } from 'app/types'; import { DEFAULT_RANGE, diff --git a/public/app/features/explore/TableContainer.test.tsx b/public/app/features/explore/TableContainer.test.tsx index 302f44074cb..288c836113e 100644 --- a/public/app/features/explore/TableContainer.test.tsx +++ b/public/app/features/explore/TableContainer.test.tsx @@ -15,6 +15,8 @@ describe('TableContainer', () => { showingTable: true, tableResult: {} as DataFrame, toggleTable: {} as typeof toggleTable, + splitOpen: (() => {}) as any, + range: {} as any, }; const wrapper = shallow(); @@ -34,6 +36,8 @@ describe('TableContainer', () => { length: 0, } as DataFrame, toggleTable: {} as typeof toggleTable, + splitOpen: (() => {}) as any, + range: {} as any, }; const wrapper = render(); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index c7539f0c301..2da451fec50 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -1,15 +1,16 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import { DataFrame } from '@grafana/data'; +import { DataFrame, TimeRange, ValueLinkConfig } from '@grafana/data'; import { Collapse, Table } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { toggleTable } from './state/actions'; +import { splitOpen, toggleTable } from './state/actions'; import { config } from 'app/core/config'; import { PANEL_BORDER } from 'app/core/constants'; import { MetaInfoText } from './MetaInfoText'; import { FilterItem } from '@grafana/ui/src/components/Table/types'; +import { getFieldLinksForExplore } from './utils/links'; interface TableContainerProps { exploreId: ExploreId; @@ -19,6 +20,8 @@ interface TableContainerProps { showingTable: boolean; tableResult?: DataFrame; toggleTable: typeof toggleTable; + splitOpen: typeof splitOpen; + range: TimeRange; } export class TableContainer extends PureComponent { @@ -38,12 +41,23 @@ export class TableContainer extends PureComponent { } render() { - const { loading, onCellFilterAdded, showingTable, tableResult, width } = this.props; + const { loading, onCellFilterAdded, showingTable, tableResult, width, splitOpen, range } = this.props; const height = this.getTableHeight(); const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER; const hasTableResult = tableResult?.length; + if (hasTableResult) { + // Bit of code smell here. We need to add links here to the frame modifying the frame on every render. + // Should work fine in essence but still not the ideal way to pass props. In logs container we do this + // differently and sidestep this getLinks API on a dataframe + for (const field of tableResult.fields) { + field.getLinks = (config: ValueLinkConfig) => { + return getFieldLinksForExplore(field, config.valueRowIndex, splitOpen, range); + }; + } + } + return ( {hasTableResult ? ( @@ -60,13 +74,14 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } const explore = state.explore; // @ts-ignore const item: ExploreItemState = explore[exploreId]; - const { loading: loadingInState, showingTable, tableResult } = item; + const { loading: loadingInState, showingTable, tableResult, range } = item; const loading = tableResult && tableResult.length > 0 ? false : loadingInState; - return { loading, showingTable, tableResult }; + return { loading, showingTable, tableResult, range }; } const mapDispatchToProps = { toggleTable, + splitOpen, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index a9dbdf36670..d816cbf8cf5 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -14,8 +14,9 @@ import { QueryFixAction, TimeRange, ExploreMode, + ExploreUIState, } from '@grafana/data'; -import { ExploreId, ExploreItemState, ExploreUIState } from 'app/types/explore'; +import { ExploreId, ExploreItemState } from 'app/types/explore'; export interface AddQueryRowPayload { exploreId: ExploreId; diff --git a/public/app/features/explore/state/actions.test.ts b/public/app/features/explore/state/actions.test.ts index bba493605da..492be9b5d42 100644 --- a/public/app/features/explore/state/actions.test.ts +++ b/public/app/features/explore/state/actions.test.ts @@ -1,5 +1,5 @@ import { PayloadAction } from '@reduxjs/toolkit'; -import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, toUtc } from '@grafana/data'; +import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, toUtc, ExploreUrlState } from '@grafana/data'; import * as Actions from './actions'; import { @@ -10,7 +10,7 @@ import { navigateToExplore, refreshExplore, } from './actions'; -import { ExploreId, ExploreUpdateState, ExploreUrlState } from 'app/types'; +import { ExploreId, ExploreUpdateState } from 'app/types'; import { thunkTester } from 'test/core/thunk/thunkTester'; import { cancelQueriesAction, diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 5182c0f5c3e..809d507a657 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -17,6 +17,8 @@ import { RawTimeRange, TimeRange, ExploreMode, + ExploreUrlState, + ExploreUIState, } from '@grafana/data'; // Services & Utils import store from 'app/core/store'; @@ -34,7 +36,6 @@ import { hasNonEmptyQuery, lastUsedDatasourceKeyForOrgId, parseUrlState, - serializeStateToUrlParam, stopQueryState, updateHistory, } from 'app/core/utils/explore'; @@ -47,9 +48,9 @@ import { getRichHistory, } from 'app/core/utils/richHistory'; // Types -import { ExploreItemState, ExploreUrlState, ThunkResult } from 'app/types'; +import { ExploreItemState, ThunkResult } from 'app/types'; -import { ExploreId, ExploreUIState, QueryOptions } from 'app/types/explore'; +import { ExploreId, QueryOptions } from 'app/types/explore'; import { addQueryRowAction, changeModeAction, @@ -94,6 +95,7 @@ import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv'; import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest'; import { PanelModel } from 'app/features/dashboard/state'; import { getExploreDatasources } from './selectors'; +import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; /** * Updates UI state and save it to the URL @@ -696,7 +698,7 @@ export function splitClose(itemId: ExploreId): ThunkResult { * Otherwise it copies the left state to be the right state. The copy keeps all query modifications but wipes the query * results. */ -export function splitOpen(options?: { datasourceUid: string; query: string }): ThunkResult { +export function splitOpen(options?: { datasourceUid: string; query: T }): ThunkResult { return async (dispatch, getState) => { // Clone left state to become the right state const leftState: ExploreItemState = getState().explore[ExploreId.left]; @@ -706,17 +708,20 @@ export function splitOpen(options?: { datasourceUid: string; query: string }): T const queryState = getState().location.query[ExploreId.left] as string; const urlState = parseUrlState(queryState); - // TODO: Instead of splitting and then setting query/datasource we may probably do it in one action call - rightState.queries = leftState.queries.slice(); - rightState.urlState = urlState; - dispatch(splitOpenAction({ itemState: rightState })); - if (options) { - // TODO: This is hardcoded for Jaeger right now. Need to be changed so that target datasource can define the - // query shape. + rightState.queries = []; + rightState.graphResult = undefined; + rightState.logsResult = undefined; + rightState.tableResult = undefined; + rightState.queryKeys = []; + urlState.queries = []; + rightState.urlState = urlState; + + dispatch(splitOpenAction({ itemState: rightState })); + const queries = [ { - query: options.query, + ...options.query, refId: 'A', } as DataQuery, ]; @@ -724,6 +729,10 @@ export function splitOpen(options?: { datasourceUid: string; query: string }): T const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.datasourceUid); await dispatch(changeDatasource(ExploreId.right, dataSourceSettings.name)); await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries })); + } else { + rightState.queries = leftState.queries.slice(); + rightState.urlState = urlState; + dispatch(splitOpenAction({ itemState: rightState })); } dispatch(stateSave()); diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 4fa6ac3bb52..75618d0dd20 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -8,6 +8,7 @@ import { RawTimeRange, toDataFrame, UrlQueryMap, + ExploreUrlState, } from '@grafana/data'; import { @@ -18,7 +19,7 @@ import { makeExploreItemState, makeInitialUpdateState, } from './reducers'; -import { ExploreId, ExploreItemState, ExploreState, ExploreUrlState } from 'app/types/explore'; +import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore'; import { reducerTester } from 'test/core/redux/reducerTester'; import { changeModeAction, @@ -34,8 +35,8 @@ import { addQueryRowAction, removeQueryRowAction, } from './actionTypes'; -import { serializeStateToUrlParam } from 'app/core/utils/explore'; import { updateLocation } from '../../../core/actions'; +import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; const QUERY_KEY_REGEX = /Q-([0-9]+)-([0-9.]+)-([0-9]+)/; diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index f55fb01fb55..c31bd789e6e 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -39,8 +39,9 @@ describe('getFieldLinksForExplore', () => { it('returns correct link model for internal link', () => { const { field, range } = setup({ title: '', - url: 'query_1', - meta: { + url: '', + internal: { + query: { query: 'query_1' }, datasourceUid: 'uid_1', }, }); @@ -56,7 +57,7 @@ describe('getFieldLinksForExplore', () => { links[0].onClick({}); } - expect(splitfn).toBeCalledWith({ datasourceUid: 'uid_1', query: 'query_1' }); + expect(splitfn).toBeCalledWith({ datasourceUid: 'uid_1', query: { query: 'query_1' } }); }); }); @@ -70,10 +71,10 @@ function setup(link: DataLink) { origin: origin, }; }, - getAnchorInfo(link: DataLink) { + getAnchorInfo(link: any) { return { ...link }; }, - getLinkUrl(link: DataLink) { + getLinkUrl(link: any) { return link.url; }, }); diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index fd1a9e17918..5bb8caf505a 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -1,8 +1,8 @@ import { splitOpen } from '../state/actions'; -import { ExploreMode, Field, LinkModel, locationUtil, TimeRange } from '@grafana/data'; -import { getLinksFromLogsField } from '../../panel/panellinks/linkSuppliers'; -import { serializeStateToUrlParam } from '../../../core/utils/explore'; -import { getDataSourceSrv } from '@grafana/runtime'; +import { Field, LinkModel, TimeRange } from '@grafana/data'; +import { getLinkSrv } from '../../panel/panellinks/link_srv'; +import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks'; +import { getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; /** * Get links from the field of a dataframe and in addition check if there is associated @@ -11,81 +11,52 @@ import { getDataSourceSrv } from '@grafana/runtime'; * appropriately. This is for example used for transition from log with traceId to trace datasource to show that * trace. */ -export function getFieldLinksForExplore( +export const getFieldLinksForExplore = ( field: Field, rowIndex: number, splitOpenFn: typeof splitOpen, range: TimeRange -): Array> { - const data = getLinksFromLogsField(field, rowIndex); - return data.map(d => { - if (d.link.meta?.datasourceUid) { - return { - ...d.linkModel, - title: - d.linkModel.title || - getDataSourceSrv().getDataSourceSettingsByUid(d.link.meta.datasourceUid)?.name || - 'Unknown datasource', - onClick: () => { - splitOpenFn({ - datasourceUid: d.link.meta.datasourceUid, - // TODO: fix the ambiguity here - // This looks weird but in case meta.datasourceUid is set we save the query in url which will get - // interpolated into href - query: d.linkModel.href, +): Array> => { + const scopedVars: any = {}; + scopedVars['__value'] = { + value: { + raw: field.values.get(rowIndex), + }, + text: 'Raw value', + }; + + return field.config.links + ? field.config.links.map(link => { + if (!link.internal) { + const linkModel = getLinkSrv().getDataLinkUIModel(link, scopedVars, field); + if (!linkModel.title) { + linkModel.title = getTitleFromHref(linkModel.href); + } + return linkModel; + } else { + return mapInternalLinkToExplore(link, scopedVars, range, field, { + onClickFn: splitOpenFn, + replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), + getDataSourceSettingsByUid: getDataSourceSrv().getDataSourceSettingsByUid.bind(getDataSourceSrv()), }); - }, - // We need to create real href here as the linkModel.href actually contains query. As in this case this is - // meant to be internal link (opens split view by default) the href will also points to explore but this - // way you can open it in new tab. - href: generateInternalHref(d.link.meta.datasourceUid, d.linkModel.href, range), - }; - } + } + }) + : []; +}; - if (!d.linkModel.title) { - let href = d.linkModel.href; - // The URL constructor needs the url to have protocol - if (href.indexOf('://') < 0) { - // Doesn't really matter what protocol we use. - href = `http://${href}`; - } - let title; - try { - const parsedUrl = new URL(href); - title = parsedUrl.hostname; - } catch (_e) { - // Should be good enough fallback, user probably did not input valid url. - title = href; - } - - return { - ...d.linkModel, - title, - }; - } - return d.linkModel; - }); -} - -/** - * Generates href for internal derived field link. - */ -function generateInternalHref(datasourceUid: string, query: string, range: TimeRange): string { - return locationUtil.assureBaseUrl( - `/explore?left=${serializeStateToUrlParam({ - range: range.raw, - datasource: getDataSourceSrv().getDataSourceSettingsByUid(datasourceUid).name, - // Again hardcoded for Jaeger query structure - // TODO: fix - queries: [{ query }], - // This should get overwritten if datasource does not support that mode and we do not know what mode is - // preferred anyway. - mode: ExploreMode.Metrics, - ui: { - showingGraph: true, - showingTable: true, - showingLogs: true, - }, - })}` - ); +function getTitleFromHref(href: string): string { + // The URL constructor needs the url to have protocol + if (href.indexOf('://') < 0) { + // Doesn't really matter what protocol we use. + href = `http://${href}`; + } + let title; + try { + const parsedUrl = new URL(href); + title = parsedUrl.hostname; + } catch (_e) { + // Should be good enough fallback, user probably did not input valid url. + title = href; + } + return title; } diff --git a/public/app/features/panel/panellinks/linkSuppliers.test.ts b/public/app/features/panel/panellinks/linkSuppliers.test.ts index fa903c8d4a8..374dfe8fcb8 100644 --- a/public/app/features/panel/panellinks/linkSuppliers.test.ts +++ b/public/app/features/panel/panellinks/linkSuppliers.test.ts @@ -1,20 +1,10 @@ -import { getFieldLinksSupplier, getLinksFromLogsField } from './linkSuppliers'; -import { - applyFieldOverrides, - ArrayVector, - DataFrameView, - dateTime, - Field, - FieldDisplay, - FieldType, - GrafanaTheme, - toDataFrame, -} from '@grafana/data'; +import { getFieldLinksSupplier } from './linkSuppliers'; +import { applyFieldOverrides, DataFrameView, dateTime, FieldDisplay, GrafanaTheme, toDataFrame } from '@grafana/data'; import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv'; import { TemplateSrv } from '../../templating/template_srv'; import { TimeSrv } from '../../dashboard/services/TimeSrv'; -describe('getLinksFromLogsField', () => { +describe('getFieldLinksSupplier', () => { let originalLinkSrv: LinkService; beforeAll(() => { // We do not need more here and TimeSrv is hard to setup fully. @@ -34,41 +24,6 @@ describe('getLinksFromLogsField', () => { setLinkSrv(originalLinkSrv); }); - it('interpolates link from field', () => { - const field: Field = { - name: 'test field', - type: FieldType.number, - config: { - links: [ - { - title: 'title1', - url: 'http://domain.com/${__value.raw}', - }, - { - title: 'title2', - url: 'http://anotherdomain.sk/${__value.raw}', - }, - ], - }, - values: new ArrayVector([1, 2, 3]), - }; - const links = getLinksFromLogsField(field, 2); - expect(links.length).toBe(2); - expect(links[0].linkModel.href).toBe('http://domain.com/3'); - expect(links[1].linkModel.href).toBe('http://anotherdomain.sk/3'); - }); - - it('handles zero links', () => { - const field: Field = { - name: 'test field', - type: FieldType.number, - config: {}, - values: new ArrayVector([1, 2, 3]), - }; - const links = getLinksFromLogsField(field, 2); - expect(links.length).toBe(0); - }); - it('links to items on the row', () => { const data = applyFieldOverrides({ data: [ @@ -134,6 +89,7 @@ describe('getLinksFromLogsField', () => { overrides: [], }, replaceVariables: (val: string) => val, + getDataSourceSettingsByUid: (val: string) => ({} as any), timeZone: 'utc', theme: {} as GrafanaTheme, autoMinMax: true, diff --git a/public/app/features/panel/panellinks/linkSuppliers.ts b/public/app/features/panel/panellinks/linkSuppliers.ts index 5fa8a254b8e..4b0da5d5882 100644 --- a/public/app/features/panel/panellinks/linkSuppliers.ts +++ b/public/app/features/panel/panellinks/linkSuppliers.ts @@ -2,13 +2,11 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DataLink, DisplayValue, - Field, FieldDisplay, formattedValueToString, getFieldDisplayValuesProxy, getTimeField, Labels, - LinkModel, LinkModelSupplier, ScopedVar, ScopedVars, @@ -50,7 +48,6 @@ interface DataLinkScopedVars extends ScopedVars { /** * 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) { @@ -124,7 +121,7 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier { + return links.map((link: DataLink) => { return getLinkSrv().getDataLinkUIModel(link, scopedVars, value); }); }, @@ -146,25 +143,3 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier; link: DataLink }> => { - const scopedVars: any = {}; - scopedVars['__value'] = { - value: { - raw: field.values.get(rowIndex), - }, - text: 'Raw value', - }; - - return field.config.links - ? field.config.links.map(link => { - return { - link, - linkModel: getLinkSrv().getDataLinkUIModel(link, scopedVars, field), - }; - }) - : []; -}; diff --git a/public/app/features/panel/panellinks/link_srv.ts b/public/app/features/panel/panellinks/link_srv.ts index f421a8c02dc..5302466a2d8 100644 --- a/public/app/features/panel/panellinks/link_srv.ts +++ b/public/app/features/panel/panellinks/link_srv.ts @@ -5,7 +5,6 @@ import coreModule from 'app/core/core_module'; import { getConfig } from 'app/core/config'; import { DataFrame, - DataLink, DataLinkBuiltInVars, deprecationWarning, Field, @@ -19,6 +18,7 @@ import { VariableSuggestionsScope, urlUtil, textUtil, + DataLink, } from '@grafana/data'; const timeRangeVars = [ diff --git a/public/app/plugins/datasource/loki/configuration/DebugSection.tsx b/public/app/plugins/datasource/loki/configuration/DebugSection.tsx index f3188cc456e..64d878e9252 100644 --- a/public/app/plugins/datasource/loki/configuration/DebugSection.tsx +++ b/public/app/plugins/datasource/loki/configuration/DebugSection.tsx @@ -4,8 +4,8 @@ import cx from 'classnames'; import { LegacyForms } from '@grafana/ui'; const { FormField } = LegacyForms; import { DerivedFieldConfig } from '../types'; -import { getLinksFromLogsField } from '../../../../features/panel/panellinks/linkSuppliers'; import { ArrayVector, Field, FieldType, LinkModel } from '@grafana/data'; +import { getFieldLinksForExplore } from '../../../../features/explore/utils/links'; type Props = { derivedFields: DerivedFieldConfig[]; @@ -94,7 +94,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string) let link: LinkModel; if (field.url && value) { - link = getLinksFromLogsField( + link = getFieldLinksForExplore( { name: '', type: FieldType.string, @@ -103,8 +103,10 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string) links: [{ title: '', url: field.url }], }, }, - 0 - )[0].linkModel; + 0, + (() => {}) as any, + {} as any + )[0]; } return { @@ -113,6 +115,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string) href: link && link.href, } as DebugField; } catch (error) { + console.error(error); return { name: field.name, error, diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index 14c4199aa99..45a529321fa 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -129,11 +129,13 @@ describe('enhanceDataFrame', () => { { matcherRegex: 'trace2=(\\w+)', name: 'trace2', + url: 'test', datasourceUid: 'uid', }, { matcherRegex: 'trace2=(\\w+)', name: 'trace2', + url: 'test', datasourceUid: 'uid2', }, ], @@ -150,11 +152,13 @@ describe('enhanceDataFrame', () => { expect(fc.getFieldByName('trace2').config.links.length).toBe(2); expect(fc.getFieldByName('trace2').config.links[0]).toEqual({ title: '', - meta: { datasourceUid: 'uid' }, + internal: { datasourceUid: 'uid', query: { query: 'test' } }, + url: '', }); expect(fc.getFieldByName('trace2').config.links[1]).toEqual({ title: '', - meta: { datasourceUid: 'uid2' }, + internal: { datasourceUid: 'uid2', query: { query: 'test' } }, + url: '', }); }); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 8899049b75a..4ae03bd1d64 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -357,17 +357,24 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul */ function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): Field { const dataLinks = derivedFieldConfigs.reduce((acc, derivedFieldConfig) => { - if (derivedFieldConfig.url || derivedFieldConfig.datasourceUid) { + // Having field.datasourceUid means it is an internal link. + if (derivedFieldConfig.datasourceUid) { + acc.push({ + // Will be filled out later + title: '', + url: '', + // This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object + internal: { + query: { query: derivedFieldConfig.url }, + datasourceUid: derivedFieldConfig.datasourceUid, + }, + }); + } else if (derivedFieldConfig.url) { acc.push({ // We do not know what title to give here so we count on presentation layer to create a title from metadata. title: '', + // This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object url: derivedFieldConfig.url, - // Having field.datasourceUid means it is an internal link. - meta: derivedFieldConfig.datasourceUid - ? { - datasourceUid: derivedFieldConfig.datasourceUid, - } - : undefined, }); } return acc; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 07f4654af87..e0e91a8495a 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -15,6 +15,7 @@ import { GraphSeriesXY, DataFrame, ExploreMode, + ExploreUrlState, } from '@grafana/data'; import { Emitter } from 'app/core/core'; @@ -197,23 +198,6 @@ export interface ExploreUpdateState { ui: boolean; } -export interface ExploreUIState { - showingTable: boolean; - showingGraph: boolean; - showingLogs: boolean; - dedupStrategy?: LogsDedupStrategy; -} - -export interface ExploreUrlState { - datasource: string; - queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense - mode: ExploreMode; - range: RawTimeRange; - ui: ExploreUIState; - originPanelId?: number; - context?: string; -} - export interface QueryOptions { minInterval: string; maxDataPoints?: number;