mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Add link to logs from trace span (#28229)
* Add trace to logs link * Do a bit of refactor and allow for custom time range in split * Add margin and noopener to the link * Fix tests * Fix tests
This commit is contained in:
parent
26e2faa779
commit
c8658f3ee8
@ -35,6 +35,8 @@ export interface FeatureToggles {
|
||||
live: boolean;
|
||||
expressions: boolean;
|
||||
ngalert: boolean;
|
||||
// Just for demo at the moment
|
||||
traceToLogs: boolean;
|
||||
|
||||
/**
|
||||
* @remarks
|
||||
|
@ -51,6 +51,7 @@ export type TraceSpanData = {
|
||||
traceID: string;
|
||||
processID: string;
|
||||
operationName: string;
|
||||
// Times are in microseconds
|
||||
startTime: number;
|
||||
duration: number;
|
||||
logs: TraceLog[];
|
||||
|
@ -26,8 +26,9 @@ export const DataLinkBuiltInVars = {
|
||||
valueCalc: '__value.calc',
|
||||
};
|
||||
|
||||
// We inject these because we cannot import them directly as they reside inside grafana main package.
|
||||
type Options = {
|
||||
onClickFn?: (options: { datasourceUid: string; query: any }) => void;
|
||||
onClickFn?: (options: { datasourceUid: string; query: any; range?: TimeRange }) => void;
|
||||
replaceVariables: InterpolateFunction;
|
||||
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
|
||||
};
|
||||
@ -62,6 +63,7 @@ export function mapInternalLinkToExplore(
|
||||
onClickFn?.({
|
||||
datasourceUid: link.internal!.datasourceUid,
|
||||
query: interpolatedQuery,
|
||||
range,
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
|
@ -15,6 +15,6 @@ export { getMappedValue } from './valueMappings';
|
||||
export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
|
||||
export { locationUtil } from './location';
|
||||
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url';
|
||||
export { DataLinkBuiltInVars } from './dataLinks';
|
||||
export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks';
|
||||
export { DocsId } from './docs';
|
||||
export { observableTester } from './tests/observableTester';
|
||||
|
@ -56,6 +56,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
expressions: false,
|
||||
meta: false,
|
||||
ngalert: false,
|
||||
traceToLogs: false,
|
||||
};
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
|
@ -19,6 +19,16 @@ export interface DataSourceSrv {
|
||||
* Returns metadata based on UID.
|
||||
*/
|
||||
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined;
|
||||
|
||||
/**
|
||||
* Get all data sources
|
||||
*/
|
||||
getAll(): DataSourceInstanceSettings[];
|
||||
|
||||
/**
|
||||
* Get all data sources except for internal ones that usually should not be listed like mixed data source.
|
||||
*/
|
||||
getExternal(): DataSourceInstanceSettings[];
|
||||
}
|
||||
|
||||
let singletonInstance: DataSourceSrv;
|
||||
|
@ -310,6 +310,9 @@ type SpanBarRowProps = {
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
clippingLeft?: boolean;
|
||||
clippingRight?: boolean;
|
||||
createSpanLink?: (
|
||||
span: TraceSpan
|
||||
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -356,6 +359,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
||||
clippingLeft,
|
||||
clippingRight,
|
||||
theme,
|
||||
createSpanLink,
|
||||
} = this.props;
|
||||
const {
|
||||
duration,
|
||||
@ -437,6 +441,32 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
||||
</span>
|
||||
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
|
||||
</a>
|
||||
{createSpanLink &&
|
||||
(() => {
|
||||
const link = createSpanLink(span);
|
||||
return (
|
||||
<a
|
||||
href={link.href}
|
||||
// Needs to have target otherwise preventDefault would not work due to angularRouter.
|
||||
target={'_blank'}
|
||||
style={{ marginRight: '5px' }}
|
||||
rel="noopener noreferrer"
|
||||
onClick={
|
||||
link.onClick
|
||||
? event => {
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) {
|
||||
event.preventDefault();
|
||||
link.onClick(event);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.content}
|
||||
</a>
|
||||
);
|
||||
})()}
|
||||
|
||||
{span.references && span.references.length > 1 && (
|
||||
<ReferencesButton
|
||||
references={span.references}
|
||||
|
@ -79,6 +79,9 @@ type TVirtualizedTraceViewOwnProps = {
|
||||
addHoverIndentGuideId: (spanID: string) => void;
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
theme: Theme;
|
||||
createSpanLink?: (
|
||||
span: TraceSpan
|
||||
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
|
||||
};
|
||||
|
||||
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
|
||||
@ -330,6 +333,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
||||
addHoverIndentGuideId,
|
||||
removeHoverIndentGuideId,
|
||||
theme,
|
||||
createSpanLink,
|
||||
} = this.props;
|
||||
// to avert flow error
|
||||
if (!trace) {
|
||||
@ -379,6 +383,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
||||
hoverIndentGuideIds={hoverIndentGuideIds}
|
||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||
createSpanLink={createSpanLink}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -99,6 +99,9 @@ type TProps = TExtractUiFindFromStateReturn & {
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[];
|
||||
theme: Theme;
|
||||
createSpanLink?: (
|
||||
span: TraceSpan
|
||||
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataSourceSrv } from '@grafana/runtime';
|
||||
import { DataSourceApi, PluginMeta, DataTransformerConfig } from '@grafana/data';
|
||||
import { DataSourceApi, PluginMeta, DataTransformerConfig, DataSourceInstanceSettings } from '@grafana/data';
|
||||
|
||||
import { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types';
|
||||
import { getAlertingValidationMessage } from './getAlertingValidationMessage';
|
||||
@ -23,6 +23,12 @@ describe('getAlertingValidationMessage', () => {
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
getDataSourceSettingsByUid(): any {},
|
||||
getExternal(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
getAll(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const targets: ElasticsearchQuery[] = [
|
||||
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||
@ -60,6 +66,12 @@ describe('getAlertingValidationMessage', () => {
|
||||
return Promise.resolve(alertingDatasource);
|
||||
},
|
||||
getDataSourceSettingsByUid(): any {},
|
||||
getExternal(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
getAll(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const targets: any[] = [
|
||||
{ refId: 'A', query: 'some query', datasource: 'alertingDatasource' },
|
||||
@ -84,6 +96,12 @@ describe('getAlertingValidationMessage', () => {
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
getDataSourceSettingsByUid(): any {},
|
||||
getExternal(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
getAll(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const targets: ElasticsearchQuery[] = [
|
||||
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||
@ -110,6 +128,12 @@ describe('getAlertingValidationMessage', () => {
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
getDataSourceSettingsByUid(): any {},
|
||||
getExternal(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
getAll(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const targets: ElasticsearchQuery[] = [
|
||||
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
||||
@ -136,6 +160,12 @@ describe('getAlertingValidationMessage', () => {
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
getDataSourceSettingsByUid(): any {},
|
||||
getExternal(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
getAll(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const targets: ElasticsearchQuery[] = [
|
||||
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
||||
|
@ -101,6 +101,7 @@ const dummyProps: ExploreProps = {
|
||||
showLogs: true,
|
||||
showTable: true,
|
||||
showTrace: true,
|
||||
splitOpen: (() => {}) as any,
|
||||
};
|
||||
|
||||
const setupErrors = (hasRefId?: boolean) => {
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
scanStart,
|
||||
setQueries,
|
||||
updateTimeRange,
|
||||
splitOpen,
|
||||
} from './state/actions';
|
||||
|
||||
import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types/explore';
|
||||
@ -120,6 +121,7 @@ export interface ExploreProps {
|
||||
showTable: boolean;
|
||||
showLogs: boolean;
|
||||
showTrace: boolean;
|
||||
splitOpen: typeof splitOpen;
|
||||
}
|
||||
|
||||
enum ExploreDrawer {
|
||||
@ -309,6 +311,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
showTable,
|
||||
showLogs,
|
||||
showTrace,
|
||||
splitOpen,
|
||||
} = this.props;
|
||||
const { openDrawer } = this.state;
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
@ -405,7 +408,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
// We expect only one trace at the moment to be in the dataframe
|
||||
// If there is not data (like 404) we show a separate error so no need to show anything here
|
||||
queryResponse.series[0] && (
|
||||
<TraceView trace={queryResponse.series[0].fields[0].values.get(0) as any} />
|
||||
<TraceView
|
||||
trace={queryResponse.series[0].fields[0].values.get(0) as any}
|
||||
splitOpenFn={splitOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@ -505,6 +511,7 @@ const mapDispatchToProps: Partial<ExploreProps> = {
|
||||
setQueries,
|
||||
updateTimeRange,
|
||||
addQueryRow,
|
||||
splitOpen,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
|
@ -5,7 +5,7 @@ import { TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-c
|
||||
import { TraceSpanData, TraceData } from '@grafana/data';
|
||||
|
||||
function renderTraceView() {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
const wrapper = shallow(<TraceView trace={response} splitOpenFn={() => {}} />);
|
||||
return {
|
||||
timeline: wrapper.find(TraceTimelineViewer),
|
||||
header: wrapper.find(TracePageHeader),
|
||||
|
@ -17,9 +17,11 @@ import { useDetailState } from './useDetailState';
|
||||
import { useHoverIndentGuide } from './useHoverIndentGuide';
|
||||
import { colors, useTheme } from '@grafana/ui';
|
||||
import { TraceData, TraceSpanData, Trace, TraceSpan, TraceKeyValuePair, TraceLink } from '@grafana/data';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
|
||||
type Props = {
|
||||
trace: TraceData & { spans: TraceSpanData[] };
|
||||
splitOpenFn: (options: { datasourceUid: string; query: any }) => void;
|
||||
};
|
||||
|
||||
export function TraceView(props: Props) {
|
||||
@ -77,6 +79,8 @@ export function TraceView(props: Props) {
|
||||
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
|
||||
);
|
||||
|
||||
const createSpanLink = useMemo(() => createSpanLinkFactory(props.splitOpenFn), [props.splitOpenFn]);
|
||||
|
||||
if (!traceProp) {
|
||||
return null;
|
||||
}
|
||||
@ -140,6 +144,7 @@ export function TraceView(props: Props) {
|
||||
[]
|
||||
)}
|
||||
uiFind={search}
|
||||
createSpanLink={createSpanLink}
|
||||
/>
|
||||
</UIElementsContext.Provider>
|
||||
</ThemeProvider>
|
||||
|
88
public/app/features/explore/TraceView/createSpanLink.test.ts
Normal file
88
public/app/features/explore/TraceView/createSpanLink.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { config, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
|
||||
describe('createSpanLinkFactory', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.traceToLogs = true;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.traceToLogs = false;
|
||||
});
|
||||
|
||||
it('returns undefined if there is no loki data source', () => {
|
||||
setDataSourceSrv({
|
||||
getExternal() {
|
||||
return [
|
||||
{
|
||||
meta: {
|
||||
id: 'not loki',
|
||||
},
|
||||
} as DataSourceInstanceSettings,
|
||||
];
|
||||
},
|
||||
} as any);
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn);
|
||||
expect(createLink).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('creates correct link', () => {
|
||||
setDataSourceSrv({
|
||||
getExternal() {
|
||||
return [
|
||||
{
|
||||
name: 'loki1',
|
||||
uid: 'lokiUid',
|
||||
meta: {
|
||||
id: 'loki',
|
||||
},
|
||||
} as DataSourceInstanceSettings,
|
||||
];
|
||||
},
|
||||
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
|
||||
if (uid === 'lokiUid') {
|
||||
return {
|
||||
name: 'Loki1',
|
||||
} as any;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
} as any);
|
||||
|
||||
setTemplateSrv({
|
||||
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string {
|
||||
return target!;
|
||||
},
|
||||
} as any);
|
||||
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn);
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!({
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
process: {
|
||||
tags: [
|
||||
{
|
||||
key: 'cluster',
|
||||
value: 'cluster1',
|
||||
},
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'label2',
|
||||
value: 'val2',
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
} as any);
|
||||
|
||||
expect(linkDef.href).toBe(
|
||||
`/explore?left={"range":{"from":"20201014T005955","to":"20201014T020001"},"datasource":"Loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}`
|
||||
);
|
||||
});
|
||||
});
|
92
public/app/features/explore/TraceView/createSpanLink.tsx
Normal file
92
public/app/features/explore/TraceView/createSpanLink.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { config, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
import { DataLink, dateTime, Field, mapInternalLinkToExplore, TimeRange, TraceSpan } from '@grafana/data';
|
||||
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
||||
import { Icon } from '@grafana/ui';
|
||||
|
||||
/**
|
||||
* This is a factory for the link creator. It returns the function mainly so it can return undefined in which case
|
||||
* the trace view won't create any links and to capture the datasource and split function making it easier to memoize
|
||||
* with useMemo.
|
||||
*/
|
||||
export function createSpanLinkFactory(splitOpenFn: (options: { datasourceUid: string; query: any }) => void) {
|
||||
if (!config.featureToggles.traceToLogs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Right now just hardcoded for first loki DS we can find
|
||||
const lokiDs = getDataSourceSrv()
|
||||
.getExternal()
|
||||
.find(ds => ds.meta.id === 'loki');
|
||||
|
||||
if (!lokiDs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function(span: TraceSpan): { href: string; onClick?: (event: any) => void; content: React.ReactNode } {
|
||||
// This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at
|
||||
// the moment. Issue is that the trace itself isn't clearly mapped to dataFrame (right now it's just a json blob
|
||||
// inside a single field) so the dataLinks as config of that dataFrame abstraction breaks down a bit and we do
|
||||
// it manually here instead of leaving it for the data source to supply the config.
|
||||
|
||||
const dataLink: DataLink<LokiQuery> = {
|
||||
title: lokiDs.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: lokiDs.uid,
|
||||
query: {
|
||||
expr: getLokiQueryFromSpan(span),
|
||||
refId: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const link = mapInternalLinkToExplore(dataLink, {}, getTimeRangeFromSpan(span), {} as Field, {
|
||||
onClickFn: splitOpenFn,
|
||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||
getDataSourceSettingsByUid: getDataSourceSrv().getDataSourceSettingsByUid.bind(getDataSourceSrv()),
|
||||
});
|
||||
return {
|
||||
href: link.href,
|
||||
onClick: link.onClick,
|
||||
content: <Icon name="file-alt" title="Show logs" />,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Right now this is just hardcoded and later will probably be part of some user configuration.
|
||||
*/
|
||||
const allowedKeys = ['cluster', 'hostname', 'namespace', 'pod'];
|
||||
|
||||
function getLokiQueryFromSpan(span: TraceSpan): string {
|
||||
const tags = span.process.tags.reduce((acc, tag) => {
|
||||
if (allowedKeys.includes(tag.key)) {
|
||||
acc.push(`${tag.key}="${tag.value}"`);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
return `{${tags.join(', ')}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a time range from the span. Naively this could be just start and end time of the span but we also want some
|
||||
* buffer around that just so we do not miss some logs which may not have timestamps aligned with the span. Right
|
||||
* now the buffers are hardcoded which may be a bit weird for very short spans but at the same time, fractional buffers
|
||||
* with very short spans could mean microseconds and that could miss some logs relevant to that spans. In the future
|
||||
* something more intelligent should probably be implemented
|
||||
*/
|
||||
function getTimeRangeFromSpan(span: TraceSpan): TimeRange {
|
||||
const from = dateTime(span.startTime / 1000 - 5 * 1000);
|
||||
const spanEndMs = (span.startTime + span.duration) / 1000;
|
||||
const to = dateTime(spanEndMs + 1000 * 60 * 60);
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
// Weirdly Explore does not handle ISO string which would have been the default stringification if passed as object
|
||||
// and we have to use this custom format :( .
|
||||
raw: {
|
||||
from: from.format('YYYYMMDDTHHmmss'),
|
||||
to: to.format('YYYYMMDDTHHmmss'),
|
||||
},
|
||||
};
|
||||
}
|
@ -678,7 +678,11 @@ 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<T extends DataQuery = any>(options?: { datasourceUid: string; query: T }): ThunkResult<void> {
|
||||
export function splitOpen<T extends DataQuery = any>(options?: {
|
||||
datasourceUid: string;
|
||||
query: T;
|
||||
range?: TimeRange;
|
||||
}): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
// Clone left state to become the right state
|
||||
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
||||
@ -696,6 +700,10 @@ export function splitOpen<T extends DataQuery = any>(options?: { datasourceUid:
|
||||
rightState.queryKeys = [];
|
||||
urlState.queries = [];
|
||||
rightState.urlState = urlState;
|
||||
if (options.range) {
|
||||
urlState.range = options.range.raw;
|
||||
rightState.range = options.range;
|
||||
}
|
||||
|
||||
dispatch(splitOpenAction({ itemState: rightState }));
|
||||
|
||||
@ -707,6 +715,7 @@ export function splitOpen<T extends DataQuery = any>(options?: { datasourceUid:
|
||||
];
|
||||
|
||||
const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.datasourceUid);
|
||||
|
||||
await dispatch(changeDatasource(ExploreId.right, dataSourceSettings!.name));
|
||||
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
|
||||
await dispatch(runQueries(ExploreId.right));
|
||||
|
@ -57,7 +57,11 @@ describe('getFieldLinksForExplore', () => {
|
||||
links[0].onClick({});
|
||||
}
|
||||
|
||||
expect(splitfn).toBeCalledWith({ datasourceUid: 'uid_1', query: { query: 'query_1' } });
|
||||
expect(splitfn).toBeCalledWith({
|
||||
datasourceUid: 'uid_1',
|
||||
query: { query: 'query_1' },
|
||||
range,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -100,8 +104,8 @@ function setup(link: DataLink) {
|
||||
};
|
||||
|
||||
const range: TimeRange = {
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
from: dateTime('2020-10-14T00:00:00'),
|
||||
to: dateTime('2020-10-14T01:00:00'),
|
||||
raw: {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
|
@ -1,8 +1,7 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { splitOpen } from '../state/actions';
|
||||
import { Field, LinkModel, TimeRange } from '@grafana/data';
|
||||
import { Field, LinkModel, TimeRange, mapInternalLinkToExplore } from '@grafana/data';
|
||||
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks';
|
||||
import { getDataSourceSrv, getTemplateSrv, getBackendSrv, config } from '@grafana/runtime';
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user