Commit c8658f3e by Andrej Ocenas Committed by GitHub

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
parent 26e2faa7
...@@ -35,6 +35,8 @@ export interface FeatureToggles { ...@@ -35,6 +35,8 @@ export interface FeatureToggles {
live: boolean; live: boolean;
expressions: boolean; expressions: boolean;
ngalert: boolean; ngalert: boolean;
// Just for demo at the moment
traceToLogs: boolean;
/** /**
* @remarks * @remarks
......
...@@ -51,6 +51,7 @@ export type TraceSpanData = { ...@@ -51,6 +51,7 @@ export type TraceSpanData = {
traceID: string; traceID: string;
processID: string; processID: string;
operationName: string; operationName: string;
// Times are in microseconds
startTime: number; startTime: number;
duration: number; duration: number;
logs: TraceLog[]; logs: TraceLog[];
......
...@@ -26,8 +26,9 @@ export const DataLinkBuiltInVars = { ...@@ -26,8 +26,9 @@ export const DataLinkBuiltInVars = {
valueCalc: '__value.calc', valueCalc: '__value.calc',
}; };
// We inject these because we cannot import them directly as they reside inside grafana main package.
type Options = { type Options = {
onClickFn?: (options: { datasourceUid: string; query: any }) => void; onClickFn?: (options: { datasourceUid: string; query: any; range?: TimeRange }) => void;
replaceVariables: InterpolateFunction; replaceVariables: InterpolateFunction;
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined; getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
}; };
...@@ -62,6 +63,7 @@ export function mapInternalLinkToExplore( ...@@ -62,6 +63,7 @@ export function mapInternalLinkToExplore(
onClickFn?.({ onClickFn?.({
datasourceUid: link.internal!.datasourceUid, datasourceUid: link.internal!.datasourceUid,
query: interpolatedQuery, query: interpolatedQuery,
range,
}); });
} }
: undefined, : undefined,
......
...@@ -15,6 +15,6 @@ export { getMappedValue } from './valueMappings'; ...@@ -15,6 +15,6 @@ export { getMappedValue } from './valueMappings';
export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
export { locationUtil } from './location'; export { locationUtil } from './location';
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url'; export { urlUtil, UrlQueryMap, UrlQueryValue } from './url';
export { DataLinkBuiltInVars } from './dataLinks'; export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks';
export { DocsId } from './docs'; export { DocsId } from './docs';
export { observableTester } from './tests/observableTester'; export { observableTester } from './tests/observableTester';
...@@ -56,6 +56,7 @@ export class GrafanaBootConfig implements GrafanaConfig { ...@@ -56,6 +56,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
expressions: false, expressions: false,
meta: false, meta: false,
ngalert: false, ngalert: false,
traceToLogs: false,
}; };
licenseInfo: LicenseInfo = {} as LicenseInfo; licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false; rendererAvailable = false;
......
...@@ -19,6 +19,16 @@ export interface DataSourceSrv { ...@@ -19,6 +19,16 @@ export interface DataSourceSrv {
* Returns metadata based on UID. * Returns metadata based on UID.
*/ */
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined; 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; let singletonInstance: DataSourceSrv;
......
...@@ -310,6 +310,9 @@ type SpanBarRowProps = { ...@@ -310,6 +310,9 @@ type SpanBarRowProps = {
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
clippingLeft?: boolean; clippingLeft?: boolean;
clippingRight?: 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> { ...@@ -356,6 +359,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
clippingLeft, clippingLeft,
clippingRight, clippingRight,
theme, theme,
createSpanLink,
} = this.props; } = this.props;
const { const {
duration, duration,
...@@ -437,6 +441,32 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> { ...@@ -437,6 +441,32 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
</span> </span>
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small> <small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
</a> </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 && ( {span.references && span.references.length > 1 && (
<ReferencesButton <ReferencesButton
references={span.references} references={span.references}
......
...@@ -79,6 +79,9 @@ type TVirtualizedTraceViewOwnProps = { ...@@ -79,6 +79,9 @@ type TVirtualizedTraceViewOwnProps = {
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: Theme; theme: Theme;
createSpanLink?: (
span: TraceSpan
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
}; };
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline; type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
...@@ -330,6 +333,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra ...@@ -330,6 +333,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
addHoverIndentGuideId, addHoverIndentGuideId,
removeHoverIndentGuideId, removeHoverIndentGuideId,
theme, theme,
createSpanLink,
} = this.props; } = this.props;
// to avert flow error // to avert flow error
if (!trace) { if (!trace) {
...@@ -379,6 +383,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra ...@@ -379,6 +383,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
hoverIndentGuideIds={hoverIndentGuideIds} hoverIndentGuideIds={hoverIndentGuideIds}
addHoverIndentGuideId={addHoverIndentGuideId} addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId} removeHoverIndentGuideId={removeHoverIndentGuideId}
createSpanLink={createSpanLink}
/> />
</div> </div>
); );
......
...@@ -99,6 +99,9 @@ type TProps = TExtractUiFindFromStateReturn & { ...@@ -99,6 +99,9 @@ type TProps = TExtractUiFindFromStateReturn & {
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[]; linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[];
theme: Theme; theme: Theme;
createSpanLink?: (
span: TraceSpan
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
}; };
type State = { type State = {
......
import { DataSourceSrv } from '@grafana/runtime'; 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 { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types';
import { getAlertingValidationMessage } from './getAlertingValidationMessage'; import { getAlertingValidationMessage } from './getAlertingValidationMessage';
...@@ -23,6 +23,12 @@ describe('getAlertingValidationMessage', () => { ...@@ -23,6 +23,12 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
return [];
},
}; };
const targets: ElasticsearchQuery[] = [ const targets: ElasticsearchQuery[] = [
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false }, { refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
...@@ -60,6 +66,12 @@ describe('getAlertingValidationMessage', () => { ...@@ -60,6 +66,12 @@ describe('getAlertingValidationMessage', () => {
return Promise.resolve(alertingDatasource); return Promise.resolve(alertingDatasource);
}, },
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
return [];
},
}; };
const targets: any[] = [ const targets: any[] = [
{ refId: 'A', query: 'some query', datasource: 'alertingDatasource' }, { refId: 'A', query: 'some query', datasource: 'alertingDatasource' },
...@@ -84,6 +96,12 @@ describe('getAlertingValidationMessage', () => { ...@@ -84,6 +96,12 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
return [];
},
}; };
const targets: ElasticsearchQuery[] = [ const targets: ElasticsearchQuery[] = [
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false }, { refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
...@@ -110,6 +128,12 @@ describe('getAlertingValidationMessage', () => { ...@@ -110,6 +128,12 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
return [];
},
}; };
const targets: ElasticsearchQuery[] = [ const targets: ElasticsearchQuery[] = [
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false }, { refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
...@@ -136,6 +160,12 @@ describe('getAlertingValidationMessage', () => { ...@@ -136,6 +160,12 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
return [];
},
}; };
const targets: ElasticsearchQuery[] = [ const targets: ElasticsearchQuery[] = [
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false }, { refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
......
...@@ -101,6 +101,7 @@ const dummyProps: ExploreProps = { ...@@ -101,6 +101,7 @@ const dummyProps: ExploreProps = {
showLogs: true, showLogs: true,
showTable: true, showTable: true,
showTrace: true, showTrace: true,
splitOpen: (() => {}) as any,
}; };
const setupErrors = (hasRefId?: boolean) => { const setupErrors = (hasRefId?: boolean) => {
......
...@@ -37,6 +37,7 @@ import { ...@@ -37,6 +37,7 @@ import {
scanStart, scanStart,
setQueries, setQueries,
updateTimeRange, updateTimeRange,
splitOpen,
} from './state/actions'; } from './state/actions';
import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types/explore'; import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types/explore';
...@@ -120,6 +121,7 @@ export interface ExploreProps { ...@@ -120,6 +121,7 @@ export interface ExploreProps {
showTable: boolean; showTable: boolean;
showLogs: boolean; showLogs: boolean;
showTrace: boolean; showTrace: boolean;
splitOpen: typeof splitOpen;
} }
enum ExploreDrawer { enum ExploreDrawer {
...@@ -309,6 +311,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -309,6 +311,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
showTable, showTable,
showLogs, showLogs,
showTrace, showTrace,
splitOpen,
} = this.props; } = this.props;
const { openDrawer } = this.state; const { openDrawer } = this.state;
const exploreClass = split ? 'explore explore-split' : 'explore'; const exploreClass = split ? 'explore explore-split' : 'explore';
...@@ -405,7 +408,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -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 // 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 // If there is not data (like 404) we show a separate error so no need to show anything here
queryResponse.series[0] && ( 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> = { ...@@ -505,6 +511,7 @@ const mapDispatchToProps: Partial<ExploreProps> = {
setQueries, setQueries,
updateTimeRange, updateTimeRange,
addQueryRow, addQueryRow,
splitOpen,
}; };
export default compose( export default compose(
......
...@@ -5,7 +5,7 @@ import { TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-c ...@@ -5,7 +5,7 @@ import { TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-c
import { TraceSpanData, TraceData } from '@grafana/data'; import { TraceSpanData, TraceData } from '@grafana/data';
function renderTraceView() { function renderTraceView() {
const wrapper = shallow(<TraceView trace={response} />); const wrapper = shallow(<TraceView trace={response} splitOpenFn={() => {}} />);
return { return {
timeline: wrapper.find(TraceTimelineViewer), timeline: wrapper.find(TraceTimelineViewer),
header: wrapper.find(TracePageHeader), header: wrapper.find(TracePageHeader),
......
...@@ -17,9 +17,11 @@ import { useDetailState } from './useDetailState'; ...@@ -17,9 +17,11 @@ import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide'; import { useHoverIndentGuide } from './useHoverIndentGuide';
import { colors, useTheme } from '@grafana/ui'; import { colors, useTheme } from '@grafana/ui';
import { TraceData, TraceSpanData, Trace, TraceSpan, TraceKeyValuePair, TraceLink } from '@grafana/data'; import { TraceData, TraceSpanData, Trace, TraceSpan, TraceKeyValuePair, TraceLink } from '@grafana/data';
import { createSpanLinkFactory } from './createSpanLink';
type Props = { type Props = {
trace: TraceData & { spans: TraceSpanData[] }; trace: TraceData & { spans: TraceSpanData[] };
splitOpenFn: (options: { datasourceUid: string; query: any }) => void;
}; };
export function TraceView(props: Props) { export function TraceView(props: Props) {
...@@ -77,6 +79,8 @@ export function TraceView(props: Props) { ...@@ -77,6 +79,8 @@ export function TraceView(props: Props) {
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID] [childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
); );
const createSpanLink = useMemo(() => createSpanLinkFactory(props.splitOpenFn), [props.splitOpenFn]);
if (!traceProp) { if (!traceProp) {
return null; return null;
} }
...@@ -140,6 +144,7 @@ export function TraceView(props: Props) { ...@@ -140,6 +144,7 @@ export function TraceView(props: Props) {
[] []
)} )}
uiFind={search} uiFind={search}
createSpanLink={createSpanLink}
/> />
</UIElementsContext.Provider> </UIElementsContext.Provider>
</ThemeProvider> </ThemeProvider>
......
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":""}]}`
);
});
});
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> { ...@@ -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 * 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<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) => { 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];
...@@ -696,6 +700,10 @@ export function splitOpen<T extends DataQuery = any>(options?: { datasourceUid: ...@@ -696,6 +700,10 @@ export function splitOpen<T extends DataQuery = any>(options?: { datasourceUid:
rightState.queryKeys = []; rightState.queryKeys = [];
urlState.queries = []; urlState.queries = [];
rightState.urlState = urlState; rightState.urlState = urlState;
if (options.range) {
urlState.range = options.range.raw;
rightState.range = options.range;
}
dispatch(splitOpenAction({ itemState: rightState })); dispatch(splitOpenAction({ itemState: rightState }));
...@@ -707,6 +715,7 @@ export function splitOpen<T extends DataQuery = any>(options?: { datasourceUid: ...@@ -707,6 +715,7 @@ export function splitOpen<T extends DataQuery = any>(options?: { datasourceUid:
]; ];
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 }));
await dispatch(runQueries(ExploreId.right)); await dispatch(runQueries(ExploreId.right));
......
...@@ -57,7 +57,11 @@ describe('getFieldLinksForExplore', () => { ...@@ -57,7 +57,11 @@ describe('getFieldLinksForExplore', () => {
links[0].onClick({}); 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) { ...@@ -100,8 +104,8 @@ function setup(link: DataLink) {
}; };
const range: TimeRange = { const range: TimeRange = {
from: dateTime(), from: dateTime('2020-10-14T00:00:00'),
to: dateTime(), to: dateTime('2020-10-14T01:00:00'),
raw: { raw: {
from: 'now-1h', from: 'now-1h',
to: 'now', to: 'now',
......
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { splitOpen } from '../state/actions'; 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 { getLinkSrv } from '../../panel/panellinks/link_srv';
import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks';
import { getDataSourceSrv, getTemplateSrv, getBackendSrv, config } from '@grafana/runtime'; import { getDataSourceSrv, getTemplateSrv, getBackendSrv, config } from '@grafana/runtime';
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment