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
|
* @param url
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
const assureBaseUrl = (url: string) => {
|
const assureBaseUrl = (url: string): string => {
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
return `${grafanaConfig ? grafanaConfig().appSubUrl : ''}${stripBaseFromUrl(url)}`;
|
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
|
* 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.
|
* @param scopedVars - variables used to interpolate a templated passed as name.
|
||||||
*/
|
*/
|
||||||
get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns metadata based on UID.
|
||||||
|
*/
|
||||||
|
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let singletonInstance: DataSourceSrv;
|
let singletonInstance: DataSourceSrv;
|
||||||
|
@ -22,6 +22,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
|
getDataSourceSettingsByUid(): any {},
|
||||||
};
|
};
|
||||||
const targets: ElasticsearchQuery[] = [
|
const targets: ElasticsearchQuery[] = [
|
||||||
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||||
@ -58,6 +59,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
|
|
||||||
return Promise.resolve(alertingDatasource);
|
return Promise.resolve(alertingDatasource);
|
||||||
},
|
},
|
||||||
|
getDataSourceSettingsByUid(): any {},
|
||||||
};
|
};
|
||||||
const targets: any[] = [
|
const targets: any[] = [
|
||||||
{ refId: 'A', query: 'some query', datasource: 'alertingDatasource' },
|
{ refId: 'A', query: 'some query', datasource: 'alertingDatasource' },
|
||||||
@ -81,6 +83,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
|
getDataSourceSettingsByUid(): any {},
|
||||||
};
|
};
|
||||||
const targets: ElasticsearchQuery[] = [
|
const targets: ElasticsearchQuery[] = [
|
||||||
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||||
@ -106,6 +109,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
|
getDataSourceSettingsByUid(): any {},
|
||||||
};
|
};
|
||||||
const targets: ElasticsearchQuery[] = [
|
const targets: ElasticsearchQuery[] = [
|
||||||
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
||||||
@ -131,6 +135,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
|
getDataSourceSettingsByUid(): any {},
|
||||||
};
|
};
|
||||||
const targets: ElasticsearchQuery[] = [
|
const targets: ElasticsearchQuery[] = [
|
||||||
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
||||||
|
@ -4,23 +4,23 @@ import { connect } from 'react-redux';
|
|||||||
import { Collapse } from '@grafana/ui';
|
import { Collapse } from '@grafana/ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataSourceApi,
|
|
||||||
RawTimeRange,
|
|
||||||
LogLevel,
|
|
||||||
TimeZone,
|
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
|
DataSourceApi,
|
||||||
|
Field,
|
||||||
|
GraphSeriesXY,
|
||||||
|
LogLevel,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
TimeRange,
|
|
||||||
LogsMetaItem,
|
LogsMetaItem,
|
||||||
GraphSeriesXY,
|
RawTimeRange,
|
||||||
Field,
|
TimeRange,
|
||||||
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
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 { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
|
||||||
import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors';
|
import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors';
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
@ -28,7 +28,7 @@ import { LiveLogsWithTheme } from './LiveLogs';
|
|||||||
import { Logs } from './Logs';
|
import { Logs } from './Logs';
|
||||||
import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
import { getLinksFromLogsField } from '../panel/panellinks/linkSuppliers';
|
import { getFieldLinksForExplore } from './utils/links';
|
||||||
|
|
||||||
interface LogsContainerProps {
|
interface LogsContainerProps {
|
||||||
datasourceInstance?: DataSourceApi;
|
datasourceInstance?: DataSourceApi;
|
||||||
@ -89,28 +89,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
return [];
|
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) => {
|
getFieldLinks = (field: Field, rowIndex: number) => {
|
||||||
const data = getLinksFromLogsField(field, rowIndex);
|
return getFieldLinksForExplore(field, rowIndex, this.props.splitOpen, this.props.range);
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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
|
* Otherwise it copies the left state to be the right state. The copy keeps all query modifications but wipes the query
|
||||||
* results.
|
* 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) => {
|
return async (dispatch, getState) => {
|
||||||
// Clone left state to become the right state
|
// Clone left state to become the right state
|
||||||
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
||||||
@ -727,7 +727,7 @@ export function splitOpen(options?: { dataSourceUid: string; query: string }): T
|
|||||||
} as DataQuery,
|
} as DataQuery,
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.dataSourceUid);
|
const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.datasourceUid);
|
||||||
await dispatch(changeDatasource(ExploreId.right, dataSourceSettings.name));
|
await dispatch(changeDatasource(ExploreId.right, dataSourceSettings.name));
|
||||||
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
|
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