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>
pull/25942/head
Andrej Ocenas 5 years ago committed by GitHub
parent 463e8ffd92
commit 81d7cb1773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/grafana-data/src/dataframe/processDataFrame.ts
  2. 67
      packages/grafana-data/src/field/fieldOverrides.test.ts
  3. 56
      packages/grafana-data/src/field/fieldOverrides.ts
  4. 1
      packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx
  5. 21
      packages/grafana-data/src/types/dataLink.ts
  6. 22
      packages/grafana-data/src/types/explore.ts
  7. 11
      packages/grafana-data/src/types/fieldOverrides.ts
  8. 1
      packages/grafana-data/src/types/index.ts
  9. 39
      packages/grafana-data/src/utils/dataLinks.test.ts
  10. 107
      packages/grafana-data/src/utils/dataLinks.ts
  11. 23
      packages/grafana-data/src/utils/url.ts
  12. 2
      packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx
  13. 18
      packages/grafana-ui/src/components/Table/DefaultCell.tsx
  14. 1
      packages/grafana-ui/src/components/Table/Table.story.tsx
  15. 4
      public/app/core/utils/explore.test.ts
  16. 30
      public/app/core/utils/explore.ts
  17. 15
      public/app/core/utils/richHistory.ts
  18. 2
      public/app/features/dashboard/components/Inspector/InspectDataTab.tsx
  19. 2
      public/app/features/dashboard/state/DashboardMigrator.ts
  20. 7
      public/app/features/dashboard/state/PanelModel.test.ts
  21. 2
      public/app/features/dashboard/state/PanelModel.ts
  22. 10
      public/app/features/dashboard/state/PanelQueryRunner.test.ts
  23. 1
      public/app/features/dashboard/state/PanelQueryRunner.ts
  24. 2
      public/app/features/dashboard/state/runRequest.ts
  25. 4
      public/app/features/explore/Explore.tsx
  26. 4
      public/app/features/explore/TableContainer.test.tsx
  27. 25
      public/app/features/explore/TableContainer.tsx
  28. 3
      public/app/features/explore/state/actionTypes.ts
  29. 4
      public/app/features/explore/state/actions.test.ts
  30. 33
      public/app/features/explore/state/actions.ts
  31. 5
      public/app/features/explore/state/reducers.test.ts
  32. 11
      public/app/features/explore/utils/links.test.ts
  33. 121
      public/app/features/explore/utils/links.ts
  34. 52
      public/app/features/panel/panellinks/linkSuppliers.test.ts
  35. 27
      public/app/features/panel/panellinks/linkSuppliers.ts
  36. 2
      public/app/features/panel/panellinks/link_srv.ts
  37. 11
      public/app/plugins/datasource/loki/configuration/DebugSection.tsx
  38. 8
      public/app/plugins/datasource/loki/result_transformer.test.ts
  39. 21
      public/app/plugins/datasource/loki/result_transformer.ts
  40. 18
      public/app/types/explore.ts

@ -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);
const info: LinkModel<Field> = {
href,
title: replaceVariables(link.title || '', variables),
target: link.targetBlank ? '_blank' : undefined,
origin: field,
};
return info;
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,
});
},
// 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;
}
): Array<LinkModel<Field>> => {
const scopedVars: any = {};
scopedVars['__value'] = {
value: {
raw: field.values.get(rowIndex),
},
text: 'Raw value',
};
return {
...d.linkModel,
title,
};
}
return d.linkModel;
});
}
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()),
});
}
})
: [];
};
/**
* 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;

Loading…
Cancel
Save