mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataLinks: Add internal links in table and allow custom query (#25613)
* Add internal links in table and with custom query * Add specific types for internal and external link * Change the datalink types to be more backward compatible * Refactor the link utils for explore * Add internal linking to table panels * Fix derived field condition * Prettify * Add and fix tests * Prettify * Fix imports and tests * Remove unused type * Update packages/grafana-data/src/types/explore.ts Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> * Update packages/grafana-data/src/types/explore.ts Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import { fieldIndexComparer } from '../field/fieldComparers';
|
|||||||
|
|
||||||
function convertTableToDataFrame(table: TableData): DataFrame {
|
function convertTableToDataFrame(table: TableData): DataFrame {
|
||||||
const fields = table.columns.map(c => {
|
const fields = table.columns.map(c => {
|
||||||
|
// TODO: should be Column but type does not exists there so not sure whats up here.
|
||||||
const { text, type, ...disp } = c as any;
|
const { text, type, ...disp } = c as any;
|
||||||
return {
|
return {
|
||||||
name: text, // rename 'text' to the 'name' field
|
name: text, // rename 'text' to the 'name' field
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ describe('applyFieldOverrides', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
replaceVariables: (value: any) => value,
|
replaceVariables: (value: any) => value,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
|
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
|
||||||
});
|
});
|
||||||
@@ -187,6 +188,7 @@ describe('applyFieldOverrides', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
fieldConfigRegistry: customFieldRegistry,
|
fieldConfigRegistry: customFieldRegistry,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
replaceVariables: v => v,
|
replaceVariables: v => v,
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
})[0];
|
})[0];
|
||||||
@@ -204,6 +206,7 @@ describe('applyFieldOverrides', () => {
|
|||||||
data: [f0], // the frame
|
data: [f0], // the frame
|
||||||
fieldConfig: src as FieldConfigSource, // defaults + overrides
|
fieldConfig: src as FieldConfigSource, // defaults + overrides
|
||||||
replaceVariables: (undefined as any) as InterpolateFunction,
|
replaceVariables: (undefined as any) as InterpolateFunction,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
theme: (undefined as any) as GrafanaTheme,
|
theme: (undefined as any) as GrafanaTheme,
|
||||||
fieldConfigRegistry: customFieldRegistry,
|
fieldConfigRegistry: customFieldRegistry,
|
||||||
})[0];
|
})[0];
|
||||||
@@ -231,6 +234,7 @@ describe('applyFieldOverrides', () => {
|
|||||||
data: [f0], // the frame
|
data: [f0], // the frame
|
||||||
fieldConfig: src as FieldConfigSource, // defaults + overrides
|
fieldConfig: src as FieldConfigSource, // defaults + overrides
|
||||||
replaceVariables: (undefined as any) as InterpolateFunction,
|
replaceVariables: (undefined as any) as InterpolateFunction,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
theme: (undefined as any) as GrafanaTheme,
|
theme: (undefined as any) as GrafanaTheme,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
})[0];
|
})[0];
|
||||||
@@ -478,11 +482,72 @@ describe('getLinksSupplier', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const replaceSpy = jest.fn();
|
const replaceSpy = jest.fn();
|
||||||
const supplier = getLinksSupplier(f0, f0.fields[0], {}, replaceSpy, { theme: {} as GrafanaTheme });
|
const supplier = getLinksSupplier(
|
||||||
|
f0,
|
||||||
|
f0.fields[0],
|
||||||
|
{},
|
||||||
|
replaceSpy,
|
||||||
|
// this is used only for internal links so isn't needed here
|
||||||
|
() => ({} as any),
|
||||||
|
{
|
||||||
|
theme: {} as GrafanaTheme,
|
||||||
|
}
|
||||||
|
);
|
||||||
supplier({});
|
supplier({});
|
||||||
|
|
||||||
expect(replaceSpy).toBeCalledTimes(2);
|
expect(replaceSpy).toBeCalledTimes(2);
|
||||||
expect(replaceSpy.mock.calls[0][0]).toEqual('url to be interpolated');
|
expect(replaceSpy.mock.calls[0][0]).toEqual('url to be interpolated');
|
||||||
expect(replaceSpy.mock.calls[1][0]).toEqual('title to be interpolated');
|
expect(replaceSpy.mock.calls[1][0]).toEqual('title to be interpolated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles internal links', () => {
|
||||||
|
locationUtil.initialize({
|
||||||
|
getConfig: () => ({ appSubUrl: '' } as any),
|
||||||
|
buildParamsFromVariables: (() => {}) as any,
|
||||||
|
getTimeRangeForUrl: (() => {}) as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const f0 = new MutableDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: [10, 20],
|
||||||
|
config: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
internal: {
|
||||||
|
datasourceUid: '0',
|
||||||
|
query: '12345',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const supplier = getLinksSupplier(
|
||||||
|
f0,
|
||||||
|
f0.fields[0],
|
||||||
|
{},
|
||||||
|
// We do not need to interpolate anything for this test
|
||||||
|
(value, vars, format) => value,
|
||||||
|
uid => ({ name: 'testDS' } as any),
|
||||||
|
{ theme: {} as GrafanaTheme }
|
||||||
|
);
|
||||||
|
const links = supplier({ valueRowIndex: 0 });
|
||||||
|
expect(links.length).toBe(1);
|
||||||
|
expect(links[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'testDS',
|
||||||
|
href:
|
||||||
|
'/explore?left={"datasource":"testDS","queries":["12345"],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}',
|
||||||
|
onClick: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
ValueLinkConfig,
|
ValueLinkConfig,
|
||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
|
DataLink,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||||
import { FieldMatcher } from '../types/transformations';
|
import { FieldMatcher } from '../types/transformations';
|
||||||
@@ -33,6 +35,7 @@ import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
|||||||
import { formatLabels } from '../utils/labels';
|
import { formatLabels } from '../utils/labels';
|
||||||
import { getFrameDisplayName, getFieldDisplayName } from './fieldState';
|
import { getFrameDisplayName, getFieldDisplayName } from './fieldState';
|
||||||
import { getTimeField } from '../dataframe/processDataFrame';
|
import { getTimeField } from '../dataframe/processDataFrame';
|
||||||
|
import { mapInternalLinkToExplore } from '../utils/dataLinks';
|
||||||
|
|
||||||
interface OverrideProps {
|
interface OverrideProps {
|
||||||
match: FieldMatcher;
|
match: FieldMatcher;
|
||||||
@@ -129,6 +132,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
|||||||
data: options.data!,
|
data: options.data!,
|
||||||
dataFrameIndex: index,
|
dataFrameIndex: index,
|
||||||
replaceVariables: options.replaceVariables,
|
replaceVariables: options.replaceVariables,
|
||||||
|
getDataSourceSettingsByUid: options.getDataSourceSettingsByUid,
|
||||||
fieldConfigRegistry: fieldConfigRegistry,
|
fieldConfigRegistry: fieldConfigRegistry,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,10 +210,17 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Attach data links supplier
|
// Attach data links supplier
|
||||||
f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, {
|
f.getLinks = getLinksSupplier(
|
||||||
theme: options.theme,
|
frame,
|
||||||
timeZone: options.timeZone,
|
f,
|
||||||
});
|
fieldScopedVars,
|
||||||
|
context.replaceVariables,
|
||||||
|
context.getDataSourceSettingsByUid,
|
||||||
|
{
|
||||||
|
theme: options.theme,
|
||||||
|
timeZone: options.timeZone,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return f;
|
return f;
|
||||||
});
|
});
|
||||||
@@ -348,6 +359,7 @@ export const getLinksSupplier = (
|
|||||||
field: Field,
|
field: Field,
|
||||||
fieldScopedVars: ScopedVars,
|
fieldScopedVars: ScopedVars,
|
||||||
replaceVariables: InterpolateFunction,
|
replaceVariables: InterpolateFunction,
|
||||||
|
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined,
|
||||||
options: {
|
options: {
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
@@ -359,20 +371,11 @@ export const getLinksSupplier = (
|
|||||||
const timeRangeUrl = locationUtil.getTimeRangeUrlParams();
|
const timeRangeUrl = locationUtil.getTimeRangeUrlParams();
|
||||||
const { timeField } = getTimeField(frame);
|
const { timeField } = getTimeField(frame);
|
||||||
|
|
||||||
return field.config.links.map(link => {
|
return field.config.links.map((link: DataLink) => {
|
||||||
let href = link.url;
|
const variablesQuery = locationUtil.getVariablesUrlParams();
|
||||||
let dataFrameVars = {};
|
let dataFrameVars = {};
|
||||||
let valueVars = {};
|
let valueVars = {};
|
||||||
|
|
||||||
const info: LinkModel<Field> = {
|
|
||||||
href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')),
|
|
||||||
title: link.title || '',
|
|
||||||
target: link.targetBlank ? '_blank' : undefined,
|
|
||||||
origin: field,
|
|
||||||
};
|
|
||||||
|
|
||||||
const variablesQuery = locationUtil.getVariablesUrlParams();
|
|
||||||
|
|
||||||
// We are not displaying reduction result
|
// We are not displaying reduction result
|
||||||
if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) {
|
if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) {
|
||||||
const fieldsProxy = getFieldDisplayValuesProxy(frame, config.valueRowIndex, options);
|
const fieldsProxy = getFieldDisplayValuesProxy(frame, config.valueRowIndex, options);
|
||||||
@@ -419,10 +422,25 @@ export const getLinksSupplier = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
info.href = replaceVariables(info.href, variables);
|
if (link.internal) {
|
||||||
info.title = replaceVariables(info.title, variables);
|
// For internal links at the moment only destination is Explore.
|
||||||
info.href = locationUtil.processUrl(info.href);
|
return mapInternalLinkToExplore(link, variables, {} as any, field, {
|
||||||
|
replaceVariables,
|
||||||
|
getDataSourceSettingsByUid,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let href = locationUtil.assureBaseUrl(link.url.replace(/\n/g, ''));
|
||||||
|
href = replaceVariables(href, variables);
|
||||||
|
href = locationUtil.processUrl(href);
|
||||||
|
|
||||||
return info;
|
const info: LinkModel<Field> = {
|
||||||
|
href,
|
||||||
|
title: replaceVariables(link.title || '', variables),
|
||||||
|
target: link.targetBlank ? '_blank' : undefined,
|
||||||
|
origin: field,
|
||||||
|
};
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('getFieldDisplayValuesProxy', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
replaceVariables: (val: string) => val,
|
replaceVariables: (val: string) => val,
|
||||||
|
getDataSourceSettingsByUid: (val: string) => ({} as any),
|
||||||
timeZone: 'utc',
|
timeZone: 'utc',
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ScopedVars } from './ScopedVars';
|
import { ScopedVars } from './ScopedVars';
|
||||||
|
import { DataQuery } from './datasource';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback info for DataLink click events
|
* Callback info for DataLink click events
|
||||||
@@ -10,10 +11,13 @@ export interface DataLinkClickEvent<T = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link configuration. The values may contain variables that need to be
|
* Link configuration. The values may contain variables that need to be
|
||||||
* processed before running
|
* processed before showing the link to user.
|
||||||
|
*
|
||||||
|
* TODO: <T extends DataQuery> is not strictly true for internal links as we do not need refId for example but all
|
||||||
|
* data source defined queries extend this so this is more for documentation.
|
||||||
*/
|
*/
|
||||||
export interface DataLink {
|
export interface DataLink<T extends DataQuery = any> {
|
||||||
title: string;
|
title: string;
|
||||||
targetBlank?: boolean;
|
targetBlank?: boolean;
|
||||||
|
|
||||||
@@ -28,16 +32,19 @@ export interface DataLink {
|
|||||||
// Not saved in JSON/DTO
|
// Not saved in JSON/DTO
|
||||||
onClick?: (event: DataLinkClickEvent) => void;
|
onClick?: (event: DataLinkClickEvent) => void;
|
||||||
|
|
||||||
// At the moment this is used for derived fields for metadata about internal linking.
|
// If dataLink represents internal link this has to be filled. Internal link is defined as a query in a particular
|
||||||
meta?: {
|
// datas ource that we want to show to the user. Usually this results in a link to explore but can also lead to
|
||||||
datasourceUid?: string;
|
// more custom onClick behaviour if needed.
|
||||||
|
internal?: {
|
||||||
|
query: T;
|
||||||
|
datasourceUid: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processed Link Model. The values are ready to use
|
* Processed Link Model. The values are ready to use
|
||||||
*/
|
*/
|
||||||
export interface LinkModel<T = any> {
|
export interface LinkModel<T = any> {
|
||||||
href: string;
|
href: string;
|
||||||
|
|||||||
22
packages/grafana-data/src/types/explore.ts
Normal file
22
packages/grafana-data/src/types/explore.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ExploreMode } from './datasource';
|
||||||
|
import { RawTimeRange } from './time';
|
||||||
|
import { LogsDedupStrategy } from './logs';
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export interface ExploreUrlState {
|
||||||
|
datasource: string;
|
||||||
|
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
||||||
|
mode: ExploreMode;
|
||||||
|
range: RawTimeRange;
|
||||||
|
ui: ExploreUIState;
|
||||||
|
originPanelId?: number;
|
||||||
|
context?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export interface ExploreUIState {
|
||||||
|
showingTable: boolean;
|
||||||
|
showingGraph: boolean;
|
||||||
|
showingLogs: boolean;
|
||||||
|
dedupStrategy?: LogsDedupStrategy;
|
||||||
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import { MatcherConfig, FieldConfig, Field, DataFrame, GrafanaTheme, TimeZone } from '../types';
|
import {
|
||||||
|
MatcherConfig,
|
||||||
|
FieldConfig,
|
||||||
|
Field,
|
||||||
|
DataFrame,
|
||||||
|
GrafanaTheme,
|
||||||
|
TimeZone,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
} from '../types';
|
||||||
import { InterpolateFunction } from './panel';
|
import { InterpolateFunction } from './panel';
|
||||||
import { StandardEditorProps, FieldConfigOptionsRegistry, StandardEditorContext } from '../field';
|
import { StandardEditorProps, FieldConfigOptionsRegistry, StandardEditorContext } from '../field';
|
||||||
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
|
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
|
||||||
@@ -106,6 +114,7 @@ export interface ApplyFieldOverrideOptions {
|
|||||||
data?: DataFrame[];
|
data?: DataFrame[];
|
||||||
fieldConfig: FieldConfigSource;
|
fieldConfig: FieldConfigSource;
|
||||||
replaceVariables: InterpolateFunction;
|
replaceVariables: InterpolateFunction;
|
||||||
|
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
autoMinMax?: boolean;
|
autoMinMax?: boolean;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export * from './theme';
|
|||||||
export * from './orgs';
|
export * from './orgs';
|
||||||
export * from './flot';
|
export * from './flot';
|
||||||
export * from './trace';
|
export * from './trace';
|
||||||
|
export * from './explore';
|
||||||
|
|
||||||
import * as AppEvents from './appEvents';
|
import * as AppEvents from './appEvents';
|
||||||
import { AppEvent } from './appEvents';
|
import { AppEvent } from './appEvents';
|
||||||
|
|||||||
39
packages/grafana-data/src/utils/dataLinks.test.ts
Normal file
39
packages/grafana-data/src/utils/dataLinks.test.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { mapInternalLinkToExplore } from './dataLinks';
|
||||||
|
import { FieldType } from '../types';
|
||||||
|
import { ArrayVector } from '../vector';
|
||||||
|
|
||||||
|
describe('mapInternalLinkToExplore', () => {
|
||||||
|
it('creates internal link', () => {
|
||||||
|
const link = mapInternalLinkToExplore(
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
internal: {
|
||||||
|
datasourceUid: 'uid',
|
||||||
|
query: { query: '12344' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{} as any,
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([2]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
replaceVariables: val => val,
|
||||||
|
getDataSourceSettingsByUid: uid => ({ name: 'testDS' } as any),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(link).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'testDS',
|
||||||
|
href:
|
||||||
|
'/explore?left={"datasource":"testDS","queries":[{"query":"12344"}],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}',
|
||||||
|
onClick: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
import {
|
||||||
|
DataLink,
|
||||||
|
DataQuery,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
ExploreMode,
|
||||||
|
Field,
|
||||||
|
InterpolateFunction,
|
||||||
|
LinkModel,
|
||||||
|
ScopedVars,
|
||||||
|
TimeRange,
|
||||||
|
} from '../types';
|
||||||
|
import { locationUtil } from './location';
|
||||||
|
import { serializeStateToUrlParam } from './url';
|
||||||
|
|
||||||
export const DataLinkBuiltInVars = {
|
export const DataLinkBuiltInVars = {
|
||||||
keepTime: '__url_time_range',
|
keepTime: '__url_time_range',
|
||||||
timeRangeFrom: '__from',
|
timeRangeFrom: '__from',
|
||||||
@@ -12,3 +26,96 @@ export const DataLinkBuiltInVars = {
|
|||||||
// name of the calculation represented by the value
|
// name of the calculation represented by the value
|
||||||
valueCalc: '__value.calc',
|
valueCalc: '__value.calc',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
onClickFn?: (options: { datasourceUid: string; query: any }) => void;
|
||||||
|
replaceVariables: InterpolateFunction;
|
||||||
|
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mapInternalLinkToExplore(
|
||||||
|
link: DataLink,
|
||||||
|
scopedVars: ScopedVars,
|
||||||
|
range: TimeRange,
|
||||||
|
field: Field,
|
||||||
|
options: Options
|
||||||
|
): LinkModel<Field> {
|
||||||
|
if (!link.internal) {
|
||||||
|
throw new Error('Trying to map external link as internal');
|
||||||
|
}
|
||||||
|
const { onClickFn, replaceVariables, getDataSourceSettingsByUid } = options;
|
||||||
|
|
||||||
|
const interpolatedQuery = interpolateQuery(link, scopedVars, replaceVariables);
|
||||||
|
return {
|
||||||
|
title: link.title
|
||||||
|
? replaceVariables(link.title || '', scopedVars)
|
||||||
|
: getDataSourceSettingsByUid(link.internal.datasourceUid)?.name || 'Unknown datasource',
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
getDataSourceSettingsByUid(link.internal.datasourceUid)?.name || 'unknown',
|
||||||
|
interpolatedQuery,
|
||||||
|
range
|
||||||
|
),
|
||||||
|
onClick: onClickFn
|
||||||
|
? () => {
|
||||||
|
onClickFn?.({
|
||||||
|
datasourceUid: link.internal!.datasourceUid,
|
||||||
|
query: interpolatedQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
target: '_self',
|
||||||
|
origin: field,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates href for internal derived field link.
|
||||||
|
*/
|
||||||
|
function generateInternalHref<T extends DataQuery = any>(datasourceName: string, query: T, range: TimeRange): string {
|
||||||
|
return locationUtil.assureBaseUrl(
|
||||||
|
`/explore?left=${serializeStateToUrlParam({
|
||||||
|
range: range.raw,
|
||||||
|
datasource: datasourceName,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateQuery<T extends DataQuery = any>(
|
||||||
|
link: DataLink,
|
||||||
|
scopedVars: ScopedVars,
|
||||||
|
replaceVariables: InterpolateFunction
|
||||||
|
): T {
|
||||||
|
let stringifiedQuery = '';
|
||||||
|
try {
|
||||||
|
stringifiedQuery = JSON.stringify(link.internal?.query || '');
|
||||||
|
} catch (err) {
|
||||||
|
// should not happen and not much to do about this, possibly something non stringifiable in the query
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace any variables inside the query. This may not be the safest as it can also replace keys etc so may not
|
||||||
|
// actually work with every datasource query right now.
|
||||||
|
stringifiedQuery = replaceVariables(stringifiedQuery, scopedVars);
|
||||||
|
|
||||||
|
let replacedQuery = {} as T;
|
||||||
|
try {
|
||||||
|
replacedQuery = JSON.parse(stringifiedQuery);
|
||||||
|
} catch (err) {
|
||||||
|
// again should not happen and not much to do about this, probably some issue with how we replaced the variables.
|
||||||
|
console.error(stringifiedQuery, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacedQuery;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
|
* @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ExploreUrlState } from '../types/explore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type to represent the value of a single query variable.
|
* Type to represent the value of a single query variable.
|
||||||
*
|
*
|
||||||
@@ -129,3 +131,24 @@ export const urlUtil = {
|
|||||||
appendQueryToUrl,
|
appendQueryToUrl,
|
||||||
getUrlSearchParams,
|
getUrlSearchParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
|
if (compact) {
|
||||||
|
return JSON.stringify([
|
||||||
|
urlState.range.from,
|
||||||
|
urlState.range.to,
|
||||||
|
urlState.datasource,
|
||||||
|
...urlState.queries,
|
||||||
|
{ mode: urlState.mode },
|
||||||
|
{
|
||||||
|
ui: [
|
||||||
|
!!urlState.ui.showingGraph,
|
||||||
|
!!urlState.ui.showingLogs,
|
||||||
|
!!urlState.ui.showingTable,
|
||||||
|
urlState.ui.dedupStrategy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return JSON.stringify(urlState);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { ChangeEvent, useContext } from 'react';
|
import React, { ChangeEvent, useContext } from 'react';
|
||||||
import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data';
|
import { VariableSuggestion, GrafanaTheme, DataLink } from '@grafana/data';
|
||||||
import { Switch } from '../Switch/Switch';
|
import { Switch } from '../Switch/Switch';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { ThemeContext, stylesFactory } from '../../themes/index';
|
import { ThemeContext, stylesFactory } from '../../themes/index';
|
||||||
|
|||||||
@@ -20,7 +20,23 @@ export const DefaultCell: FC<TableCellProps> = props => {
|
|||||||
<div className={tableStyles.tableCell}>
|
<div className={tableStyles.tableCell}>
|
||||||
{link ? (
|
{link ? (
|
||||||
<Tooltip content={link.title}>
|
<Tooltip content={link.title}>
|
||||||
<a href={link.href} target={link.target} title={link.title} className={tableStyles.tableCellLink}>
|
<a
|
||||||
|
href={link.href}
|
||||||
|
onClick={
|
||||||
|
link.onClick
|
||||||
|
? event => {
|
||||||
|
// Allow opening in new tab
|
||||||
|
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link!.onClick) {
|
||||||
|
event.preventDefault();
|
||||||
|
link!.onClick(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
target={link.target}
|
||||||
|
title={link.title}
|
||||||
|
className={tableStyles.tableCellLink}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ function buildData(theme: GrafanaTheme, config: Record<string, FieldConfig>): Da
|
|||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
replaceVariables: (value: string) => value,
|
replaceVariables: (value: string) => value,
|
||||||
|
getDataSourceSettingsByUid: (value: string) => ({} as any),
|
||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import {
|
|||||||
hasNonEmptyQuery,
|
hasNonEmptyQuery,
|
||||||
parseUrlState,
|
parseUrlState,
|
||||||
refreshIntervalToSortOrder,
|
refreshIntervalToSortOrder,
|
||||||
serializeStateToUrlParam,
|
|
||||||
sortLogsResult,
|
sortLogsResult,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
updateHistory,
|
updateHistory,
|
||||||
} from './explore';
|
} from './explore';
|
||||||
import { ExploreUrlState } from 'app/types/explore';
|
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import {
|
import {
|
||||||
DataQueryError,
|
DataQueryError,
|
||||||
@@ -24,8 +22,10 @@ import {
|
|||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
LogsModel,
|
LogsModel,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
|
ExploreUrlState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
|
|
||||||
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||||
datasource: '',
|
datasource: '',
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import _ from 'lodash';
|
|||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
|
||||||
CoreApp,
|
CoreApp,
|
||||||
|
DataQuery,
|
||||||
DataQueryError,
|
DataQueryError,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
dateMath,
|
dateMath,
|
||||||
|
DefaultTimeZone,
|
||||||
|
ExploreMode,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
IntervalValues,
|
IntervalValues,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
@@ -19,16 +21,15 @@ import {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
toUtc,
|
toUtc,
|
||||||
ExploreMode,
|
|
||||||
urlUtil,
|
urlUtil,
|
||||||
DefaultTimeZone,
|
ExploreUrlState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import { getNextRefIdChar } from './query';
|
import { getNextRefIdChar } from './query';
|
||||||
// Types
|
// Types
|
||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
import { ExploreUrlState, QueryOptions, QueryTransaction } from 'app/types/explore';
|
import { QueryOptions, QueryTransaction } from 'app/types/explore';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { DataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv } from '@grafana/runtime';
|
||||||
@@ -260,27 +261,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
return { datasource, queries, range, ui, mode, originPanelId };
|
return { datasource, queries, range, ui, mode, originPanelId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
|
||||||
if (compact) {
|
|
||||||
return JSON.stringify([
|
|
||||||
urlState.range.from,
|
|
||||||
urlState.range.to,
|
|
||||||
urlState.datasource,
|
|
||||||
...urlState.queries,
|
|
||||||
{ mode: urlState.mode },
|
|
||||||
{
|
|
||||||
ui: [
|
|
||||||
!!urlState.ui.showingGraph,
|
|
||||||
!!urlState.ui.showingLogs,
|
|
||||||
!!urlState.ui.showingTable,
|
|
||||||
urlState.ui.dedupStrategy,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return JSON.stringify(urlState);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateKey(index = 0): string {
|
export function generateKey(index = 0): string {
|
||||||
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,23 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { DataQuery, DataSourceApi, ExploreMode, dateTimeFormat, AppEvents, urlUtil } from '@grafana/data';
|
import {
|
||||||
|
DataQuery,
|
||||||
|
DataSourceApi,
|
||||||
|
ExploreMode,
|
||||||
|
dateTimeFormat,
|
||||||
|
AppEvents,
|
||||||
|
urlUtil,
|
||||||
|
ExploreUrlState,
|
||||||
|
} from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { serializeStateToUrlParam, SortOrder } from './explore';
|
import { SortOrder } from './explore';
|
||||||
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
|
|
||||||
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
|
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { GetDataOptions } from '../../state/PanelQueryRunner';
|
|||||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { DetailText } from './DetailText';
|
import { DetailText } from './DetailText';
|
||||||
|
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -139,6 +140,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
|||||||
replaceVariables: (value: string) => {
|
replaceVariables: (value: string) => {
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn';
|
|||||||
// Types
|
// Types
|
||||||
import { PanelModel } from './PanelModel';
|
import { PanelModel } from './PanelModel';
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
import { DataLink, DataLinkBuiltInVars, urlUtil } from '@grafana/data';
|
import { DataLinkBuiltInVars, DataLink, urlUtil } from '@grafana/data';
|
||||||
// Constants
|
// Constants
|
||||||
import {
|
import {
|
||||||
DEFAULT_PANEL_SPAN,
|
DEFAULT_PANEL_SPAN,
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
standardEditorsRegistry,
|
standardEditorsRegistry,
|
||||||
standardFieldConfigEditorRegistry,
|
standardFieldConfigEditorRegistry,
|
||||||
PanelData,
|
PanelData,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { ComponentClass } from 'react';
|
import { ComponentClass } from 'react';
|
||||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
class TablePanelCtrl {}
|
class TablePanelCtrl {}
|
||||||
|
|
||||||
@@ -149,6 +151,11 @@ describe('PanelModel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should apply field config defaults', () => {
|
it('should apply field config defaults', () => {
|
||||||
|
setDataSourceSrv({
|
||||||
|
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
// default unit is overriden by model
|
// default unit is overriden by model
|
||||||
expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg');
|
expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg');
|
||||||
// default decimals are aplied
|
// default decimals are aplied
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { EDIT_PANEL_ID } from 'app/core/constants';
|
import { EDIT_PANEL_ID } from 'app/core/constants';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||||
|
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||||
|
|
||||||
export const panelAdded = eventFactory<PanelModel | undefined>('panel-added');
|
export const panelAdded = eventFactory<PanelModel | undefined>('panel-added');
|
||||||
export const panelRemoved = eventFactory<PanelModel | undefined>('panel-removed');
|
export const panelRemoved = eventFactory<PanelModel | undefined>('panel-removed');
|
||||||
@@ -439,6 +440,7 @@ export class PanelModel implements DataConfigSource {
|
|||||||
return {
|
return {
|
||||||
fieldConfig: this.fieldConfig,
|
fieldConfig: this.fieldConfig,
|
||||||
replaceVariables: this.replaceVariables,
|
replaceVariables: this.replaceVariables,
|
||||||
|
getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid.bind(getDatasourceSrv()),
|
||||||
fieldConfigRegistry: this.plugin.fieldConfigRegistry,
|
fieldConfigRegistry: this.plugin.fieldConfigRegistry,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ScopedVars,
|
ScopedVars,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { DashboardModel } from './index';
|
import { DashboardModel } from './index';
|
||||||
import { setEchoSrv } from '@grafana/runtime';
|
import { setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
|
||||||
import { Echo } from '../../../core/services/echo/Echo';
|
import { Echo } from '../../../core/services/echo/Echo';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
@@ -80,6 +80,11 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
setDataSourceSrv({
|
||||||
|
getDataSourceSettingsByUid() {
|
||||||
|
return {} as any;
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
setEchoSrv(new Echo());
|
setEchoSrv(new Echo());
|
||||||
@@ -226,6 +231,7 @@ describe('PanelQueryRunner', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
replaceVariables: v => v,
|
replaceVariables: v => v,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
}),
|
}),
|
||||||
getTransformations: () => undefined,
|
getTransformations: () => undefined,
|
||||||
@@ -292,6 +298,7 @@ describe('PanelQueryRunner', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
replaceVariables: v => v,
|
replaceVariables: v => v,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
}),
|
}),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -336,6 +343,7 @@ describe('PanelQueryRunner', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
replaceVariables: v => v,
|
replaceVariables: v => v,
|
||||||
|
getDataSourceSettingsByUid: undefined as any,
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
}),
|
}),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export class PanelQueryRunner {
|
|||||||
timeZone: this.timeZone,
|
timeZone: this.timeZone,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
data: processedData.series,
|
data: processedData.series,
|
||||||
|
getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid.bind(getDatasourceSrv()),
|
||||||
...fieldConfig,
|
...fieldConfig,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export function callQueryMethod(datasource: DataSourceApi, request: DataQueryReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All panels will be passed tables that have our best guess at colum type set
|
* All panels will be passed tables that have our best guess at column type set
|
||||||
*
|
*
|
||||||
* This is also used by PanelChrome for snapshot support
|
* This is also used by PanelChrome for snapshot support
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
|
ExploreUIState,
|
||||||
|
ExploreUrlState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
@@ -38,7 +40,7 @@ import {
|
|||||||
updateTimeRange,
|
updateTimeRange,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState, ExploreUIState, ExploreUpdateState, ExploreUrlState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import {
|
import {
|
||||||
DEFAULT_RANGE,
|
DEFAULT_RANGE,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ describe('TableContainer', () => {
|
|||||||
showingTable: true,
|
showingTable: true,
|
||||||
tableResult: {} as DataFrame,
|
tableResult: {} as DataFrame,
|
||||||
toggleTable: {} as typeof toggleTable,
|
toggleTable: {} as typeof toggleTable,
|
||||||
|
splitOpen: (() => {}) as any,
|
||||||
|
range: {} as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<TableContainer {...props} />);
|
const wrapper = shallow(<TableContainer {...props} />);
|
||||||
@@ -34,6 +36,8 @@ describe('TableContainer', () => {
|
|||||||
length: 0,
|
length: 0,
|
||||||
} as DataFrame,
|
} as DataFrame,
|
||||||
toggleTable: {} as typeof toggleTable,
|
toggleTable: {} as typeof toggleTable,
|
||||||
|
splitOpen: (() => {}) as any,
|
||||||
|
range: {} as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = render(<TableContainer {...props} />);
|
const wrapper = render(<TableContainer {...props} />);
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame, TimeRange, ValueLinkConfig } from '@grafana/data';
|
||||||
import { Collapse, Table } from '@grafana/ui';
|
import { Collapse, Table } from '@grafana/ui';
|
||||||
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 { toggleTable } from './state/actions';
|
import { splitOpen, toggleTable } from './state/actions';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
import { MetaInfoText } from './MetaInfoText';
|
import { MetaInfoText } from './MetaInfoText';
|
||||||
import { FilterItem } from '@grafana/ui/src/components/Table/types';
|
import { FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||||
|
import { getFieldLinksForExplore } from './utils/links';
|
||||||
|
|
||||||
interface TableContainerProps {
|
interface TableContainerProps {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
@@ -19,6 +20,8 @@ interface TableContainerProps {
|
|||||||
showingTable: boolean;
|
showingTable: boolean;
|
||||||
tableResult?: DataFrame;
|
tableResult?: DataFrame;
|
||||||
toggleTable: typeof toggleTable;
|
toggleTable: typeof toggleTable;
|
||||||
|
splitOpen: typeof splitOpen;
|
||||||
|
range: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TableContainer extends PureComponent<TableContainerProps> {
|
export class TableContainer extends PureComponent<TableContainerProps> {
|
||||||
@@ -38,12 +41,23 @@ export class TableContainer extends PureComponent<TableContainerProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, onCellFilterAdded, showingTable, tableResult, width } = this.props;
|
const { loading, onCellFilterAdded, showingTable, tableResult, width, splitOpen, range } = this.props;
|
||||||
|
|
||||||
const height = this.getTableHeight();
|
const height = this.getTableHeight();
|
||||||
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
||||||
const hasTableResult = tableResult?.length;
|
const hasTableResult = tableResult?.length;
|
||||||
|
|
||||||
|
if (hasTableResult) {
|
||||||
|
// Bit of code smell here. We need to add links here to the frame modifying the frame on every render.
|
||||||
|
// Should work fine in essence but still not the ideal way to pass props. In logs container we do this
|
||||||
|
// differently and sidestep this getLinks API on a dataframe
|
||||||
|
for (const field of tableResult.fields) {
|
||||||
|
field.getLinks = (config: ValueLinkConfig) => {
|
||||||
|
return getFieldLinksForExplore(field, config.valueRowIndex, splitOpen, range);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
|
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
|
||||||
{hasTableResult ? (
|
{hasTableResult ? (
|
||||||
@@ -60,13 +74,14 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
|||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const item: ExploreItemState = explore[exploreId];
|
const item: ExploreItemState = explore[exploreId];
|
||||||
const { loading: loadingInState, showingTable, tableResult } = item;
|
const { loading: loadingInState, showingTable, tableResult, range } = item;
|
||||||
const loading = tableResult && tableResult.length > 0 ? false : loadingInState;
|
const loading = tableResult && tableResult.length > 0 ? false : loadingInState;
|
||||||
return { loading, showingTable, tableResult };
|
return { loading, showingTable, tableResult, range };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
toggleTable,
|
toggleTable,
|
||||||
|
splitOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import {
|
|||||||
QueryFixAction,
|
QueryFixAction,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
ExploreMode,
|
ExploreMode,
|
||||||
|
ExploreUIState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { ExploreId, ExploreItemState, ExploreUIState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
|
|
||||||
export interface AddQueryRowPayload {
|
export interface AddQueryRowPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PayloadAction } from '@reduxjs/toolkit';
|
import { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, toUtc } from '@grafana/data';
|
import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, toUtc, ExploreUrlState } from '@grafana/data';
|
||||||
|
|
||||||
import * as Actions from './actions';
|
import * as Actions from './actions';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
navigateToExplore,
|
navigateToExplore,
|
||||||
refreshExplore,
|
refreshExplore,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { ExploreId, ExploreUpdateState, ExploreUrlState } from 'app/types';
|
import { ExploreId, ExploreUpdateState } from 'app/types';
|
||||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
import {
|
import {
|
||||||
cancelQueriesAction,
|
cancelQueriesAction,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
ExploreMode,
|
ExploreMode,
|
||||||
|
ExploreUrlState,
|
||||||
|
ExploreUIState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
@@ -34,7 +36,6 @@ import {
|
|||||||
hasNonEmptyQuery,
|
hasNonEmptyQuery,
|
||||||
lastUsedDatasourceKeyForOrgId,
|
lastUsedDatasourceKeyForOrgId,
|
||||||
parseUrlState,
|
parseUrlState,
|
||||||
serializeStateToUrlParam,
|
|
||||||
stopQueryState,
|
stopQueryState,
|
||||||
updateHistory,
|
updateHistory,
|
||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
@@ -47,9 +48,9 @@ import {
|
|||||||
getRichHistory,
|
getRichHistory,
|
||||||
} from 'app/core/utils/richHistory';
|
} from 'app/core/utils/richHistory';
|
||||||
// Types
|
// Types
|
||||||
import { ExploreItemState, ExploreUrlState, ThunkResult } from 'app/types';
|
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||||
|
|
||||||
import { ExploreId, ExploreUIState, QueryOptions } from 'app/types/explore';
|
import { ExploreId, QueryOptions } from 'app/types/explore';
|
||||||
import {
|
import {
|
||||||
addQueryRowAction,
|
addQueryRowAction,
|
||||||
changeModeAction,
|
changeModeAction,
|
||||||
@@ -94,6 +95,7 @@ import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
|||||||
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
|
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { getExploreDatasources } from './selectors';
|
import { getExploreDatasources } from './selectors';
|
||||||
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates UI state and save it to the URL
|
* Updates UI state and save it to the URL
|
||||||
@@ -696,7 +698,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<T extends DataQuery = any>(options?: { datasourceUid: string; query: T }): 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];
|
||||||
@@ -706,17 +708,20 @@ export function splitOpen(options?: { datasourceUid: string; query: string }): T
|
|||||||
const queryState = getState().location.query[ExploreId.left] as string;
|
const queryState = getState().location.query[ExploreId.left] as string;
|
||||||
const urlState = parseUrlState(queryState);
|
const urlState = parseUrlState(queryState);
|
||||||
|
|
||||||
// TODO: Instead of splitting and then setting query/datasource we may probably do it in one action call
|
|
||||||
rightState.queries = leftState.queries.slice();
|
|
||||||
rightState.urlState = urlState;
|
|
||||||
dispatch(splitOpenAction({ itemState: rightState }));
|
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
// TODO: This is hardcoded for Jaeger right now. Need to be changed so that target datasource can define the
|
rightState.queries = [];
|
||||||
// query shape.
|
rightState.graphResult = undefined;
|
||||||
|
rightState.logsResult = undefined;
|
||||||
|
rightState.tableResult = undefined;
|
||||||
|
rightState.queryKeys = [];
|
||||||
|
urlState.queries = [];
|
||||||
|
rightState.urlState = urlState;
|
||||||
|
|
||||||
|
dispatch(splitOpenAction({ itemState: rightState }));
|
||||||
|
|
||||||
const queries = [
|
const queries = [
|
||||||
{
|
{
|
||||||
query: options.query,
|
...options.query,
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
} as DataQuery,
|
} as DataQuery,
|
||||||
];
|
];
|
||||||
@@ -724,6 +729,10 @@ export function splitOpen(options?: { datasourceUid: string; query: string }): T
|
|||||||
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 }));
|
||||||
|
} else {
|
||||||
|
rightState.queries = leftState.queries.slice();
|
||||||
|
rightState.urlState = urlState;
|
||||||
|
dispatch(splitOpenAction({ itemState: rightState }));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(stateSave());
|
dispatch(stateSave());
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
toDataFrame,
|
toDataFrame,
|
||||||
UrlQueryMap,
|
UrlQueryMap,
|
||||||
|
ExploreUrlState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
makeExploreItemState,
|
makeExploreItemState,
|
||||||
makeInitialUpdateState,
|
makeInitialUpdateState,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { ExploreId, ExploreItemState, ExploreState, ExploreUrlState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
|
||||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
import {
|
import {
|
||||||
changeModeAction,
|
changeModeAction,
|
||||||
@@ -34,8 +35,8 @@ import {
|
|||||||
addQueryRowAction,
|
addQueryRowAction,
|
||||||
removeQueryRowAction,
|
removeQueryRowAction,
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
|
||||||
import { updateLocation } from '../../../core/actions';
|
import { updateLocation } from '../../../core/actions';
|
||||||
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
|
|
||||||
const QUERY_KEY_REGEX = /Q-([0-9]+)-([0-9.]+)-([0-9]+)/;
|
const QUERY_KEY_REGEX = /Q-([0-9]+)-([0-9.]+)-([0-9]+)/;
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
it('returns correct link model for internal link', () => {
|
it('returns correct link model for internal link', () => {
|
||||||
const { field, range } = setup({
|
const { field, range } = setup({
|
||||||
title: '',
|
title: '',
|
||||||
url: 'query_1',
|
url: '',
|
||||||
meta: {
|
internal: {
|
||||||
|
query: { query: 'query_1' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -56,7 +57,7 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
links[0].onClick({});
|
links[0].onClick({});
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(splitfn).toBeCalledWith({ datasourceUid: 'uid_1', query: 'query_1' });
|
expect(splitfn).toBeCalledWith({ datasourceUid: 'uid_1', query: { query: 'query_1' } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,10 +71,10 @@ function setup(link: DataLink) {
|
|||||||
origin: origin,
|
origin: origin,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAnchorInfo(link: DataLink) {
|
getAnchorInfo(link: any) {
|
||||||
return { ...link };
|
return { ...link };
|
||||||
},
|
},
|
||||||
getLinkUrl(link: DataLink) {
|
getLinkUrl(link: any) {
|
||||||
return link.url;
|
return link.url;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { splitOpen } from '../state/actions';
|
import { splitOpen } from '../state/actions';
|
||||||
import { ExploreMode, Field, LinkModel, locationUtil, TimeRange } from '@grafana/data';
|
import { Field, LinkModel, TimeRange } from '@grafana/data';
|
||||||
import { getLinksFromLogsField } from '../../panel/panellinks/linkSuppliers';
|
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||||
import { serializeStateToUrlParam } from '../../../core/utils/explore';
|
import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get links from the field of a dataframe and in addition check if there is associated
|
* Get links from the field of a dataframe and in addition check if there is associated
|
||||||
@@ -11,81 +11,52 @@ import { getDataSourceSrv } from '@grafana/runtime';
|
|||||||
* appropriately. This is for example used for transition from log with traceId to trace datasource to show that
|
* appropriately. This is for example used for transition from log with traceId to trace datasource to show that
|
||||||
* trace.
|
* trace.
|
||||||
*/
|
*/
|
||||||
export function getFieldLinksForExplore(
|
export const getFieldLinksForExplore = (
|
||||||
field: Field,
|
field: Field,
|
||||||
rowIndex: number,
|
rowIndex: number,
|
||||||
splitOpenFn: typeof splitOpen,
|
splitOpenFn: typeof splitOpen,
|
||||||
range: TimeRange
|
range: TimeRange
|
||||||
): Array<LinkModel<Field>> {
|
): Array<LinkModel<Field>> => {
|
||||||
const data = getLinksFromLogsField(field, rowIndex);
|
const scopedVars: any = {};
|
||||||
return data.map(d => {
|
scopedVars['__value'] = {
|
||||||
if (d.link.meta?.datasourceUid) {
|
value: {
|
||||||
return {
|
raw: field.values.get(rowIndex),
|
||||||
...d.linkModel,
|
},
|
||||||
title:
|
text: 'Raw value',
|
||||||
d.linkModel.title ||
|
};
|
||||||
getDataSourceSrv().getDataSourceSettingsByUid(d.link.meta.datasourceUid)?.name ||
|
|
||||||
'Unknown datasource',
|
return field.config.links
|
||||||
onClick: () => {
|
? field.config.links.map(link => {
|
||||||
splitOpenFn({
|
if (!link.internal) {
|
||||||
datasourceUid: d.link.meta.datasourceUid,
|
const linkModel = getLinkSrv().getDataLinkUIModel(link, scopedVars, field);
|
||||||
// TODO: fix the ambiguity here
|
if (!linkModel.title) {
|
||||||
// This looks weird but in case meta.datasourceUid is set we save the query in url which will get
|
linkModel.title = getTitleFromHref(linkModel.href);
|
||||||
// interpolated into href
|
}
|
||||||
query: d.linkModel.href,
|
return linkModel;
|
||||||
|
} else {
|
||||||
|
return mapInternalLinkToExplore(link, scopedVars, range, field, {
|
||||||
|
onClickFn: splitOpenFn,
|
||||||
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||||
|
getDataSourceSettingsByUid: getDataSourceSrv().getDataSourceSettingsByUid.bind(getDataSourceSrv()),
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
// 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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!d.linkModel.title) {
|
function getTitleFromHref(href: string): string {
|
||||||
let href = d.linkModel.href;
|
// The URL constructor needs the url to have protocol
|
||||||
// The URL constructor needs the url to have protocol
|
if (href.indexOf('://') < 0) {
|
||||||
if (href.indexOf('://') < 0) {
|
// Doesn't really matter what protocol we use.
|
||||||
// Doesn't really matter what protocol we use.
|
href = `http://${href}`;
|
||||||
href = `http://${href}`;
|
}
|
||||||
}
|
let title;
|
||||||
let title;
|
try {
|
||||||
try {
|
const parsedUrl = new URL(href);
|
||||||
const parsedUrl = new URL(href);
|
title = parsedUrl.hostname;
|
||||||
title = parsedUrl.hostname;
|
} catch (_e) {
|
||||||
} catch (_e) {
|
// Should be good enough fallback, user probably did not input valid url.
|
||||||
// Should be good enough fallback, user probably did not input valid url.
|
title = href;
|
||||||
title = href;
|
}
|
||||||
}
|
return title;
|
||||||
|
|
||||||
return {
|
|
||||||
...d.linkModel,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
import { getFieldLinksSupplier, getLinksFromLogsField } from './linkSuppliers';
|
import { getFieldLinksSupplier } from './linkSuppliers';
|
||||||
import {
|
import { applyFieldOverrides, DataFrameView, dateTime, FieldDisplay, GrafanaTheme, toDataFrame } from '@grafana/data';
|
||||||
applyFieldOverrides,
|
|
||||||
ArrayVector,
|
|
||||||
DataFrameView,
|
|
||||||
dateTime,
|
|
||||||
Field,
|
|
||||||
FieldDisplay,
|
|
||||||
FieldType,
|
|
||||||
GrafanaTheme,
|
|
||||||
toDataFrame,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv';
|
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv';
|
||||||
import { TemplateSrv } from '../../templating/template_srv';
|
import { TemplateSrv } from '../../templating/template_srv';
|
||||||
import { TimeSrv } from '../../dashboard/services/TimeSrv';
|
import { TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||||
|
|
||||||
describe('getLinksFromLogsField', () => {
|
describe('getFieldLinksSupplier', () => {
|
||||||
let originalLinkSrv: LinkService;
|
let originalLinkSrv: LinkService;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// We do not need more here and TimeSrv is hard to setup fully.
|
// We do not need more here and TimeSrv is hard to setup fully.
|
||||||
@@ -34,41 +24,6 @@ describe('getLinksFromLogsField', () => {
|
|||||||
setLinkSrv(originalLinkSrv);
|
setLinkSrv(originalLinkSrv);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('interpolates link from field', () => {
|
|
||||||
const field: Field = {
|
|
||||||
name: 'test field',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
title: 'title1',
|
|
||||||
url: 'http://domain.com/${__value.raw}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'title2',
|
|
||||||
url: 'http://anotherdomain.sk/${__value.raw}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
values: new ArrayVector([1, 2, 3]),
|
|
||||||
};
|
|
||||||
const links = getLinksFromLogsField(field, 2);
|
|
||||||
expect(links.length).toBe(2);
|
|
||||||
expect(links[0].linkModel.href).toBe('http://domain.com/3');
|
|
||||||
expect(links[1].linkModel.href).toBe('http://anotherdomain.sk/3');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles zero links', () => {
|
|
||||||
const field: Field = {
|
|
||||||
name: 'test field',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {},
|
|
||||||
values: new ArrayVector([1, 2, 3]),
|
|
||||||
};
|
|
||||||
const links = getLinksFromLogsField(field, 2);
|
|
||||||
expect(links.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('links to items on the row', () => {
|
it('links to items on the row', () => {
|
||||||
const data = applyFieldOverrides({
|
const data = applyFieldOverrides({
|
||||||
data: [
|
data: [
|
||||||
@@ -134,6 +89,7 @@ describe('getLinksFromLogsField', () => {
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
},
|
},
|
||||||
replaceVariables: (val: string) => val,
|
replaceVariables: (val: string) => val,
|
||||||
|
getDataSourceSettingsByUid: (val: string) => ({} as any),
|
||||||
timeZone: 'utc',
|
timeZone: 'utc',
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
|||||||
import {
|
import {
|
||||||
DataLink,
|
DataLink,
|
||||||
DisplayValue,
|
DisplayValue,
|
||||||
Field,
|
|
||||||
FieldDisplay,
|
FieldDisplay,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getFieldDisplayValuesProxy,
|
getFieldDisplayValuesProxy,
|
||||||
getTimeField,
|
getTimeField,
|
||||||
Labels,
|
Labels,
|
||||||
LinkModel,
|
|
||||||
LinkModelSupplier,
|
LinkModelSupplier,
|
||||||
ScopedVar,
|
ScopedVar,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
@@ -50,7 +48,6 @@ interface DataLinkScopedVars extends ScopedVars {
|
|||||||
/**
|
/**
|
||||||
* Link suppliers creates link models based on a link origin
|
* Link suppliers creates link models based on a link origin
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<FieldDisplay> | undefined => {
|
export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<FieldDisplay> | undefined => {
|
||||||
const links = value.field.links;
|
const links = value.field.links;
|
||||||
if (!links || links.length === 0) {
|
if (!links || links.length === 0) {
|
||||||
@@ -124,7 +121,7 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
|
|||||||
console.log('VALUE', value);
|
console.log('VALUE', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return links.map(link => {
|
return links.map((link: DataLink) => {
|
||||||
return getLinkSrv().getDataLinkUIModel(link, scopedVars, value);
|
return getLinkSrv().getDataLinkUIModel(link, scopedVars, value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -146,25 +143,3 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<Pane
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLinksFromLogsField = (
|
|
||||||
field: Field,
|
|
||||||
rowIndex: number
|
|
||||||
): Array<{ linkModel: LinkModel<Field>; link: DataLink }> => {
|
|
||||||
const scopedVars: any = {};
|
|
||||||
scopedVars['__value'] = {
|
|
||||||
value: {
|
|
||||||
raw: field.values.get(rowIndex),
|
|
||||||
},
|
|
||||||
text: 'Raw value',
|
|
||||||
};
|
|
||||||
|
|
||||||
return field.config.links
|
|
||||||
? field.config.links.map(link => {
|
|
||||||
return {
|
|
||||||
link,
|
|
||||||
linkModel: getLinkSrv().getDataLinkUIModel(link, scopedVars, field),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import coreModule from 'app/core/core_module';
|
|||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataLink,
|
|
||||||
DataLinkBuiltInVars,
|
DataLinkBuiltInVars,
|
||||||
deprecationWarning,
|
deprecationWarning,
|
||||||
Field,
|
Field,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
VariableSuggestionsScope,
|
VariableSuggestionsScope,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
textUtil,
|
textUtil,
|
||||||
|
DataLink,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
const timeRangeVars = [
|
const timeRangeVars = [
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import cx from 'classnames';
|
|||||||
import { LegacyForms } from '@grafana/ui';
|
import { LegacyForms } from '@grafana/ui';
|
||||||
const { FormField } = LegacyForms;
|
const { FormField } = LegacyForms;
|
||||||
import { DerivedFieldConfig } from '../types';
|
import { DerivedFieldConfig } from '../types';
|
||||||
import { getLinksFromLogsField } from '../../../../features/panel/panellinks/linkSuppliers';
|
|
||||||
import { ArrayVector, Field, FieldType, LinkModel } from '@grafana/data';
|
import { ArrayVector, Field, FieldType, LinkModel } from '@grafana/data';
|
||||||
|
import { getFieldLinksForExplore } from '../../../../features/explore/utils/links';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
derivedFields: DerivedFieldConfig[];
|
derivedFields: DerivedFieldConfig[];
|
||||||
@@ -94,7 +94,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
|||||||
let link: LinkModel<Field>;
|
let link: LinkModel<Field>;
|
||||||
|
|
||||||
if (field.url && value) {
|
if (field.url && value) {
|
||||||
link = getLinksFromLogsField(
|
link = getFieldLinksForExplore(
|
||||||
{
|
{
|
||||||
name: '',
|
name: '',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
@@ -103,8 +103,10 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
|||||||
links: [{ title: '', url: field.url }],
|
links: [{ title: '', url: field.url }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
0
|
0,
|
||||||
)[0].linkModel;
|
(() => {}) as any,
|
||||||
|
{} as any
|
||||||
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,6 +115,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
|||||||
href: link && link.href,
|
href: link && link.href,
|
||||||
} as DebugField;
|
} as DebugField;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
return {
|
return {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -129,11 +129,13 @@ describe('enhanceDataFrame', () => {
|
|||||||
{
|
{
|
||||||
matcherRegex: 'trace2=(\\w+)',
|
matcherRegex: 'trace2=(\\w+)',
|
||||||
name: 'trace2',
|
name: 'trace2',
|
||||||
|
url: 'test',
|
||||||
datasourceUid: 'uid',
|
datasourceUid: 'uid',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matcherRegex: 'trace2=(\\w+)',
|
matcherRegex: 'trace2=(\\w+)',
|
||||||
name: 'trace2',
|
name: 'trace2',
|
||||||
|
url: 'test',
|
||||||
datasourceUid: 'uid2',
|
datasourceUid: 'uid2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -150,11 +152,13 @@ describe('enhanceDataFrame', () => {
|
|||||||
expect(fc.getFieldByName('trace2').config.links.length).toBe(2);
|
expect(fc.getFieldByName('trace2').config.links.length).toBe(2);
|
||||||
expect(fc.getFieldByName('trace2').config.links[0]).toEqual({
|
expect(fc.getFieldByName('trace2').config.links[0]).toEqual({
|
||||||
title: '',
|
title: '',
|
||||||
meta: { datasourceUid: 'uid' },
|
internal: { datasourceUid: 'uid', query: { query: 'test' } },
|
||||||
|
url: '',
|
||||||
});
|
});
|
||||||
expect(fc.getFieldByName('trace2').config.links[1]).toEqual({
|
expect(fc.getFieldByName('trace2').config.links[1]).toEqual({
|
||||||
title: '',
|
title: '',
|
||||||
meta: { datasourceUid: 'uid2' },
|
internal: { datasourceUid: 'uid2', query: { query: 'test' } },
|
||||||
|
url: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -357,17 +357,24 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul
|
|||||||
*/
|
*/
|
||||||
function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): Field<any, ArrayVector> {
|
function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): Field<any, ArrayVector> {
|
||||||
const dataLinks = derivedFieldConfigs.reduce((acc, derivedFieldConfig) => {
|
const dataLinks = derivedFieldConfigs.reduce((acc, derivedFieldConfig) => {
|
||||||
if (derivedFieldConfig.url || derivedFieldConfig.datasourceUid) {
|
// Having field.datasourceUid means it is an internal link.
|
||||||
|
if (derivedFieldConfig.datasourceUid) {
|
||||||
|
acc.push({
|
||||||
|
// Will be filled out later
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
// This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object
|
||||||
|
internal: {
|
||||||
|
query: { query: derivedFieldConfig.url },
|
||||||
|
datasourceUid: derivedFieldConfig.datasourceUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (derivedFieldConfig.url) {
|
||||||
acc.push({
|
acc.push({
|
||||||
// We do not know what title to give here so we count on presentation layer to create a title from metadata.
|
// We do not know what title to give here so we count on presentation layer to create a title from metadata.
|
||||||
title: '',
|
title: '',
|
||||||
|
// This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object
|
||||||
url: derivedFieldConfig.url,
|
url: derivedFieldConfig.url,
|
||||||
// Having field.datasourceUid means it is an internal link.
|
|
||||||
meta: derivedFieldConfig.datasourceUid
|
|
||||||
? {
|
|
||||||
datasourceUid: derivedFieldConfig.datasourceUid,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
GraphSeriesXY,
|
GraphSeriesXY,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
ExploreMode,
|
ExploreMode,
|
||||||
|
ExploreUrlState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { Emitter } from 'app/core/core';
|
import { Emitter } from 'app/core/core';
|
||||||
@@ -197,23 +198,6 @@ export interface ExploreUpdateState {
|
|||||||
ui: boolean;
|
ui: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUIState {
|
|
||||||
showingTable: boolean;
|
|
||||||
showingGraph: boolean;
|
|
||||||
showingLogs: boolean;
|
|
||||||
dedupStrategy?: LogsDedupStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExploreUrlState {
|
|
||||||
datasource: string;
|
|
||||||
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
|
||||||
mode: ExploreMode;
|
|
||||||
range: RawTimeRange;
|
|
||||||
ui: ExploreUIState;
|
|
||||||
originPanelId?: number;
|
|
||||||
context?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
minInterval: string;
|
minInterval: string;
|
||||||
maxDataPoints?: number;
|
maxDataPoints?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user