mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Logs: Add href to internal link (#23757)
This commit is contained in:
parent
eae11f53f3
commit
319a0585a5
@ -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)}`;
|
||||
}
|
||||
|
@ -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<DataSourceApi>;
|
||||
|
||||
/**
|
||||
* Returns metadata based on UID.
|
||||
*/
|
||||
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined;
|
||||
}
|
||||
|
||||
let singletonInstance: DataSourceSrv;
|
||||
|
@ -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 },
|
||||
|
@ -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<LogsContainerProps> {
|
||||
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() {
|
||||
|
@ -702,7 +702,7 @@ export function splitClose(itemId: ExploreId): ThunkResult<void> {
|
||||
* 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<void> {
|
||||
export function splitOpen(options?: { datasourceUid: string; query: string }): ThunkResult<void> {
|
||||
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 }));
|
||||
}
|
||||
|
91
public/app/features/explore/utils/links.test.ts
Normal file
91
public/app/features/explore/utils/links.test.ts
Normal file
@ -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<any> {
|
||||
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<string> = {
|
||||
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 };
|
||||
}
|
65
public/app/features/explore/utils/links.ts
Normal file
65
public/app/features/explore/utils/links.ts
Normal file
@ -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<LinkModel<Field>> {
|
||||
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,
|
||||
},
|
||||
})}`
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user