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:
co-authored by
Dominik Prokop
parent
463e8ffd92
commit
81d7cb1773
@@ -27,6 +27,7 @@ import { fieldIndexComparer } from '../field/fieldComparers';
|
||||
|
||||
function convertTableToDataFrame(table: TableData): DataFrame {
|
||||
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;
|
||||
return {
|
||||
name: text, // rename 'text' to the 'name' field
|
||||
|
||||
@@ -118,6 +118,7 @@ describe('applyFieldOverrides', () => {
|
||||
overrides: [],
|
||||
},
|
||||
replaceVariables: (value: any) => value,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
theme: {} as GrafanaTheme,
|
||||
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
|
||||
});
|
||||
@@ -187,6 +188,7 @@ describe('applyFieldOverrides', () => {
|
||||
overrides: [],
|
||||
},
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
replaceVariables: v => v,
|
||||
theme: {} as GrafanaTheme,
|
||||
})[0];
|
||||
@@ -204,6 +206,7 @@ describe('applyFieldOverrides', () => {
|
||||
data: [f0], // the frame
|
||||
fieldConfig: src as FieldConfigSource, // defaults + overrides
|
||||
replaceVariables: (undefined as any) as InterpolateFunction,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
theme: (undefined as any) as GrafanaTheme,
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
})[0];
|
||||
@@ -231,6 +234,7 @@ describe('applyFieldOverrides', () => {
|
||||
data: [f0], // the frame
|
||||
fieldConfig: src as FieldConfigSource, // defaults + overrides
|
||||
replaceVariables: (undefined as any) as InterpolateFunction,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
theme: (undefined as any) as GrafanaTheme,
|
||||
autoMinMax: true,
|
||||
})[0];
|
||||
@@ -478,11 +482,72 @@ describe('getLinksSupplier', () => {
|
||||
});
|
||||
|
||||
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({});
|
||||
|
||||
expect(replaceSpy).toBeCalledTimes(2);
|
||||
expect(replaceSpy.mock.calls[0][0]).toEqual('url 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,
|
||||
GrafanaTheme,
|
||||
TimeZone,
|
||||
DataLink,
|
||||
DataSourceInstanceSettings,
|
||||
} from '../types';
|
||||
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||
import { FieldMatcher } from '../types/transformations';
|
||||
@@ -33,6 +35,7 @@ import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
||||
import { formatLabels } from '../utils/labels';
|
||||
import { getFrameDisplayName, getFieldDisplayName } from './fieldState';
|
||||
import { getTimeField } from '../dataframe/processDataFrame';
|
||||
import { mapInternalLinkToExplore } from '../utils/dataLinks';
|
||||
|
||||
interface OverrideProps {
|
||||
match: FieldMatcher;
|
||||
@@ -129,6 +132,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
data: options.data!,
|
||||
dataFrameIndex: index,
|
||||
replaceVariables: options.replaceVariables,
|
||||
getDataSourceSettingsByUid: options.getDataSourceSettingsByUid,
|
||||
fieldConfigRegistry: fieldConfigRegistry,
|
||||
};
|
||||
|
||||
@@ -206,10 +210,17 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
});
|
||||
|
||||
// Attach data links supplier
|
||||
f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, {
|
||||
theme: options.theme,
|
||||
timeZone: options.timeZone,
|
||||
});
|
||||
f.getLinks = getLinksSupplier(
|
||||
frame,
|
||||
f,
|
||||
fieldScopedVars,
|
||||
context.replaceVariables,
|
||||
context.getDataSourceSettingsByUid,
|
||||
{
|
||||
theme: options.theme,
|
||||
timeZone: options.timeZone,
|
||||
}
|
||||
);
|
||||
|
||||
return f;
|
||||
});
|
||||
@@ -348,6 +359,7 @@ export const getLinksSupplier = (
|
||||
field: Field,
|
||||
fieldScopedVars: ScopedVars,
|
||||
replaceVariables: InterpolateFunction,
|
||||
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined,
|
||||
options: {
|
||||
theme: GrafanaTheme;
|
||||
timeZone?: TimeZone;
|
||||
@@ -359,20 +371,11 @@ export const getLinksSupplier = (
|
||||
const timeRangeUrl = locationUtil.getTimeRangeUrlParams();
|
||||
const { timeField } = getTimeField(frame);
|
||||
|
||||
return field.config.links.map(link => {
|
||||
let href = link.url;
|
||||
return field.config.links.map((link: DataLink) => {
|
||||
const variablesQuery = locationUtil.getVariablesUrlParams();
|
||||
let dataFrameVars = {};
|
||||
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
|
||||
if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) {
|
||||
const fieldsProxy = getFieldDisplayValuesProxy(frame, config.valueRowIndex, options);
|
||||
@@ -419,10 +422,25 @@ export const getLinksSupplier = (
|
||||
},
|
||||
};
|
||||
|
||||
info.href = replaceVariables(info.href, variables);
|
||||
info.title = replaceVariables(info.title, variables);
|
||||
info.href = locationUtil.processUrl(info.href);
|
||||
if (link.internal) {
|
||||
// For internal links at the moment only destination is Explore.
|
||||
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: [],
|
||||
},
|
||||
replaceVariables: (val: string) => val,
|
||||
getDataSourceSettingsByUid: (val: string) => ({} as any),
|
||||
timeZone: 'utc',
|
||||
theme: {} as GrafanaTheme,
|
||||
autoMinMax: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { DataQuery } from './datasource';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* processed before running
|
||||
* Link configuration. The values may contain variables that need to be
|
||||
* 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;
|
||||
targetBlank?: boolean;
|
||||
|
||||
@@ -28,16 +32,19 @@ export interface DataLink {
|
||||
// Not saved in JSON/DTO
|
||||
onClick?: (event: DataLinkClickEvent) => void;
|
||||
|
||||
// At the moment this is used for derived fields for metadata about internal linking.
|
||||
meta?: {
|
||||
datasourceUid?: string;
|
||||
// If dataLink represents internal link this has to be filled. Internal link is defined as a query in a particular
|
||||
// datas ource that we want to show to the user. Usually this results in a link to explore but can also lead to
|
||||
// more custom onClick behaviour if needed.
|
||||
internal?: {
|
||||
query: T;
|
||||
datasourceUid: string;
|
||||
};
|
||||
}
|
||||
|
||||
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> {
|
||||
href: string;
|
||||
|
||||
@@ -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 { MatcherConfig, FieldConfig, Field, DataFrame, GrafanaTheme, TimeZone } from '../types';
|
||||
import {
|
||||
MatcherConfig,
|
||||
FieldConfig,
|
||||
Field,
|
||||
DataFrame,
|
||||
GrafanaTheme,
|
||||
TimeZone,
|
||||
DataSourceInstanceSettings,
|
||||
} from '../types';
|
||||
import { InterpolateFunction } from './panel';
|
||||
import { StandardEditorProps, FieldConfigOptionsRegistry, StandardEditorContext } from '../field';
|
||||
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
|
||||
@@ -106,6 +114,7 @@ export interface ApplyFieldOverrideOptions {
|
||||
data?: DataFrame[];
|
||||
fieldConfig: FieldConfigSource;
|
||||
replaceVariables: InterpolateFunction;
|
||||
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
|
||||
theme: GrafanaTheme;
|
||||
timeZone?: TimeZone;
|
||||
autoMinMax?: boolean;
|
||||
|
||||
@@ -25,6 +25,7 @@ export * from './theme';
|
||||
export * from './orgs';
|
||||
export * from './flot';
|
||||
export * from './trace';
|
||||
export * from './explore';
|
||||
|
||||
import * as AppEvents from './appEvents';
|
||||
import { AppEvent } from './appEvents';
|
||||
|
||||
@@ -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 = {
|
||||
keepTime: '__url_time_range',
|
||||
timeRangeFrom: '__from',
|
||||
@@ -12,3 +26,96 @@ export const DataLinkBuiltInVars = {
|
||||
// name of the calculation represented by the value
|
||||
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
|
||||
*/
|
||||
|
||||
import { ExploreUrlState } from '../types/explore';
|
||||
|
||||
/**
|
||||
* Type to represent the value of a single query variable.
|
||||
*
|
||||
@@ -129,3 +131,24 @@ export const urlUtil = {
|
||||
appendQueryToUrl,
|
||||
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 { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data';
|
||||
import { VariableSuggestion, GrafanaTheme, DataLink } from '@grafana/data';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { css } from 'emotion';
|
||||
import { ThemeContext, stylesFactory } from '../../themes/index';
|
||||
|
||||
@@ -20,7 +20,23 @@ export const DefaultCell: FC<TableCellProps> = props => {
|
||||
<div className={tableStyles.tableCell}>
|
||||
{link ? (
|
||||
<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}
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
@@ -90,6 +90,7 @@ function buildData(theme: GrafanaTheme, config: Record<string, FieldConfig>): Da
|
||||
},
|
||||
theme,
|
||||
replaceVariables: (value: string) => value,
|
||||
getDataSourceSettingsByUid: (value: string) => ({} as any),
|
||||
})[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@ import {
|
||||
hasNonEmptyQuery,
|
||||
parseUrlState,
|
||||
refreshIntervalToSortOrder,
|
||||
serializeStateToUrlParam,
|
||||
sortLogsResult,
|
||||
SortOrder,
|
||||
updateHistory,
|
||||
} from './explore';
|
||||
import { ExploreUrlState } from 'app/types/explore';
|
||||
import store from 'app/core/store';
|
||||
import {
|
||||
DataQueryError,
|
||||
@@ -24,8 +22,10 @@ import {
|
||||
LogsDedupStrategy,
|
||||
LogsModel,
|
||||
MutableDataFrame,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||
|
||||
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||
datasource: '',
|
||||
|
||||
@@ -3,12 +3,14 @@ import _ from 'lodash';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
// Services & Utils
|
||||
import {
|
||||
DataQuery,
|
||||
CoreApp,
|
||||
DataQuery,
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
dateMath,
|
||||
DefaultTimeZone,
|
||||
ExploreMode,
|
||||
HistoryItem,
|
||||
IntervalValues,
|
||||
LogRowModel,
|
||||
@@ -19,16 +21,15 @@ import {
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
toUtc,
|
||||
ExploreMode,
|
||||
urlUtil,
|
||||
DefaultTimeZone,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
import store from 'app/core/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { getNextRefIdChar } from './query';
|
||||
// Types
|
||||
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 { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { DataSourceSrv } from '@grafana/runtime';
|
||||
@@ -260,27 +261,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
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 {
|
||||
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
// 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 store from 'app/core/store';
|
||||
import { serializeStateToUrlParam, SortOrder } from './explore';
|
||||
import { SortOrder } from './explore';
|
||||
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
||||
|
||||
// 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';
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GetDataOptions } from '../../state/PanelQueryRunner';
|
||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { DetailText } from './DetailText';
|
||||
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -139,6 +140,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
replaceVariables: (value: string) => {
|
||||
return value;
|
||||
},
|
||||
getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn';
|
||||
// Types
|
||||
import { PanelModel } from './PanelModel';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
import { DataLink, DataLinkBuiltInVars, urlUtil } from '@grafana/data';
|
||||
import { DataLinkBuiltInVars, DataLink, urlUtil } from '@grafana/data';
|
||||
// Constants
|
||||
import {
|
||||
DEFAULT_PANEL_SPAN,
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
standardEditorsRegistry,
|
||||
standardFieldConfigEditorRegistry,
|
||||
PanelData,
|
||||
DataSourceInstanceSettings,
|
||||
} from '@grafana/data';
|
||||
import { ComponentClass } from 'react';
|
||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
class TablePanelCtrl {}
|
||||
|
||||
@@ -149,6 +151,11 @@ describe('PanelModel', () => {
|
||||
});
|
||||
|
||||
it('should apply field config defaults', () => {
|
||||
setDataSourceSrv({
|
||||
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
|
||||
return undefined;
|
||||
},
|
||||
} as any);
|
||||
// default unit is overriden by model
|
||||
expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg');
|
||||
// default decimals are aplied
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { EDIT_PANEL_ID } from 'app/core/constants';
|
||||
import config from 'app/core/config';
|
||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
|
||||
export const panelAdded = eventFactory<PanelModel | undefined>('panel-added');
|
||||
export const panelRemoved = eventFactory<PanelModel | undefined>('panel-removed');
|
||||
@@ -439,6 +440,7 @@ export class PanelModel implements DataConfigSource {
|
||||
return {
|
||||
fieldConfig: this.fieldConfig,
|
||||
replaceVariables: this.replaceVariables,
|
||||
getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid.bind(getDatasourceSrv()),
|
||||
fieldConfigRegistry: this.plugin.fieldConfigRegistry,
|
||||
theme: config.theme,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { DashboardModel } from './index';
|
||||
import { setEchoSrv } from '@grafana/runtime';
|
||||
import { setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
|
||||
import { Echo } from '../../../core/services/echo/Echo';
|
||||
|
||||
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 () => {
|
||||
setEchoSrv(new Echo());
|
||||
@@ -226,6 +231,7 @@ describe('PanelQueryRunner', () => {
|
||||
overrides: [],
|
||||
},
|
||||
replaceVariables: v => v,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
theme: {} as GrafanaTheme,
|
||||
}),
|
||||
getTransformations: () => undefined,
|
||||
@@ -292,6 +298,7 @@ describe('PanelQueryRunner', () => {
|
||||
overrides: [],
|
||||
},
|
||||
replaceVariables: v => v,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
theme: {} as GrafanaTheme,
|
||||
}),
|
||||
// @ts-ignore
|
||||
@@ -336,6 +343,7 @@ describe('PanelQueryRunner', () => {
|
||||
overrides: [],
|
||||
},
|
||||
replaceVariables: v => v,
|
||||
getDataSourceSettingsByUid: undefined as any,
|
||||
theme: {} as GrafanaTheme,
|
||||
}),
|
||||
// @ts-ignore
|
||||
|
||||
@@ -100,6 +100,7 @@ export class PanelQueryRunner {
|
||||
timeZone: this.timeZone,
|
||||
autoMinMax: true,
|
||||
data: processedData.series,
|
||||
getDataSourceSettingsByUid: getDatasourceSrv().getDataSourceSettingsByUid.bind(getDatasourceSrv()),
|
||||
...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
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
ExploreUIState,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
|
||||
import store from 'app/core/store';
|
||||
@@ -38,7 +40,7 @@ import {
|
||||
updateTimeRange,
|
||||
} 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 {
|
||||
DEFAULT_RANGE,
|
||||
|
||||
@@ -15,6 +15,8 @@ describe('TableContainer', () => {
|
||||
showingTable: true,
|
||||
tableResult: {} as DataFrame,
|
||||
toggleTable: {} as typeof toggleTable,
|
||||
splitOpen: (() => {}) as any,
|
||||
range: {} as any,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<TableContainer {...props} />);
|
||||
@@ -34,6 +36,8 @@ describe('TableContainer', () => {
|
||||
length: 0,
|
||||
} as DataFrame,
|
||||
toggleTable: {} as typeof toggleTable,
|
||||
splitOpen: (() => {}) as any,
|
||||
range: {} as any,
|
||||
};
|
||||
|
||||
const wrapper = render(<TableContainer {...props} />);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { DataFrame, TimeRange, ValueLinkConfig } from '@grafana/data';
|
||||
import { Collapse, Table } from '@grafana/ui';
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
import { toggleTable } from './state/actions';
|
||||
import { splitOpen, toggleTable } from './state/actions';
|
||||
import { config } from 'app/core/config';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import { MetaInfoText } from './MetaInfoText';
|
||||
import { FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||
import { getFieldLinksForExplore } from './utils/links';
|
||||
|
||||
interface TableContainerProps {
|
||||
exploreId: ExploreId;
|
||||
@@ -19,6 +20,8 @@ interface TableContainerProps {
|
||||
showingTable: boolean;
|
||||
tableResult?: DataFrame;
|
||||
toggleTable: typeof toggleTable;
|
||||
splitOpen: typeof splitOpen;
|
||||
range: TimeRange;
|
||||
}
|
||||
|
||||
export class TableContainer extends PureComponent<TableContainerProps> {
|
||||
@@ -38,12 +41,23 @@ export class TableContainer extends PureComponent<TableContainerProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, onCellFilterAdded, showingTable, tableResult, width } = this.props;
|
||||
const { loading, onCellFilterAdded, showingTable, tableResult, width, splitOpen, range } = this.props;
|
||||
|
||||
const height = this.getTableHeight();
|
||||
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
||||
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 (
|
||||
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
|
||||
{hasTableResult ? (
|
||||
@@ -60,13 +74,14 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
||||
const explore = state.explore;
|
||||
// @ts-ignore
|
||||
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;
|
||||
return { loading, showingTable, tableResult };
|
||||
return { loading, showingTable, tableResult, range };
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleTable,
|
||||
splitOpen,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
QueryFixAction,
|
||||
TimeRange,
|
||||
ExploreMode,
|
||||
ExploreUIState,
|
||||
} from '@grafana/data';
|
||||
import { ExploreId, ExploreItemState, ExploreUIState } from 'app/types/explore';
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
|
||||
export interface AddQueryRowPayload {
|
||||
exploreId: ExploreId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
navigateToExplore,
|
||||
refreshExplore,
|
||||
} from './actions';
|
||||
import { ExploreId, ExploreUpdateState, ExploreUrlState } from 'app/types';
|
||||
import { ExploreId, ExploreUpdateState } from 'app/types';
|
||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||
import {
|
||||
cancelQueriesAction,
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
ExploreMode,
|
||||
ExploreUrlState,
|
||||
ExploreUIState,
|
||||
} from '@grafana/data';
|
||||
// Services & Utils
|
||||
import store from 'app/core/store';
|
||||
@@ -34,7 +36,6 @@ import {
|
||||
hasNonEmptyQuery,
|
||||
lastUsedDatasourceKeyForOrgId,
|
||||
parseUrlState,
|
||||
serializeStateToUrlParam,
|
||||
stopQueryState,
|
||||
updateHistory,
|
||||
} from 'app/core/utils/explore';
|
||||
@@ -47,9 +48,9 @@ import {
|
||||
getRichHistory,
|
||||
} from 'app/core/utils/richHistory';
|
||||
// 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 {
|
||||
addQueryRowAction,
|
||||
changeModeAction,
|
||||
@@ -94,6 +95,7 @@ import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { getExploreDatasources } from './selectors';
|
||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/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
|
||||
* 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) => {
|
||||
// Clone left state to become the right state
|
||||
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 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) {
|
||||
// TODO: This is hardcoded for Jaeger right now. Need to be changed so that target datasource can define the
|
||||
// query shape.
|
||||
rightState.queries = [];
|
||||
rightState.graphResult = undefined;
|
||||
rightState.logsResult = undefined;
|
||||
rightState.tableResult = undefined;
|
||||
rightState.queryKeys = [];
|
||||
urlState.queries = [];
|
||||
rightState.urlState = urlState;
|
||||
|
||||
dispatch(splitOpenAction({ itemState: rightState }));
|
||||
|
||||
const queries = [
|
||||
{
|
||||
query: options.query,
|
||||
...options.query,
|
||||
refId: 'A',
|
||||
} as DataQuery,
|
||||
];
|
||||
@@ -724,6 +729,10 @@ export function splitOpen(options?: { datasourceUid: string; query: string }): T
|
||||
const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.datasourceUid);
|
||||
await dispatch(changeDatasource(ExploreId.right, dataSourceSettings.name));
|
||||
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
|
||||
} else {
|
||||
rightState.queries = leftState.queries.slice();
|
||||
rightState.urlState = urlState;
|
||||
dispatch(splitOpenAction({ itemState: rightState }));
|
||||
}
|
||||
|
||||
dispatch(stateSave());
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RawTimeRange,
|
||||
toDataFrame,
|
||||
UrlQueryMap,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
|
||||
import {
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
makeExploreItemState,
|
||||
makeInitialUpdateState,
|
||||
} 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 {
|
||||
changeModeAction,
|
||||
@@ -34,8 +35,8 @@ import {
|
||||
addQueryRowAction,
|
||||
removeQueryRowAction,
|
||||
} from './actionTypes';
|
||||
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||
|
||||
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', () => {
|
||||
const { field, range } = setup({
|
||||
title: '',
|
||||
url: 'query_1',
|
||||
meta: {
|
||||
url: '',
|
||||
internal: {
|
||||
query: { query: 'query_1' },
|
||||
datasourceUid: 'uid_1',
|
||||
},
|
||||
});
|
||||
@@ -56,7 +57,7 @@ describe('getFieldLinksForExplore', () => {
|
||||
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,
|
||||
};
|
||||
},
|
||||
getAnchorInfo(link: DataLink) {
|
||||
getAnchorInfo(link: any) {
|
||||
return { ...link };
|
||||
},
|
||||
getLinkUrl(link: DataLink) {
|
||||
getLinkUrl(link: any) {
|
||||
return link.url;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { splitOpen } from '../state/actions';
|
||||
import { ExploreMode, Field, LinkModel, locationUtil, TimeRange } from '@grafana/data';
|
||||
import { getLinksFromLogsField } from '../../panel/panellinks/linkSuppliers';
|
||||
import { serializeStateToUrlParam } from '../../../core/utils/explore';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Field, LinkModel, TimeRange } from '@grafana/data';
|
||||
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks';
|
||||
import { getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* trace.
|
||||
*/
|
||||
export function getFieldLinksForExplore(
|
||||
export const getFieldLinksForExplore = (
|
||||
field: Field,
|
||||
rowIndex: number,
|
||||
splitOpenFn: typeof splitOpen,
|
||||
range: TimeRange
|
||||
): Array<LinkModel<Field>> {
|
||||
const data = getLinksFromLogsField(field, rowIndex);
|
||||
return data.map(d => {
|
||||
if (d.link.meta?.datasourceUid) {
|
||||
return {
|
||||
...d.linkModel,
|
||||
title:
|
||||
d.linkModel.title ||
|
||||
getDataSourceSrv().getDataSourceSettingsByUid(d.link.meta.datasourceUid)?.name ||
|
||||
'Unknown datasource',
|
||||
onClick: () => {
|
||||
splitOpenFn({
|
||||
datasourceUid: d.link.meta.datasourceUid,
|
||||
// TODO: fix the ambiguity here
|
||||
// This looks weird but in case meta.datasourceUid is set we save the query in url which will get
|
||||
// interpolated into href
|
||||
query: d.linkModel.href,
|
||||
): Array<LinkModel<Field>> => {
|
||||
const scopedVars: any = {};
|
||||
scopedVars['__value'] = {
|
||||
value: {
|
||||
raw: field.values.get(rowIndex),
|
||||
},
|
||||
text: 'Raw value',
|
||||
};
|
||||
|
||||
return field.config.links
|
||||
? field.config.links.map(link => {
|
||||
if (!link.internal) {
|
||||
const linkModel = getLinkSrv().getDataLinkUIModel(link, scopedVars, field);
|
||||
if (!linkModel.title) {
|
||||
linkModel.title = getTitleFromHref(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) {
|
||||
let href = d.linkModel.href;
|
||||
// The URL constructor needs the url to have protocol
|
||||
if (href.indexOf('://') < 0) {
|
||||
// Doesn't really matter what protocol we use.
|
||||
href = `http://${href}`;
|
||||
}
|
||||
let title;
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
title = parsedUrl.hostname;
|
||||
} catch (_e) {
|
||||
// Should be good enough fallback, user probably did not input valid url.
|
||||
title = href;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})}`
|
||||
);
|
||||
function getTitleFromHref(href: string): string {
|
||||
// The URL constructor needs the url to have protocol
|
||||
if (href.indexOf('://') < 0) {
|
||||
// Doesn't really matter what protocol we use.
|
||||
href = `http://${href}`;
|
||||
}
|
||||
let title;
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
title = parsedUrl.hostname;
|
||||
} catch (_e) {
|
||||
// Should be good enough fallback, user probably did not input valid url.
|
||||
title = href;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import { getFieldLinksSupplier, getLinksFromLogsField } from './linkSuppliers';
|
||||
import {
|
||||
applyFieldOverrides,
|
||||
ArrayVector,
|
||||
DataFrameView,
|
||||
dateTime,
|
||||
Field,
|
||||
FieldDisplay,
|
||||
FieldType,
|
||||
GrafanaTheme,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { getFieldLinksSupplier } from './linkSuppliers';
|
||||
import { applyFieldOverrides, DataFrameView, dateTime, FieldDisplay, GrafanaTheme, toDataFrame } from '@grafana/data';
|
||||
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
import { TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
|
||||
describe('getLinksFromLogsField', () => {
|
||||
describe('getFieldLinksSupplier', () => {
|
||||
let originalLinkSrv: LinkService;
|
||||
beforeAll(() => {
|
||||
// We do not need more here and TimeSrv is hard to setup fully.
|
||||
@@ -34,41 +24,6 @@ describe('getLinksFromLogsField', () => {
|
||||
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', () => {
|
||||
const data = applyFieldOverrides({
|
||||
data: [
|
||||
@@ -134,6 +89,7 @@ describe('getLinksFromLogsField', () => {
|
||||
overrides: [],
|
||||
},
|
||||
replaceVariables: (val: string) => val,
|
||||
getDataSourceSettingsByUid: (val: string) => ({} as any),
|
||||
timeZone: 'utc',
|
||||
theme: {} as GrafanaTheme,
|
||||
autoMinMax: true,
|
||||
|
||||
@@ -2,13 +2,11 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import {
|
||||
DataLink,
|
||||
DisplayValue,
|
||||
Field,
|
||||
FieldDisplay,
|
||||
formattedValueToString,
|
||||
getFieldDisplayValuesProxy,
|
||||
getTimeField,
|
||||
Labels,
|
||||
LinkModel,
|
||||
LinkModelSupplier,
|
||||
ScopedVar,
|
||||
ScopedVars,
|
||||
@@ -50,7 +48,6 @@ interface DataLinkScopedVars extends ScopedVars {
|
||||
/**
|
||||
* Link suppliers creates link models based on a link origin
|
||||
*/
|
||||
|
||||
export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<FieldDisplay> | undefined => {
|
||||
const links = value.field.links;
|
||||
if (!links || links.length === 0) {
|
||||
@@ -124,7 +121,7 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
|
||||
console.log('VALUE', value);
|
||||
}
|
||||
|
||||
return links.map(link => {
|
||||
return links.map((link: DataLink) => {
|
||||
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 {
|
||||
DataFrame,
|
||||
DataLink,
|
||||
DataLinkBuiltInVars,
|
||||
deprecationWarning,
|
||||
Field,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
VariableSuggestionsScope,
|
||||
urlUtil,
|
||||
textUtil,
|
||||
DataLink,
|
||||
} from '@grafana/data';
|
||||
|
||||
const timeRangeVars = [
|
||||
|
||||
@@ -4,8 +4,8 @@ import cx from 'classnames';
|
||||
import { LegacyForms } from '@grafana/ui';
|
||||
const { FormField } = LegacyForms;
|
||||
import { DerivedFieldConfig } from '../types';
|
||||
import { getLinksFromLogsField } from '../../../../features/panel/panellinks/linkSuppliers';
|
||||
import { ArrayVector, Field, FieldType, LinkModel } from '@grafana/data';
|
||||
import { getFieldLinksForExplore } from '../../../../features/explore/utils/links';
|
||||
|
||||
type Props = {
|
||||
derivedFields: DerivedFieldConfig[];
|
||||
@@ -94,7 +94,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
||||
let link: LinkModel<Field>;
|
||||
|
||||
if (field.url && value) {
|
||||
link = getLinksFromLogsField(
|
||||
link = getFieldLinksForExplore(
|
||||
{
|
||||
name: '',
|
||||
type: FieldType.string,
|
||||
@@ -103,8 +103,10 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
||||
links: [{ title: '', url: field.url }],
|
||||
},
|
||||
},
|
||||
0
|
||||
)[0].linkModel;
|
||||
0,
|
||||
(() => {}) as any,
|
||||
{} as any
|
||||
)[0];
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -113,6 +115,7 @@ function makeDebugFields(derivedFields: DerivedFieldConfig[], debugText: string)
|
||||
href: link && link.href,
|
||||
} as DebugField;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
name: field.name,
|
||||
error,
|
||||
|
||||
@@ -129,11 +129,13 @@ describe('enhanceDataFrame', () => {
|
||||
{
|
||||
matcherRegex: 'trace2=(\\w+)',
|
||||
name: 'trace2',
|
||||
url: 'test',
|
||||
datasourceUid: 'uid',
|
||||
},
|
||||
{
|
||||
matcherRegex: 'trace2=(\\w+)',
|
||||
name: 'trace2',
|
||||
url: 'test',
|
||||
datasourceUid: 'uid2',
|
||||
},
|
||||
],
|
||||
@@ -150,11 +152,13 @@ describe('enhanceDataFrame', () => {
|
||||
expect(fc.getFieldByName('trace2').config.links.length).toBe(2);
|
||||
expect(fc.getFieldByName('trace2').config.links[0]).toEqual({
|
||||
title: '',
|
||||
meta: { datasourceUid: 'uid' },
|
||||
internal: { datasourceUid: 'uid', query: { query: 'test' } },
|
||||
url: '',
|
||||
});
|
||||
expect(fc.getFieldByName('trace2').config.links[1]).toEqual({
|
||||
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> {
|
||||
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({
|
||||
// We do not know what title to give here so we count on presentation layer to create a title from metadata.
|
||||
title: '',
|
||||
// This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object
|
||||
url: derivedFieldConfig.url,
|
||||
// Having field.datasourceUid means it is an internal link.
|
||||
meta: derivedFieldConfig.datasourceUid
|
||||
? {
|
||||
datasourceUid: derivedFieldConfig.datasourceUid,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
GraphSeriesXY,
|
||||
DataFrame,
|
||||
ExploreMode,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { Emitter } from 'app/core/core';
|
||||
@@ -197,23 +198,6 @@ export interface ExploreUpdateState {
|
||||
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 {
|
||||
minInterval: string;
|
||||
maxDataPoints?: number;
|
||||
|
||||
Reference in New Issue
Block a user