mirror of
https://github.com/grafana/grafana.git
synced 2025-09-26 20:44:05 +08:00
Glue: Show data links only for fully interpolated correlations (#59052)
* Create data link filters for Explore * Add comments and make the code more explicit
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ArrayVector,
|
ArrayVector,
|
||||||
|
DataFrame,
|
||||||
DataLink,
|
DataLink,
|
||||||
dateTime,
|
dateTime,
|
||||||
Field,
|
Field,
|
||||||
@ -7,9 +8,11 @@ import {
|
|||||||
InterpolateFunction,
|
InterpolateFunction,
|
||||||
LinkModel,
|
LinkModel,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { setTemplateSrv } from '@grafana/runtime';
|
import { setTemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { initTemplateSrv } from '../../../../test/helpers/initTemplateSrv';
|
||||||
import { setContextSrv } from '../../../core/services/context_srv';
|
import { setContextSrv } from '../../../core/services/context_srv';
|
||||||
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||||
|
|
||||||
@ -17,18 +20,13 @@ import { getFieldLinksForExplore } from './links';
|
|||||||
|
|
||||||
describe('getFieldLinksForExplore', () => {
|
describe('getFieldLinksForExplore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setTemplateSrv({
|
setTemplateSrv(
|
||||||
replace(target, scopedVars, format) {
|
initTemplateSrv('key', [
|
||||||
return target ?? '';
|
{ type: 'custom', name: 'emptyVar', current: { value: null } },
|
||||||
},
|
{ type: 'custom', name: 'num', current: { value: 1 } },
|
||||||
getVariables() {
|
{ type: 'custom', name: 'test', current: { value: 'foo' } },
|
||||||
return [];
|
])
|
||||||
},
|
);
|
||||||
containsTemplate() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
updateTimeRange(timeRange: TimeRange) {},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns correct link model for external link', () => {
|
it('returns correct link model for external link', () => {
|
||||||
@ -36,7 +34,12 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
title: 'external',
|
title: 'external',
|
||||||
url: 'http://regionalhost',
|
url: 'http://regionalhost',
|
||||||
});
|
});
|
||||||
const links = getFieldLinksForExplore({ field, rowIndex: 0, splitOpenFn: jest.fn(), range });
|
const links = getFieldLinksForExplore({
|
||||||
|
field,
|
||||||
|
rowIndex: ROW_WITH_TEXT_VALUE.index,
|
||||||
|
splitOpenFn: jest.fn(),
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
|
||||||
expect(links[0].href).toBe('http://regionalhost');
|
expect(links[0].href).toBe('http://regionalhost');
|
||||||
expect(links[0].title).toBe('external');
|
expect(links[0].title).toBe('external');
|
||||||
@ -47,7 +50,12 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
title: '',
|
title: '',
|
||||||
url: 'http://regionalhost',
|
url: 'http://regionalhost',
|
||||||
});
|
});
|
||||||
const links = getFieldLinksForExplore({ field, rowIndex: 0, splitOpenFn: jest.fn(), range });
|
const links = getFieldLinksForExplore({
|
||||||
|
field,
|
||||||
|
rowIndex: ROW_WITH_TEXT_VALUE.index,
|
||||||
|
splitOpenFn: jest.fn(),
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
|
||||||
expect(links[0].href).toBe('http://regionalhost');
|
expect(links[0].href).toBe('http://regionalhost');
|
||||||
expect(links[0].title).toBe('regionalhost');
|
expect(links[0].title).toBe('regionalhost');
|
||||||
@ -69,7 +77,7 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const splitfn = jest.fn();
|
const splitfn = jest.fn();
|
||||||
const links = getFieldLinksForExplore({ field, rowIndex: 0, splitOpenFn: splitfn, range });
|
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, splitOpenFn: splitfn, range });
|
||||||
|
|
||||||
expect(links[0].href).toBe(
|
expect(links[0].href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
@ -102,7 +110,7 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
const links = getFieldLinksForExplore({ field, rowIndex: 0, range });
|
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range });
|
||||||
|
|
||||||
expect(links[0].href).toBe('http://regionalhost');
|
expect(links[0].href).toBe('http://regionalhost');
|
||||||
expect(links[0].title).toBe('external');
|
expect(links[0].title).toBe('external');
|
||||||
@ -121,11 +129,42 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
const links = getFieldLinksForExplore({ field, rowIndex: 0, range });
|
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range });
|
||||||
|
expect(links).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns internal links when target contains defined template variables', () => {
|
||||||
|
const { field, range, dataFrame } = setup({
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
internal: {
|
||||||
|
query: { query: 'query_1-${__data.fields.flux-dimensions}' },
|
||||||
|
datasourceUid: 'uid_1',
|
||||||
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame });
|
||||||
|
expect(links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no internal links when target contains empty template variables', () => {
|
||||||
|
const { field, range, dataFrame } = setup({
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
internal: {
|
||||||
|
query: { query: 'query_1-${__data.fields.flux-dimensions}' },
|
||||||
|
datasourceUid: 'uid_1',
|
||||||
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame });
|
||||||
expect(links).toHaveLength(0);
|
expect(links).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ROW_WITH_TEXT_VALUE = { value: 'foo', index: 0 };
|
||||||
|
const ROW_WITH_NULL_VALUE = { value: null, index: 1 };
|
||||||
|
|
||||||
function setup(link: DataLink, hasAccess = true) {
|
function setup(link: DataLink, hasAccess = true) {
|
||||||
setLinkSrv({
|
setLinkSrv({
|
||||||
getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel<any> {
|
getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel<any> {
|
||||||
@ -148,15 +187,19 @@ function setup(link: DataLink, hasAccess = true) {
|
|||||||
hasAccessToExplore: () => hasAccess,
|
hasAccessToExplore: () => hasAccess,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const field: Field<string> = {
|
const field: Field<string | null> = {
|
||||||
name: 'flux-dimensions',
|
name: 'flux-dimensions',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
values: new ArrayVector([]),
|
values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]),
|
||||||
config: {
|
config: {
|
||||||
links: [link],
|
links: [link],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dataFrame: DataFrame = toDataFrame({
|
||||||
|
fields: [field],
|
||||||
|
});
|
||||||
|
|
||||||
const range: TimeRange = {
|
const range: TimeRange = {
|
||||||
from: dateTime('2020-10-14T00:00:00'),
|
from: dateTime('2020-10-14T00:00:00'),
|
||||||
to: dateTime('2020-10-14T01:00:00'),
|
to: dateTime('2020-10-14T01:00:00'),
|
||||||
@ -166,5 +209,5 @@ function setup(link: DataLink, hasAccess = true) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { range, field };
|
return { range, field, dataFrame };
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,44 @@ import {
|
|||||||
DataFrame,
|
DataFrame,
|
||||||
getFieldDisplayValuesProxy,
|
getFieldDisplayValuesProxy,
|
||||||
SplitOpen,
|
SplitOpen,
|
||||||
|
DataLink,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||||
|
|
||||||
|
type DataLinkFilter = (link: DataLink, scopedVars: ScopedVars) => boolean;
|
||||||
|
|
||||||
|
const dataLinkHasRequiredPermissions = (link: DataLink) => {
|
||||||
|
return !link.internal || contextSrv.hasAccessToExplore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataLinkHasAllVariablesDefined = (link: DataLink, scopedVars: ScopedVars) => {
|
||||||
|
let hasAllRequiredVarDefined = true;
|
||||||
|
|
||||||
|
if (link.internal) {
|
||||||
|
let stringifiedQuery = '';
|
||||||
|
try {
|
||||||
|
stringifiedQuery = JSON.stringify(link.internal.query || {});
|
||||||
|
// Hook into format function to verify if all values are non-empty
|
||||||
|
// Format function is run on all existing field values allowing us to check it's value is non-empty
|
||||||
|
getTemplateSrv().replace(stringifiedQuery, scopedVars, (f: string) => {
|
||||||
|
hasAllRequiredVarDefined = hasAllRequiredVarDefined && !!f;
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasAllRequiredVarDefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed list of filters used in Explore. DataLinks that do not pass all the filters will not
|
||||||
|
* be passed back to the visualization.
|
||||||
|
*/
|
||||||
|
const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasAllVariablesDefined, dataLinkHasRequiredPermissions];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get links from the field of a dataframe and in addition check if there is associated
|
* Get links from the field of a dataframe and in addition check if there is associated
|
||||||
* metadata with datasource in which case we will add onClick to open the link in new split window. This assumes
|
* metadata with datasource in which case we will add onClick to open the link in new split window. This assumes
|
||||||
@ -56,13 +88,9 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.config.links) {
|
if (field.config.links) {
|
||||||
const links = [];
|
const links = field.config.links.filter((link) => {
|
||||||
|
return DATA_LINK_FILTERS.every((filter) => filter(link, scopedVars));
|
||||||
if (!contextSrv.hasAccessToExplore()) {
|
});
|
||||||
links.push(...field.config.links.filter((l) => !l.internal));
|
|
||||||
} else {
|
|
||||||
links.push(...field.config.links);
|
|
||||||
}
|
|
||||||
|
|
||||||
return links.map((link) => {
|
return links.map((link) => {
|
||||||
if (!link.internal) {
|
if (!link.internal) {
|
||||||
|
Reference in New Issue
Block a user