diff --git a/packages/grafana-data/src/utils/location.ts b/packages/grafana-data/src/utils/location.ts index dc1f8925088..af1ae515f52 100644 --- a/packages/grafana-data/src/utils/location.ts +++ b/packages/grafana-data/src/utils/location.ts @@ -25,7 +25,7 @@ const stripBaseFromUrl = (url: string): string => { * @param url * @internal */ -const assureBaseUrl = (url: string) => { +const assureBaseUrl = (url: string): string => { if (url.startsWith('/')) { return `${grafanaConfig ? grafanaConfig().appSubUrl : ''}${stripBaseFromUrl(url)}`; } diff --git a/packages/grafana-runtime/src/services/dataSourceSrv.ts b/packages/grafana-runtime/src/services/dataSourceSrv.ts index 4e87fc56157..225ca76cd31 100644 --- a/packages/grafana-runtime/src/services/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/dataSourceSrv.ts @@ -1,4 +1,4 @@ -import { ScopedVars, DataSourceApi } from '@grafana/data'; +import { ScopedVars, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; /** * This is the entry point for communicating with a datasource that is added as @@ -14,6 +14,11 @@ export interface DataSourceSrv { * @param scopedVars - variables used to interpolate a templated passed as name. */ get(name?: string, scopedVars?: ScopedVars): Promise; + + /** + * Returns metadata based on UID. + */ + getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined; } let singletonInstance: DataSourceSrv; diff --git a/public/app/features/alerting/getAlertingValidationMessage.test.ts b/public/app/features/alerting/getAlertingValidationMessage.test.ts index 06848a20aea..ec53d846571 100644 --- a/public/app/features/alerting/getAlertingValidationMessage.test.ts +++ b/public/app/features/alerting/getAlertingValidationMessage.test.ts @@ -22,6 +22,7 @@ describe('getAlertingValidationMessage', () => { const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { get: getMock, + getDataSourceSettingsByUid(): any {}, }; const targets: ElasticsearchQuery[] = [ { refId: 'A', query: '@hostname:$hostname', isLogsQuery: false }, @@ -58,6 +59,7 @@ describe('getAlertingValidationMessage', () => { return Promise.resolve(alertingDatasource); }, + getDataSourceSettingsByUid(): any {}, }; const targets: any[] = [ { refId: 'A', query: 'some query', datasource: 'alertingDatasource' }, @@ -81,6 +83,7 @@ describe('getAlertingValidationMessage', () => { const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { get: getMock, + getDataSourceSettingsByUid(): any {}, }; const targets: ElasticsearchQuery[] = [ { refId: 'A', query: '@hostname:$hostname', isLogsQuery: false }, @@ -106,6 +109,7 @@ describe('getAlertingValidationMessage', () => { const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { get: getMock, + getDataSourceSettingsByUid(): any {}, }; const targets: ElasticsearchQuery[] = [ { refId: 'A', query: '@hostname:hostname', isLogsQuery: false }, @@ -131,6 +135,7 @@ describe('getAlertingValidationMessage', () => { const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { get: getMock, + getDataSourceSettingsByUid(): any {}, }; const targets: ElasticsearchQuery[] = [ { refId: 'A', query: '@hostname:hostname', isLogsQuery: false }, diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 6367b206c60..87991c4d950 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -4,23 +4,23 @@ import { connect } from 'react-redux'; import { Collapse } from '@grafana/ui'; import { - DataSourceApi, - RawTimeRange, - LogLevel, - TimeZone, AbsoluteTimeRange, + DataSourceApi, + Field, + GraphSeriesXY, + LogLevel, LogRowModel, LogsDedupStrategy, - TimeRange, LogsMetaItem, - GraphSeriesXY, - Field, + RawTimeRange, + TimeRange, + TimeZone, } from '@grafana/data'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { changeDedupStrategy, updateTimeRange, splitOpen } from './state/actions'; +import { changeDedupStrategy, splitOpen, updateTimeRange } from './state/actions'; import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes'; import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors'; import { getTimeZone } from '../profile/state/selectors'; @@ -28,7 +28,7 @@ import { LiveLogsWithTheme } from './LiveLogs'; import { Logs } from './Logs'; import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition'; import { LiveTailControls } from './useLiveTailControls'; -import { getLinksFromLogsField } from '../panel/panellinks/linkSuppliers'; +import { getFieldLinksForExplore } from './utils/links'; interface LogsContainerProps { datasourceInstance?: DataSourceApi; @@ -89,28 +89,8 @@ export class LogsContainer extends PureComponent { return []; }; - /** - * Get links from the filed of a dataframe that was given to as and in addition check if there is associated - * metadata with datasource in which case we will add onClick to open the link in new split window. This assumes - * that we just supply datasource name and field value and Explore split window will know how to render that - * appropriately. This is for example used for transition from log with traceId to trace datasource to show that - * trace. - * @param field - * @param rowIndex - */ getFieldLinks = (field: Field, rowIndex: number) => { - const data = getLinksFromLogsField(field, rowIndex); - return data.map(d => { - if (d.link.meta?.datasourceUid) { - return { - ...d.linkModel, - onClick: () => { - this.props.splitOpen({ dataSourceUid: d.link.meta.datasourceUid, query: field.values.get(rowIndex) }); - }, - }; - } - return d.linkModel; - }); + return getFieldLinksForExplore(field, rowIndex, this.props.splitOpen, this.props.range); }; render() { diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index c2bc3e0df7a..e4f903f78ea 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -702,7 +702,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: string }): ThunkResult { return async (dispatch, getState) => { // Clone left state to become the right state const leftState: ExploreItemState = getState().explore[ExploreId.left]; @@ -727,7 +727,7 @@ export function splitOpen(options?: { dataSourceUid: string; query: string }): T } as DataQuery, ]; - const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.dataSourceUid); + const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.datasourceUid); await dispatch(changeDatasource(ExploreId.right, dataSourceSettings.name)); await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries })); } diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts new file mode 100644 index 00000000000..7a221e61ef9 --- /dev/null +++ b/public/app/features/explore/utils/links.test.ts @@ -0,0 +1,91 @@ +import { getFieldLinksForExplore } from './links'; +import { + ArrayVector, + DataLink, + DataSourceInstanceSettings, + dateTime, + Field, + FieldType, + LinkModel, + ScopedVars, + TimeRange, +} from '@grafana/data'; +import { setLinkSrv } from '../../panel/panellinks/link_srv'; +import { setDataSourceSrv } from '@grafana/runtime'; + +describe('getFieldLinksForExplore', () => { + it('returns correct link model for external link', () => { + const { field, range } = setup({ + title: 'external', + url: 'http://regionalhost', + }); + const links = getFieldLinksForExplore(field, 0, jest.fn(), range); + + expect(links[0].href).toBe('http://regionalhost'); + expect(links[0].title).toBe('external'); + }); + + it('returns correct link model for internal link', () => { + const { field, range } = setup({ + title: 'test', + url: 'query_1', + meta: { + datasourceUid: 'uid_1', + }, + }); + const splitfn = jest.fn(); + + const links = getFieldLinksForExplore(field, 0, splitfn, range); + + expect(links[0].href).toBe( + '/explore?left={"range":{"from":"now-1h","to":"now"},"datasource":"test_ds","queries":[{"query":"query_1"}],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}' + ); + expect(links[0].title).toBe('test'); + links[0].onClick({}); + expect(splitfn).toBeCalledWith({ datasourceUid: 'uid_1', query: 'query_1' }); + }); +}); + +function setup(link: DataLink) { + setLinkSrv({ + getDataLinkUIModel(link: DataLink, scopedVars: ScopedVars, origin: any): LinkModel { + return { + href: link.url, + title: link.title, + target: '_blank', + origin: origin, + }; + }, + }); + setDataSourceSrv({ + getDataSourceSettingsByUid(uid: string) { + return { + id: 1, + uid: 'uid_1', + type: 'metrics', + name: 'test_ds', + meta: {}, + jsonData: {}, + } as DataSourceInstanceSettings; + }, + } as any); + const field: Field = { + name: 'flux-dimensions', + type: FieldType.string, + values: new ArrayVector([]), + config: { + links: [link], + }, + }; + + const range: TimeRange = { + from: dateTime(), + to: dateTime(), + raw: { + from: 'now-1h', + to: 'now', + }, + }; + + return { range, field }; +} diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts new file mode 100644 index 00000000000..651f267bc41 --- /dev/null +++ b/public/app/features/explore/utils/links.ts @@ -0,0 +1,65 @@ +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'; + +/** + * Get links from the filed of a dataframe that was given to as and in addition check if there is associated + * metadata with datasource in which case we will add onClick to open the link in new split window. This assumes + * that we just supply datasource name and field value and Explore split window will know how to render that + * appropriately. This is for example used for transition from log with traceId to trace datasource to show that + * trace. + */ +export function 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, + 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, + }); + }, + // 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), + }; + } + 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, + }, + })}` + ); +}